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