1 // Written in the D programming language. 2 3 /** 4 5 6 This module contains simple controls widgets implementation. 7 8 9 TextWidget - static text 10 11 ImageWidget - image 12 13 Button - button with only text 14 15 ImageButton - button with only image 16 17 ImageTextButton - button with text and image 18 19 SwitchButton - switch widget 20 21 RadioButton - radio button 22 23 CheckBox - button with check mark 24 25 UrlImageTextButton - URL link button 26 27 CanvasWidget - for drawing arbitrary graphics 28 29 30 Note: ScrollBar and SliderWidget are moved to dlangui.widgets.scrollbar 31 32 Synopsis: 33 34 ---- 35 import dlangui.widgets.controls; 36 37 ---- 38 39 Copyright: Vadim Lopatin, 2014 40 License: Boost License 1.0 41 Authors: Vadim Lopatin, coolreader.org@gmail.com 42 */ 43 module dlangui.widgets.controls; 44 45 import dlangui.widgets.widget; 46 import dlangui.widgets.layouts; 47 import dlangui.core.stdaction; 48 49 private import std.algorithm; 50 private import std.conv : to; 51 private import std.utf : toUTF32; 52 53 /// vertical spacer to fill empty space in vertical layouts 54 class VSpacer : Widget { 55 this() { 56 styleId = STYLE_VSPACER; 57 } 58 //override void measure(int parentWidth, int parentHeight) { 59 // measuredContent(parentWidth, parentHeight, 8, 8); 60 //} 61 } 62 63 /// horizontal spacer to fill empty space in horizontal layouts 64 class HSpacer : Widget { 65 this() { 66 styleId = STYLE_HSPACER; 67 } 68 //override void measure(int parentWidth, int parentHeight) { 69 // measuredContent(parentWidth, parentHeight, 8, 8); 70 //} 71 } 72 73 /// static text widget 74 class TextWidget : Widget { 75 this(string ID = null, string textResourceId = null) { 76 super(ID); 77 styleId = STYLE_TEXT; 78 _text.id = textResourceId; 79 } 80 this(string ID, dstring rawText) { 81 super(ID); 82 styleId = STYLE_TEXT; 83 _text.value = rawText; 84 } 85 this(string ID, UIString uitext) { 86 super(ID); 87 styleId = STYLE_TEXT; 88 _text = uitext; 89 } 90 91 /// max lines to show 92 @property int maxLines() { return style.maxLines; } 93 /// set max lines to show 94 @property TextWidget maxLines(int n) { ownStyle.maxLines = n; return this; } 95 96 protected UIString _text; 97 /// get widget text 98 override @property dstring text() const { return _text; } 99 /// set text to show 100 override @property Widget text(dstring s) { 101 _text = s; 102 requestLayout(); 103 return this; 104 } 105 /// set text to show 106 override @property Widget text(UIString s) { 107 _text = s; 108 requestLayout(); 109 return this; 110 } 111 /// set text resource ID to show 112 @property Widget textResource(string s) { 113 _text = s; 114 requestLayout(); 115 return this; 116 } 117 118 private CalcSaver!(Font, dstring, uint, uint, int) _measureSaver; 119 120 override void measure(int parentWidth, int parentHeight) { 121 FontRef font = font(); 122 123 uint w; 124 if (maxLines == 1) 125 w = MAX_WIDTH_UNSPECIFIED; 126 else { 127 w = parentWidth - margins.left - margins.right - padding.left - padding.right; 128 if (maxWidth > 0 && maxWidth < w) 129 w = maxWidth - padding.left - padding.right; 130 } 131 uint flags = textFlags; 132 133 // optimization: do not measure if nothing changed 134 if (_measureSaver.check(font.get, text, w, flags, maxLines) || _needLayout) { 135 Point sz; 136 if (maxLines == 1) { 137 sz = font.textSize(text, w, 4, 0, flags); 138 } else { 139 sz = font.measureMultilineText(text, maxLines, w, 4, 0, flags); 140 } 141 // it's not very correct, but in such simple widget it doesn't make issues 142 measuredContent(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED, sz.x, sz.y); 143 _needLayout = false; 144 } 145 } 146 147 override void onDraw(DrawBuf buf) { 148 if (visibility != Visibility.Visible) 149 return; 150 super.onDraw(buf); 151 Rect rc = _pos; 152 applyMargins(rc); 153 auto saver = ClipRectSaver(buf, rc, alpha); 154 applyPadding(rc); 155 156 FontRef font = font(); 157 if (maxLines == 1) { 158 Point sz = font.textSize(text); 159 applyAlign(rc, sz); 160 font.drawText(buf, rc.left, rc.top, text, textColor, 4, 0, textFlags); 161 } else { 162 SimpleTextFormatter fmt; 163 Point sz = fmt.format(text, font, maxLines, rc.width, 4, 0, textFlags); 164 applyAlign(rc, sz); 165 // TODO: apply align to alignment lines 166 fmt.draw(buf, rc.left, rc.top, font, textColor); 167 } 168 } 169 } 170 171 /// static text widget with multiline text 172 class MultilineTextWidget : TextWidget { 173 this(string ID = null, string textResourceId = null) { 174 super(ID, textResourceId); 175 styleId = STYLE_MULTILINE_TEXT; 176 } 177 this(string ID, dstring rawText) { 178 super(ID, rawText); 179 styleId = STYLE_MULTILINE_TEXT; 180 } 181 this(string ID, UIString uitext) { 182 super(ID, uitext); 183 styleId = STYLE_MULTILINE_TEXT; 184 } 185 } 186 187 /// Switch (on/off) widget 188 class SwitchButton : Widget { 189 this(string ID = null) { 190 super(ID); 191 styleId = STYLE_SWITCH; 192 clickable = true; 193 focusable = true; 194 trackHover = true; 195 } 196 // called to process click and notify listeners 197 override protected bool handleClick() { 198 checked = !checked; 199 return super.handleClick(); 200 } 201 override void measure(int parentWidth, int parentHeight) { 202 DrawableRef img = backgroundDrawable; 203 int w = 0; 204 int h = 0; 205 if (!img.isNull) { 206 w = img.width; 207 h = img.height; 208 } 209 measuredContent(parentWidth, parentHeight, w, h); 210 } 211 212 override void onDraw(DrawBuf buf) { 213 if (visibility != Visibility.Visible) 214 return; 215 Rect rc = _pos; 216 applyMargins(rc); 217 auto saver = ClipRectSaver(buf, rc, alpha); 218 DrawableRef img = backgroundDrawable; 219 if (!img.isNull) { 220 Point sz; 221 sz.x = img.width; 222 sz.y = img.height; 223 applyAlign(rc, sz); 224 uint st = state; 225 img.drawTo(buf, rc, st); 226 } 227 _needDraw = false; 228 } 229 } 230 231 /// static image widget 232 class ImageWidget : Widget { 233 234 protected string _drawableId; 235 protected DrawableRef _drawable; 236 237 this(string ID = null, string drawableId = null) { 238 super(ID); 239 _drawableId = drawableId; 240 } 241 242 ~this() { 243 _drawable.clear(); 244 } 245 246 /// get drawable image id 247 @property string drawableId() { return _drawableId; } 248 /// set drawable image id 249 @property ImageWidget drawableId(string id) { 250 _drawableId = id; 251 _drawable.clear(); 252 requestLayout(); 253 return this; 254 } 255 /// get drawable 256 @property ref DrawableRef drawable() { 257 if (!_drawable.isNull) 258 return _drawable; 259 if (_drawableId !is null) 260 _drawable = drawableCache.get(overrideCustomDrawableId(_drawableId)); 261 return _drawable; 262 } 263 /// set custom drawable (not one from resources) 264 @property ImageWidget drawable(DrawableRef img) { 265 _drawable = img; 266 _drawableId = null; 267 return this; 268 } 269 /// set custom drawable (not one from resources) 270 @property ImageWidget drawable(string drawableId) { 271 if (_drawableId.equal(drawableId)) 272 return this; 273 _drawableId = drawableId; 274 _drawable.clear(); 275 requestLayout(); 276 return this; 277 } 278 279 /// set string property value, for ML loaders 280 mixin(generatePropertySettersMethodOverride("setStringProperty", "string", 281 "drawableId")); 282 283 /// handle theme change: e.g. reload some themed resources 284 override void onThemeChanged() { 285 super.onThemeChanged(); 286 if (_drawableId !is null) 287 _drawable.clear(); // remove cached drawable 288 } 289 290 override void measure(int parentWidth, int parentHeight) { 291 DrawableRef img = drawable; 292 int w = 0; 293 int h = 0; 294 if (!img.isNull) { 295 w = img.width; 296 h = img.height; 297 } 298 measuredContent(parentWidth, parentHeight, w, h); 299 } 300 301 override void onDraw(DrawBuf buf) { 302 if (visibility != Visibility.Visible) 303 return; 304 super.onDraw(buf); 305 Rect rc = _pos; 306 applyMargins(rc); 307 auto saver = ClipRectSaver(buf, rc, alpha); 308 applyPadding(rc); 309 DrawableRef img = drawable; 310 if (!img.isNull) { 311 Point sz; 312 sz.x = img.width; 313 sz.y = img.height; 314 applyAlign(rc, sz); 315 uint st = state; 316 img.drawTo(buf, rc, st); 317 } 318 } 319 } 320 321 /// button with image only 322 class ImageButton : ImageWidget { 323 /// constructor by id and icon resource id 324 this(string ID = null, string drawableId = null) { 325 super(ID, drawableId); 326 styleId = STYLE_BUTTON; 327 _drawableId = drawableId; 328 clickable = true; 329 focusable = true; 330 trackHover = true; 331 } 332 /// constructor from action 333 this(const Action a) { 334 this("imagebutton-action" ~ to!string(a.id), a.iconId); 335 action = a; 336 } 337 } 338 339 /// button with image working as trigger: check / uncheck occurs when pressing 340 class ImageCheckButton : ImageButton { 341 /// constructor by id and icon resource id 342 this(string ID = null, string drawableId = null) { 343 super(ID, drawableId); 344 styleId = "BUTTON_CHECK_TRANSPARENT"; 345 } 346 /// constructor from action 347 this(const Action a) { 348 super(a); 349 styleId = "BUTTON_CHECK_TRANSPARENT"; 350 } 351 352 // called to process click and notify listeners 353 override protected bool handleClick() { 354 checked = !checked; 355 return super.handleClick(); 356 } 357 } 358 359 /// button with image and text 360 class ImageTextButton : HorizontalLayout { 361 protected ImageWidget _icon; 362 protected TextWidget _label; 363 364 /// Get label text 365 override @property dstring text() const { return _label.text; } 366 /// Set label plain unicode string 367 override @property Widget text(dstring s) { _label.text = s; requestLayout(); return this; } 368 /// Set label string resource Id 369 override @property Widget text(UIString s) { _label.text = s; requestLayout(); return this; } 370 /// get text color (ARGB 32 bit value) 371 override @property uint textColor() const { return _label.textColor; } 372 /// set text color for widget - from string like "#5599CC" or "white" 373 override @property Widget textColor(string colorString) { _label.textColor(colorString); return this; } 374 /// set text color (ARGB 32 bit value) 375 override @property Widget textColor(uint value) { _label.textColor(value); return this; } 376 /// get text flags (bit set of TextFlag enum values) 377 override @property uint textFlags() { return _label.textFlags(); } 378 /// set text flags (bit set of TextFlag enum values) 379 override @property Widget textFlags(uint value) { _label.textFlags(value); return this; } 380 /// returns font face 381 override @property string fontFace() const { return _label.fontFace(); } 382 /// set font face for widget - override one from style 383 override @property Widget fontFace(string face) { _label.fontFace(face); return this; } 384 /// returns font style (italic/normal) 385 override @property bool fontItalic() const { return _label.fontItalic; } 386 /// set font style (italic/normal) for widget - override one from style 387 override @property Widget fontItalic(bool italic) { _label.fontItalic(italic); return this; } 388 /// returns font weight 389 override @property ushort fontWeight() const { return _label.fontWeight; } 390 /// set font weight for widget - override one from style 391 override @property Widget fontWeight(int weight) { _label.fontWeight(weight); return this; } 392 /// returns font size in pixels 393 override @property int fontSize() const { return _label.fontSize; } 394 /// Set label font size 395 override @property Widget fontSize(int size) { _label.fontSize(size); return this; } 396 /// returns font family 397 override @property FontFamily fontFamily() const { return _label.fontFamily; } 398 /// set font family for widget - override one from style 399 override @property Widget fontFamily(FontFamily family) { _label.fontFamily(family); return this; } 400 /// returns font set for widget using style or set manually 401 override @property FontRef font() const { return _label.font; } 402 403 /// Returns orientation: Vertical - image top, Horizontal - image left" 404 override @property Orientation orientation() const { 405 return super.orientation(); 406 } 407 408 /// Sets orientation: Vertical - image top, Horizontal - image left" 409 override @property LinearLayout orientation(Orientation value) { 410 if (!_icon || !_label) 411 return super.orientation(value); 412 if (value != orientation) { 413 super.orientation(value); 414 if (value == Orientation.Horizontal) { 415 _icon.alignment = Align.Left | Align.VCenter; 416 _label.alignment = Align.Right | Align.VCenter; 417 } else { 418 _icon.alignment = Align.Top | Align.HCenter; 419 _label.alignment = Align.Bottom | Align.HCenter; 420 } 421 } 422 return this; 423 } 424 425 protected void initialize(string drawableId, UIString caption) { 426 styleId = STYLE_BUTTON; 427 _icon = new ImageWidget("icon", drawableId); 428 _icon.styleId = STYLE_BUTTON_IMAGE; 429 _label = new TextWidget("label", caption); 430 _label.styleId = STYLE_BUTTON_LABEL; 431 _icon.state = State.Parent; 432 _label.state = State.Parent; 433 addChild(_icon); 434 addChild(_label); 435 clickable = true; 436 focusable = true; 437 trackHover = true; 438 } 439 440 this(string ID = null, string drawableId = null, string textResourceId = null) { 441 super(ID); 442 initialize(drawableId, UIString.fromId(textResourceId)); 443 } 444 445 this(string ID, string drawableId, dstring rawText) { 446 super(ID); 447 initialize(drawableId, UIString.fromRaw(rawText)); 448 } 449 450 /// constructor from action 451 this(const Action a) { 452 super("imagetextbutton-action" ~ to!string(a.id)); 453 initialize(a.iconId, a.labelValue); 454 action = a; 455 } 456 457 } 458 459 /// button - url 460 class UrlImageTextButton : ImageTextButton { 461 this(string ID, dstring labelText, string url, string icon = "applications-internet") { 462 super(ID, icon, labelText); 463 Action a = ACTION_OPEN_URL.clone(); 464 a.label = labelText; 465 a.stringParam = url; 466 _action = a; 467 styleId = null; 468 //_icon.styleId = STYLE_BUTTON_IMAGE; 469 //_label.styleId = STYLE_BUTTON_LABEL; 470 //_label.textFlags(TextFlag.Underline); 471 _label.styleId = "BUTTON_LABEL_LINK"; 472 static if (BACKEND_GUI) padding(Rect(3,3,3,3)); 473 } 474 } 475 476 /// button looking like URL, executing specified action 477 class LinkButton : ImageTextButton { 478 this(Action a) { 479 super(a); 480 styleId = null; 481 _label.styleId = "BUTTON_LABEL_LINK"; 482 static if (BACKEND_GUI) padding(Rect(3,3,3,3)); 483 } 484 } 485 486 487 /// checkbox 488 class CheckBox : ImageTextButton { 489 this(string ID = null, string textResourceId = null) { 490 super(ID, "btn_check", textResourceId); 491 } 492 this(string ID, dstring labelText) { 493 super(ID, "btn_check", labelText); 494 } 495 this(string ID, UIString label) { 496 super(ID, "btn_check", label); 497 } 498 override protected void initialize(string drawableId, UIString caption) { 499 super.initialize(drawableId, caption); 500 styleId = STYLE_CHECKBOX; 501 if (_icon) 502 _icon.styleId = STYLE_CHECKBOX_IMAGE; 503 if (_label) 504 _label.styleId = STYLE_CHECKBOX_LABEL; 505 checkable = true; 506 } 507 // called to process click and notify listeners 508 override protected bool handleClick() { 509 checked = !checked; 510 return super.handleClick(); 511 } 512 } 513 514 /// radio button 515 class RadioButton : ImageTextButton { 516 this(string ID = null, string textResourceId = null) { 517 super(ID, "btn_radio", textResourceId); 518 } 519 this(string ID, dstring labelText) { 520 super(ID, "btn_radio", labelText); 521 } 522 override protected void initialize(string drawableId, UIString caption) { 523 super.initialize(drawableId, caption); 524 styleId = STYLE_RADIOBUTTON; 525 if (_icon) 526 _icon.styleId = STYLE_RADIOBUTTON_IMAGE; 527 if (_label) 528 _label.styleId = STYLE_RADIOBUTTON_LABEL; 529 checkable = true; 530 } 531 532 private bool blockUnchecking = false; 533 534 void uncheckSiblings() { 535 Widget p = parent; 536 if (!p) 537 return; 538 for (int i = 0; i < p.childCount; i++) { 539 Widget child = p.child(i); 540 if (child is this) 541 continue; 542 RadioButton rb = cast(RadioButton)child; 543 if (rb) { 544 rb.blockUnchecking = true; 545 scope(exit) rb.blockUnchecking = false; 546 rb.checked = false; 547 } 548 } 549 } 550 551 // called to process click and notify listeners 552 override protected bool handleClick() { 553 uncheckSiblings(); 554 checked = true; 555 556 return super.handleClick(); 557 } 558 559 override protected void handleCheckChange(bool checked) { 560 if (!blockUnchecking) 561 uncheckSiblings(); 562 invalidate(); 563 checkChange(this, checked); 564 } 565 566 } 567 568 /// Text only button 569 class Button : Widget { 570 protected UIString _text; 571 override @property dstring text() const { return _text; } 572 override @property Widget text(dstring s) { _text = s; requestLayout(); return this; } 573 override @property Widget text(UIString s) { _text = s; requestLayout(); return this; } 574 @property Widget textResource(string s) { _text = s; requestLayout(); return this; } 575 /// empty parameter list constructor - for usage by factory 576 this() { 577 super(null); 578 initialize(UIString()); 579 } 580 581 private void initialize(UIString label) { 582 styleId = STYLE_BUTTON; 583 _text = label; 584 clickable = true; 585 focusable = true; 586 trackHover = true; 587 } 588 589 /// create with ID parameter 590 this(string ID) { 591 super(ID); 592 initialize(UIString()); 593 } 594 this(string ID, UIString label) { 595 super(ID); 596 initialize(label); 597 } 598 this(string ID, dstring label) { 599 super(ID); 600 initialize(UIString.fromRaw(label)); 601 } 602 this(string ID, string labelResourceId) { 603 super(ID); 604 initialize(UIString.fromId(labelResourceId)); 605 } 606 /// constructor from action 607 this(const Action a) { 608 this("button-action" ~ to!string(a.id), a.labelValue); 609 action = a; 610 } 611 612 override void measure(int parentWidth, int parentHeight) { 613 FontRef font = font(); 614 Point sz = font.textSize(text); 615 measuredContent(parentWidth, parentHeight, sz.x, sz.y); 616 } 617 618 override void onDraw(DrawBuf buf) { 619 if (visibility != Visibility.Visible) 620 return; 621 super.onDraw(buf); 622 Rect rc = _pos; 623 applyMargins(rc); 624 //buf.fillRect(_pos, backgroundColor); 625 applyPadding(rc); 626 auto saver = ClipRectSaver(buf, rc, alpha); 627 FontRef font = font(); 628 Point sz = font.textSize(text); 629 applyAlign(rc, sz); 630 font.drawText(buf, rc.left, rc.top, text, textColor, 4, 0, textFlags); 631 } 632 633 } 634 635 636 /// interface - slot for onClick 637 interface OnDrawHandler { 638 void doDraw(CanvasWidget canvas, DrawBuf buf, Rect rc); 639 } 640 641 /// canvas widget - draw on it either by overriding of doDraw() or by assigning of onDrawListener 642 class CanvasWidget : Widget { 643 644 Listener!OnDrawHandler onDrawListener; 645 646 this(string ID = null) { 647 super(ID); 648 } 649 650 override void measure(int parentWidth, int parentHeight) { 651 measuredContent(parentWidth, parentHeight, 0, 0); 652 } 653 654 void doDraw(DrawBuf buf, Rect rc) { 655 if (onDrawListener.assigned) 656 onDrawListener(this, buf, rc); 657 } 658 659 override void onDraw(DrawBuf buf) { 660 if (visibility != Visibility.Visible) 661 return; 662 super.onDraw(buf); 663 Rect rc = _pos; 664 applyMargins(rc); 665 auto saver = ClipRectSaver(buf, rc, alpha); 666 applyPadding(rc); 667 doDraw(buf, rc); 668 } 669 } 670 671 //import dlangui.widgets.metadata; 672 //mixin(registerWidgets!(Widget, TextWidget, MultilineTextWidget, Button, ImageWidget, ImageButton, ImageCheckButton, ImageTextButton, RadioButton, CheckBox, ScrollBar, HSpacer, VSpacer, CanvasWidget)());