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