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