1 // Written in the D programming language.
2 
3 /**
4 DLANGUI library.
5 
6 This module contains declaration of Widget class - base class for all widgets.
7 
8 
9 
10 Synopsis:
11 
12 ----
13 import dlangui.widgets.widget;
14 
15 ----
16 
17 Copyright: Vadim Lopatin, 2014
18 License:   $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
19 Authors:   $(WEB coolreader.org, Vadim Lopatin)
20 */
21 module dlangui.widgets.widget;
22 
23 public import dlangui.core.types;
24 public import dlangui.core.events;
25 public import dlangui.widgets.styles;
26 public import dlangui.graphics.drawbuf;
27 //public import dlangui.graphics.images;
28 public import dlangui.graphics.resources;
29 public import dlangui.graphics.fonts;
30 public import dlangui.core.i18n;
31 
32 //public import std.signals;
33 public import dlangui.core.signals;
34 
35 import dlangui.platforms.common.platform;
36 
37 import std.algorithm;
38 
39 
40 /// Visibility (see Android View Visibility)
41 enum Visibility : ubyte {
42     /// Visible on screen (default)
43     Visible,
44     /// Not visible, but occupies a space in layout
45     Invisible,
46     /// Completely hidden, as not has been added
47     Gone
48 }
49 
50 enum Orientation : ubyte {
51     Vertical,
52     Horizontal
53 }
54 
55 /// interface - slot for onClick
56 interface OnClickHandler {
57     bool onClick(Widget source);
58 }
59 
60 /// interface - slot for onCheckChanged
61 interface OnCheckHandler {
62     bool onCheckChanged(Widget source, bool checked);
63 }
64 
65 /// interface - slot for onFocusChanged
66 interface OnFocusHandler {
67     bool onFocusChanged(Widget source, bool focused);
68 }
69 
70 class Widget {
71     /// widget id
72     protected string _id;
73     /// current widget position, set by layout()
74     protected Rect _pos;
75     /// widget visibility: either Visible, Invisible, Gone
76     protected Visibility _visibility = Visibility.Visible; // visible by default
77     /// style id to lookup style in theme
78 	protected string _styleId;
79     /// own copy of style - to override some of style properties, null of no properties overriden
80 	protected Style _ownStyle;
81 
82     /// widget state (set of flags from State enum)
83     protected uint _state;
84 
85     /// width measured by measure()
86     protected int _measuredWidth;
87     /// height measured by measure()
88     protected int _measuredHeight;
89     /// true to force layout
90     protected bool _needLayout = true;
91     /// true to force redraw
92     protected bool _needDraw = true;
93     /// parent widget
94     protected Widget _parent;
95     /// window (to be used for top level widgets only!)
96     protected Window _window;
97 
98     /// does widget need to track mouse Hover
99     protected bool _trackHover;
100 
101     /// mouse movement processing flag (when true, widget will change Hover state while mouse is moving)
102     @property bool trackHover() const { return _trackHover; }
103     /// set new trackHover flag value (when true, widget will change Hover state while mouse is moving)
104     @property Widget trackHover(bool v) { _trackHover = v; return this; }
105 
106 	//private static int _instanceCount = 0;
107 	/// create widget, with optional id
108     this(string ID = null) {
109 		_id = ID;
110         _state = State.Enabled;
111 		//Log.d("Created widget, count = ", ++_instanceCount);
112     }
113 	~this() {
114 		if (_ownStyle !is null)
115 			destroy(_ownStyle);
116 		_ownStyle = null;
117 		//Log.d("Destroyed widget, count = ", --_instanceCount);
118 	}
119 
120     /// accessor to style - by lookup in theme by styleId (if style id is not set, theme base style will be used).
121 	protected @property const (Style) style() const {
122 		if (_ownStyle !is null)
123 			return _ownStyle;
124 		return currentTheme.get(_styleId);
125 	}
126     /// accessor to style - by lookup in theme by styleId (if style id is not set, theme base style will be used).
127 	protected @property const (Style) style(uint stateFlags) const {
128         const (Style) normalStyle = style();
129         if (stateFlags == State.Normal) // state is normal
130             return normalStyle;
131         const (Style) stateStyle = normalStyle.forState(stateFlags);
132         if (stateStyle !is normalStyle)
133             return stateStyle; // found style for state in current style
134         //// lookup state style in parent (one level max)
135         //const (Style) parentStyle = normalStyle.parentStyle;
136         //if (parentStyle is normalStyle)
137         //    return normalStyle; // no parent
138         //const (Style) parentStateStyle = parentStyle.forState(stateFlags);
139         //if (parentStateStyle !is parentStyle)
140         //    return parentStateStyle; // found style for state in parent
141 		return normalStyle; // fallback to current style
142 	}
143     /// returns style for current widget state
144     protected @property const(Style) stateStyle() const {
145         return style(state);
146     }
147 
148     /// enforces widget's own style - allows override some of style properties
149 	protected @property Style ownStyle() {
150 		if (_ownStyle is null)
151 			_ownStyle = currentTheme.modifyStyle(_styleId);
152 		return _ownStyle;
153 	}
154 
155     /// returns widget id, null if not set
156 	@property string id() const { return _id; }
157     /// set widget id
158     @property Widget id(string id) { _id = id; return this; }
159     /// compare widget id with specified value, returs true if matches
160     bool compareId(string id) const { return (_id !is null) && id.equal(_id); }
161 
162     /// widget state (set of flags from State enum)
163     @property uint state() const {
164         if ((_state & State.Parent) != 0 && _parent !is null)
165             return _parent.state;
166         return _state | State.WindowFocused; // TODO:
167     }
168     /// override to handle focus changes
169     protected void handleFocusChange(bool focused) {
170 		onFocusChangeListener(this, checked);
171     }
172     /// override to handle check changes
173     protected void handleCheckChange(bool checked) {
174 		onCheckChangeListener(this, checked);
175     }
176     /// set new widget state (set of flags from State enum)
177     @property Widget state(uint newState) {
178         if (newState != _state) {
179             uint oldState = _state;
180             _state = newState;
181             // need to redraw
182             invalidate();
183             // notify focus changes
184             if ((oldState & State.Focused) && !(newState & State.Focused))
185                 handleFocusChange(false);
186             else if (!(oldState & State.Focused) && (newState & State.Focused))
187                 handleFocusChange(true);
188             // notify checked changes
189             if ((oldState & State.Checked) && !(newState & State.Checked))
190                 handleCheckChange(false);
191             else if (!(oldState & State.Checked) && (newState & State.Checked))
192                 handleCheckChange(true);
193         }
194         return this;
195     }
196     /// add state flags (set of flags from State enum)
197     @property Widget setState(uint stateFlagsToSet) {
198         return state(state | stateFlagsToSet);
199     }
200     /// remove state flags (set of flags from State enum)
201     @property Widget resetState(uint stateFlagsToUnset) {
202         return state(state & ~stateFlagsToUnset);
203     }
204 
205 
206 
207     //======================================================
208     // Style related properties
209 
210     /// returns widget style id, null if not set
211 	@property string styleId() const { return _styleId; }
212     /// set widget style id
213     @property Widget styleId(string id) { _styleId = id; return this; }
214     /// get margins (between widget bounds and its background)
215     @property Rect margins() const { return style.margins; }
216     /// set margins for widget - override one from style
217     @property Widget margins(Rect rc) { ownStyle.margins = rc; return this; }
218     /// get padding (between background bounds and content of widget)
219     @property Rect padding() const { 
220 		// get max padding from style padding and background drawable padding
221 		Rect p = style.padding; 
222 		DrawableRef d = style.backgroundDrawable;
223 		if (!d.isNull) {
224 			Rect dp = style.backgroundDrawable.padding;
225 			if (p.left < dp.left)
226 				p.left = dp.left;
227 			if (p.right < dp.right)
228 				p.right = dp.right;
229 			if (p.top < dp.top)
230 				p.top = dp.top;
231 			if (p.bottom < dp.bottom)
232 				p.bottom = dp.bottom;
233 		}
234 		return p;
235 	}
236     /// set padding for widget - override one from style
237     @property Widget padding(Rect rc) { ownStyle.padding = rc; return this; }
238     /// returns background color
239     @property uint backgroundColor() const { return stateStyle.backgroundColor; }
240     /// set background color for widget - override one from style
241     @property Widget backgroundColor(uint color) { ownStyle.backgroundColor = color; return this; }
242     /// get text color (ARGB 32 bit value)
243     @property uint textColor() const { return stateStyle.textColor; }
244     /// set text color (ARGB 32 bit value)
245     @property Widget textColor(uint value) { ownStyle.textColor = value; return this; }
246     /// returns font face
247     @property string fontFace() const { return stateStyle.fontFace; }
248     /// set font face for widget - override one from style
249 	@property Widget fontFace(string face) { ownStyle.fontFace = face; return this; }
250     /// returns font style (italic/normal)
251     @property bool fontItalic() const { return stateStyle.fontItalic; }
252     /// set font style (italic/normal) for widget - override one from style
253 	@property Widget fontItalic(bool italic) { ownStyle.fontStyle = italic ? FONT_STYLE_ITALIC : FONT_STYLE_NORMAL; return this; }
254     /// returns font weight
255     @property ushort fontWeight() const { return stateStyle.fontWeight; }
256     /// set font weight for widget - override one from style
257 	@property Widget fontWeight(ushort weight) { ownStyle.fontWeight = weight; return this; }
258     /// returns font size in pixels
259     @property ushort fontSize() const { return stateStyle.fontSize; }
260     /// set font size for widget - override one from style
261 	@property Widget fontSize(ushort size) { ownStyle.fontSize = size; return this; }
262     /// returns font family
263     @property FontFamily fontFamily() const { return stateStyle.fontFamily; }
264     /// set font family for widget - override one from style
265     @property Widget fontFamily(FontFamily family) { ownStyle.fontFamily = family; return this; }
266     /// returns alignment (combined vertical and horizontal)
267     @property ubyte alignment() const { return style.alignment; }
268     /// sets alignment (combined vertical and horizontal)
269     @property Widget alignment(ubyte value) { ownStyle.alignment = value; return this; }
270     /// returns horizontal alignment
271     @property Align valign() { return cast(Align)(alignment & Align.VCenter); }
272     /// returns vertical alignment
273     @property Align halign() { return cast(Align)(alignment & Align.HCenter); }
274     /// returns font set for widget using style or set manually
275     @property FontRef font() const { return stateStyle.font; }
276 
277     /// returns widget content text (override to support this)
278     @property dstring text() { return ""; }
279     /// sets widget content text (override to support this)
280     @property Widget text(dstring s) { return this; }
281     /// sets widget content text (override to support this)
282     @property Widget text(ref UIString s) { return this; }
283 
284     //==================================================================
285     // Layout and drawing related methods
286 
287     /// returns true if layout is required for widget and its children
288     @property bool needLayout() { return _needLayout; }
289     /// returns true if redraw is required for widget and its children
290     @property bool needDraw() { return _needDraw; }
291     /// returns true is widget is being animated - need to call animate() and redraw
292     @property bool animating() { return false; }
293     /// animates window; interval is time left from previous draw, in hnsecs (1/10000 of second)
294     void animate(long interval) {
295     }
296     /// returns measured width (calculated during measure() call)
297     @property measuredWidth() { return _measuredWidth; }
298     /// returns measured height (calculated during measure() call)
299     @property measuredHeight() { return _measuredHeight; }
300     /// returns current width of widget in pixels
301     @property int width() { return _pos.width; }
302     /// returns current height of widget in pixels
303     @property int height() { return _pos.height; }
304     /// returns widget rectangle top position
305     @property int top() { return _pos.top; }
306     /// returns widget rectangle left position
307     @property int left() { return _pos.left; }
308     /// returns widget rectangle
309     @property Rect pos() { return _pos; }
310     /// returns min width constraint
311     @property int minWidth() { return style.minWidth; }
312     /// returns max width constraint (SIZE_UNSPECIFIED if no constraint set)
313     @property int maxWidth() { return style.maxWidth; }
314     /// returns min height constraint
315     @property int minHeight() { return style.minHeight; }
316     /// returns max height constraint (SIZE_UNSPECIFIED if no constraint set)
317     @property int maxHeight() { return style.maxHeight; }
318 
319     /// set max width constraint (SIZE_UNSPECIFIED for no constraint)
320     @property Widget maxWidth(int value) { ownStyle.maxWidth = value; return this; }
321     /// set max width constraint (0 for no constraint)
322     @property Widget minWidth(int value) { ownStyle.minWidth = value; return this; }
323     /// set max height constraint (SIZE_UNSPECIFIED for no constraint)
324     @property Widget maxHeight(int value) { ownStyle.maxHeight = value; return this; }
325     /// set max height constraint (0 for no constraint)
326     @property Widget minHeight(int value) { ownStyle.minHeight = value; return this; }
327 
328     /// returns layout width options (WRAP_CONTENT, FILL_PARENT, or some constant value)
329     @property int layoutWidth() { return style.layoutWidth; }
330     /// returns layout height options (WRAP_CONTENT, FILL_PARENT, or some constant value)
331     @property int layoutHeight() { return style.layoutHeight; }
332     /// returns layout weight (while resizing to fill parent, widget will be resized proportionally to this value)
333     @property int layoutWeight() { return style.layoutWeight; }
334 
335     /// sets layout width options (WRAP_CONTENT, FILL_PARENT, or some constant value)
336     @property Widget layoutWidth(int value) { ownStyle.layoutWidth = value; return this; }
337     /// sets layout height options (WRAP_CONTENT, FILL_PARENT, or some constant value)
338     @property Widget layoutHeight(int value) { ownStyle.layoutHeight = value; return this; }
339     /// sets layout weight (while resizing to fill parent, widget will be resized proportionally to this value)
340     @property Widget layoutWeight(int value) { ownStyle.layoutWeight = value; return this; }
341 
342     /// returns widget visibility (Visible, Invisible, Gone)
343     @property Visibility visibility() { return _visibility; }
344     /// sets widget visibility (Visible, Invisible, Gone)
345     @property Widget visibility(Visibility visible) {
346         if (_visibility != visible) {
347             if ((_visibility == Visibility.Gone) || (visible == Visibility.Gone))
348                 requestLayout();
349             else
350                 invalidate();
351             _visibility = visible;
352         }
353         return this;
354     }
355 
356     /// returns true if point is inside of this widget
357     bool isPointInside(int x, int y) {
358         return _pos.isPointInside(x, y);
359     }
360 
361 	/// return true if state has State.Enabled flag set
362     @property bool enabled() { return (state & State.Enabled) != 0; }
363 	/// change enabled state
364     @property Widget enabled(bool flg) { flg ? setState(State.Enabled) : resetState(State.Enabled); return this; }
365 
366     protected bool _clickable;
367 	/// when true, user can click this control, and get onClick listeners called
368     @property bool clickable() { return _clickable; }
369     @property Widget clickable(bool flg) { _clickable = flg; return this; }
370     @property bool canClick() { return _clickable && enabled && visible; }
371 
372     protected bool _checkable;
373 	/// when true, control supports Checked state
374     @property bool checkable() { return _checkable; }
375     @property Widget checkable(bool flg) { _checkable = flg; return this; }
376     @property bool canCheck() { return _checkable && enabled && visible; }
377 
378 
379     protected bool _checked;
380     /// get checked state
381     @property bool checked() { return (state & State.Checked) != 0; }
382     /// set checked state
383     @property Widget checked(bool flg) { 
384         if (flg != checked) {
385             if (flg) 
386                 setState(State.Checked); 
387             else 
388                 resetState(State.Checked); 
389             invalidate(); 
390         }
391         return this; 
392     }
393 
394     protected bool _focusable;
395     @property bool focusable() { return _focusable; }
396     @property Widget focusable(bool flg) { _focusable = flg; return this; }
397 
398     @property bool focused() {
399         return (window !is null && window.focusedWidget is this && (state & State.Focused));
400     }
401 
402 
403 
404     /// returns true if this widget and all its parents are visible
405     @property bool visible() {
406         if (visibility != Visibility.Visible)
407             return false;
408         if (parent is null)
409             return true;
410         return parent.visible;
411     }
412     /// returns true if widget is focusable and visible
413     @property bool canFocus() {
414         return focusable && visible;
415     }
416     /// sets focus to this widget or suitable focusable child, returns previously focused widget
417     Widget setFocus() {
418         if (window is null)
419             return null;
420         if (!visible)
421             return window.focusedWidget;
422         if (!canFocus) {
423             Widget w = findFocusableChild(true);
424             if (!w)
425                 w = findFocusableChild(false);
426             if (w)
427                 return window.setFocus(w);
428             // try to find focusable child
429             return window.focusedWidget;
430         }
431         return window.setFocus(this);
432     }
433     /// searches children for first focusable item, returns null if not found
434     Widget findFocusableChild(bool defaultOnly) {
435         for(int i = 0; i < childCount; i++) {
436             Widget w = child(i);
437             if (w.canFocus && (!defaultOnly || (w.state & State.Default) != 0))
438                 return w;
439             w = w.findFocusableChild(defaultOnly);
440             if (w !is null)
441                 return w;
442         }
443         if (canFocus)
444             return this;
445         return null;
446     }
447 
448     // =======================================================
449     // Events
450 
451     // called to process click and notify listeners
452     protected bool handleClick() {
453         bool res = onClickListener(this);
454         return res;
455     }
456 
457     /// process key event, return true if event is processed.
458     bool onKeyEvent(KeyEvent event) {
459 		if (canClick) {
460             // support onClick event initiated by Space or Return keys
461             if (event.action == KeyAction.KeyDown) {
462                 if (event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN) {
463                     setState(State.Pressed);
464                     return true;
465                 }
466             }
467             if (event.action == KeyAction.KeyUp) {
468                 if (event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN) {
469                     resetState(State.Pressed);
470                     handleClick();
471                     return true;
472                 }
473             }
474         }
475         return false;
476     }
477 
478     /// process mouse event; return true if event is processed by widget.
479     bool onMouseEvent(MouseEvent event) {
480         //Log.d("onMouseEvent ", id, " ", event.action, "  (", event.x, ",", event.y, ")");
481 		// support onClick
482 		if (canClick) {
483 	        if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
484 	            setState(State.Pressed);
485                 if (focusable)
486                     setFocus();
487 	            return true;
488 	        }
489 	        if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) {
490 	            resetState(State.Pressed);
491                 handleClick();
492 	            return true;
493 	        }
494 	        if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) {
495 	            resetState(State.Pressed);
496 	            resetState(State.Hovered);
497 	            return true;
498 	        }
499 	        if (event.action == MouseAction.FocusIn) {
500 	            setState(State.Pressed);
501 	            return true;
502 	        }
503 		}
504         if (focusable && event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
505             setFocus();
506         }
507         if (trackHover) {
508 	        if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) {
509                 if ((state & State.Hovered)) {
510                     Log.d("Hover off ", id);
511                     resetState(State.Hovered);
512                 }
513 	            return true;
514 	        }
515             if (event.action == MouseAction.Move) {
516                 if (!(state & State.Hovered)) {
517                     Log.d("Hover ", id);
518                     setState(State.Hovered);
519                 }
520 	            return true;
521             }
522             if (event.action == MouseAction.Leave) {
523                 Log.d("Leave ", id);
524 	            resetState(State.Hovered);
525 	            return true;
526             }
527         }
528 	    return false;
529     }
530 
531     // =======================================================
532     // Signals
533 
534 	/// on click event listener (bool delegate(Widget))
535     Signal!OnClickHandler onClickListener;
536 	/// checked state change event listener (bool delegate(Widget, bool))
537     Signal!OnCheckHandler onCheckChangeListener;
538 	/// focus state change event listener (bool delegate(Widget, bool))
539     Signal!OnFocusHandler onFocusChangeListener;
540 
541     // =======================================================
542     // Layout and measurement methods
543 
544     /// request relayout of widget and its children
545     void requestLayout() {
546         _needLayout = true;
547     }
548     /// request redraw
549     void invalidate() {
550         _needDraw = true;
551     }
552 
553     /// helper function for implement measure() when widget's content dimensions are known
554     protected void measuredContent(int parentWidth, int parentHeight, int contentWidth, int contentHeight) {
555         if (visibility == Visibility.Gone) {
556             _measuredWidth = _measuredHeight = 0;
557             return;
558         }
559         Rect m = margins;
560         Rect p = padding;
561         // summarize margins, padding, and content size
562         int dx = m.left + m.right + p.left + p.right + contentWidth;
563         int dy = m.top + m.bottom + p.top + p.bottom + contentHeight;
564         // apply min/max width and height constraints
565         int minw = minWidth;
566         int maxw = maxWidth;
567         int minh = minHeight;
568         int maxh = maxHeight;
569         if (dx < minw)
570             dx = minw;
571         if (dy < minh)
572             dy = minh;
573         if (maxw != SIZE_UNSPECIFIED && dx > maxw)
574             dx = maxw;
575         if (maxh != SIZE_UNSPECIFIED && dy > maxh)
576             dy = maxh;
577         // apply max parent size constraint
578         if (parentWidth != SIZE_UNSPECIFIED && dx > parentWidth)
579             dx = parentWidth;
580         if (parentHeight != SIZE_UNSPECIFIED && dy > parentHeight)
581             dy = parentHeight;
582         _measuredWidth = dx;
583         _measuredHeight = dy;
584     }
585 
586     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
587     void measure(int parentWidth, int parentHeight) { 
588         measuredContent(parentWidth, parentHeight, 0, 0);
589     }
590 
591     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
592     void layout(Rect rc) {
593         if (visibility == Visibility.Gone) {
594             return;
595         }
596         _pos = rc;
597         _needLayout = false;
598     }
599     /// Draw widget at its position to buffer
600     void onDraw(DrawBuf buf) {
601         if (visibility != Visibility.Visible)
602             return;
603         Rect rc = _pos;
604         applyMargins(rc);
605         DrawableRef bg = stateStyle.backgroundDrawable;
606 		if (!bg.isNull) {
607 	        bg.drawTo(buf, rc, state);
608 		}
609 	    applyPadding(rc);
610         _needDraw = false;
611     }
612 
613     /// Helper function: applies margins to rectangle
614     void applyMargins(ref Rect rc) {
615         Rect m = margins;
616         rc.left += m.left;
617         rc.top += m.top;
618         rc.bottom -= m.bottom;
619         rc.right -= m.right;
620     }
621     /// Helper function: applies padding to rectangle
622     void applyPadding(ref Rect rc) {
623         Rect m = padding;
624         rc.left += m.left;
625         rc.top += m.top;
626         rc.bottom -= m.bottom;
627         rc.right -= m.right;
628     }
629     /// Applies alignment for content of size sz - set rectangle rc to aligned value of content inside of initial value of rc.
630     void applyAlign(ref Rect rc, Point sz) {
631         Align va = valign;
632         Align ha = halign;
633         if (va == Align.Bottom) {
634             rc.top = rc.bottom - sz.y;
635         } else if (va == Align.VCenter) {
636             int dy = (rc.height - sz.y) / 2;
637             rc.top += dy;
638             rc.bottom = rc.top + sz.y;
639         } else {
640             rc.bottom = rc.top + sz.y;
641         }
642         if (ha == Align.Right) {
643             rc.left = rc.right - sz.x;
644         } else if (ha == Align.HCenter) {
645             int dx = (rc.width - sz.x) / 2;
646             rc.left += dx;
647             rc.right = rc.left + sz.x;
648         } else {
649             rc.right = rc.left + sz.x;
650         }
651     }
652 
653     // ===========================================================
654     // Widget hierarhy methods
655 
656     /// returns number of children of this widget
657     @property int childCount() { return 0; }
658     /// returns child by index
659     Widget child(int index) { return null; }
660     /// adds child, returns added item
661     Widget addChild(Widget item) { assert(false, "addChild: children not suported for this widget type"); }
662     /// removes child, returns removed item
663     Widget removeChild(int index) { assert(false, "removeChild: children not suported for this widget type"); }
664     /// removes child by ID, returns removed item
665     Widget removeChild(string id) { assert(false, "removeChild: children not suported for this widget type"); }
666     /// returns index of widget in child list, -1 if passed widget is not a child of this widget
667     int childIndex(Widget item) { return -1; }
668 
669 
670     /// returns true if item is child of this widget (when deepSearch == true - returns true if item is this widget or one of children inside children tree).
671     bool isChild(Widget item, bool deepSearch = true) {
672         if (deepSearch) {
673             // this widget or some widget inside children tree
674             if (item is this)
675                 return true;
676             for (int i = 0; i < childCount; i++) {
677                 if (child(i).isChild(item))
678                     return true;
679             }
680         } else {
681             // only one of children
682             for (int i = 0; i < childCount; i++) {
683                 if (item is child(i))
684                     return true;
685             }
686         }
687         return false;
688     }
689 
690     /// find child by id, returns null if not found
691     Widget childById(string id, bool deepSearch = true) { 
692         if (deepSearch) {
693             // search everywhere inside child tree
694             if (compareId(id))
695                 return this;
696             // lookup children
697             for (int i = childCount - 1; i >= 0; i--) {
698                 Widget res = child(i).childById(id);
699                 if (res !is null)
700                     return res;
701             }
702         } else {
703             // search only across children of this widget
704             for (int i = childCount - 1; i >= 0; i--)
705                 if (id.equal(child(i).id))
706                     return child(i);
707         }
708         // not found
709         return null; 
710     }
711 
712     /// returns parent widget, null for top level widget
713     @property Widget parent() { return _parent; }
714     /// sets parent for widget
715     @property Widget parent(Widget parent) { _parent = parent; return this; }
716     /// returns window (if widget or its parent is attached to window)
717     @property Window window() {
718         Widget p = this;
719         while (p !is null) {
720             if (p._window !is null)
721                 return p._window;
722             p = p.parent;
723         }
724         return null;
725     }
726     /// sets window (to be used for top level widget from Window implementation). TODO: hide it from API?
727     @property void window(Window window) { _window = window; }
728 
729 	
730 }
731 
732 /// widget list holder
733 struct WidgetList {
734     protected Widget[] _list;
735     protected int _count;
736     /// returns count of items
737     @property int count() const { return _count; }
738     /// get item by index
739     Widget get(int index) {
740         assert(index >= 0 && index < _count, "child index out of range");
741         return _list[index];
742     }
743     /// add item to list
744     Widget add(Widget item) {
745         if (_list.length <= _count) // resize
746             _list.length = _list.length < 4 ? 4 : _list.length * 2;
747         _list[_count++] = item;
748         return item;
749     }
750     /// add item to list
751     Widget insert(Widget item, int index = -1) {
752         if (index > _count || index < 0)
753             index = _count;
754         if (_list.length <= _count) // resize
755             _list.length = _list.length < 4 ? 4 : _list.length * 2;
756         for (int i = _count; i > index; i--)
757             _list[i] = _list[i - 1];
758         _list[index] = item;
759         _count++;
760         return item;
761     }
762     /// find child index for item, return -1 if not found
763     int indexOf(Widget item) {
764         for (int i = 0; i < _count; i++)
765             if (_list[i] == item)
766                 return i;
767         return -1;
768     }
769     /// find child index for item by id, return -1 if not found
770     int indexOf(string id) {
771         for (int i = 0; i < _count; i++)
772             if (_list[i].compareId(id))
773                 return i;
774         return -1;
775     }
776     /// remove item from list, return removed item
777     Widget remove(int index) {
778         assert(index >= 0 && index < _count, "child index out of range");
779         Widget item = _list[index];
780         for (int i = index; i < _count - 1; i++)
781             _list[i] = _list[i + 1];
782         _count--;
783         return item;
784     }
785     /// remove and destroy all items
786     void clear() {
787         for (int i = 0; i < _count; i++) {
788             destroy(_list[i]);
789             _list[i] = null;
790         }
791         _count = 0;
792     }
793     ~this() {
794         clear();
795     }
796 }
797 
798 /// base class for widgets which have children
799 class WidgetGroup : Widget {
800 
801 	this(string ID = null) {
802 		super(ID);
803 	}
804 
805     protected WidgetList _children;
806 
807     /// returns number of children of this widget
808     @property override int childCount() { return _children.count; }
809     /// returns child by index
810     override Widget child(int index) { return _children.get(index); }
811     /// adds child, returns added item
812     override Widget addChild(Widget item) { return _children.add(item).parent(this); }
813     /// removes child, returns removed item
814     override Widget removeChild(int index) { 
815         Widget res = _children.remove(index);
816         if (res !is null)
817             res.parent = null;
818         return res;
819     }
820     /// removes child by ID, returns removed item
821     override Widget removeChild(string ID) {
822         Widget res = null;
823         int index = _children.indexOf(ID);
824         if (index < 0)
825             return null;
826         res = _children.remove(index); 
827         if (res !is null)
828             res.parent = null;
829         return res;
830     }
831     /// returns index of widget in child list, -1 if passed widget is not a child of this widget
832     override int childIndex(Widget item) { return _children.indexOf(item); }
833 }