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)());