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