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