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