1 // Written in the D programming language.
2 
3 /**
4 This module contains declaration of themes and styles implementation.
5 
6 Style - style container
7 Theme - parent for all styles
8 
9 
10 Synopsis:
11 
12 ----
13 import dlangui.widgets.styles;
14 ----
15 
16 Recent changes:
17      Dimensions like fontSize, padding, margins, min/max width and height can be specified in points, e.g. minWidth = "3pt" margins="1pt,2pt,1pt,2pt"
18      % for font size, based on parent font size, e.g. fontSize="120.5%" means parentStyle.fontSize * 120.5 / 100.0;
19 
20 Copyright: Vadim Lopatin, 2014
21 License:   Boost License 1.0
22 Authors:   Vadim Lopatin, coolreader.org@gmail.com
23 */
24 module dlangui.widgets.styles;
25 
26 import dlangui.core.config;
27 
28 private import std.xml;
29 private import std.string;
30 private import std.algorithm;
31 
32 import dlangui.core.types;
33 import dlangui.graphics.colors;
34 import dlangui.graphics.fonts;
35 import dlangui.graphics.drawbuf;
36 import dlangui.graphics.resources;
37 
38 // Standard style constants
39 // Themes should define all of these styles in order to support all controls
40 /// standard style id for TextWidget
41 immutable string STYLE_TEXT = "TEXT";
42 /// standard style id for MultilineTextWidget
43 immutable string STYLE_MULTILINE_TEXT = "MULTILINE_TEXT";
44 /// standard style id for Button
45 immutable string STYLE_BUTTON = "BUTTON";
46 /// standard style id for Button label
47 immutable string STYLE_BUTTON_LABEL = "BUTTON_LABEL";
48 /// standard style id for Button image
49 immutable string STYLE_BUTTON_IMAGE = "BUTTON_IMAGE";
50 /// style id for transparent Button
51 immutable string STYLE_BUTTON_TRANSPARENT = "BUTTON_TRANSPARENT";
52 /// style id for Button w/o margins
53 immutable string STYLE_BUTTON_NOMARGINS = "BUTTON_NOMARGINS";
54 /// standard style id for Switch
55 immutable string STYLE_SWITCH = "SWITCH";
56 /// standard style id for CheckBox
57 immutable string STYLE_CHECKBOX = "CHECKBOX";
58 /// standard style id for CheckBox image
59 immutable string STYLE_CHECKBOX_IMAGE = "CHECKBOX_IMAGE";
60 /// standard style id for CheckBox label
61 immutable string STYLE_CHECKBOX_LABEL = "CHECKBOX_LABEL";
62 /// standard style id for RadioButton
63 immutable string STYLE_RADIOBUTTON = "RADIOBUTTON";
64 /// standard style id for RadioButton image
65 immutable string STYLE_RADIOBUTTON_IMAGE = "RADIOBUTTON_IMAGE";
66 /// standard style id for RadioButton label
67 immutable string STYLE_RADIOBUTTON_LABEL = "RADIOBUTTON_LABEL";
68 /// standard style id for HSpacer
69 immutable string STYLE_HSPACER = "HSPACER";
70 /// standard style id for VSpacer
71 immutable string STYLE_VSPACER = "VSPACER";
72 /// standard style id for ScrollBar
73 immutable string STYLE_SCROLLBAR = "SCROLLBAR";
74 /// standard style id for SliderWidget
75 immutable string STYLE_SLIDER = "SLIDER";
76 /// standard style id for ScrollBar button
77 immutable string STYLE_SCROLLBAR_BUTTON = "SCROLLBAR_BUTTON";
78 /// standard style id for ScrollBar button
79 immutable string STYLE_SCROLLBAR_BUTTON_TRANSPARENT = "SCROLLBAR_BUTTON_TRANSPARENT";
80 /// standard style id for ScrollBar page control
81 immutable string STYLE_PAGE_SCROLL = "PAGE_SCROLL";
82 /// standard style id for TabWidget
83 immutable string STYLE_TAB_WIDGET = "TAB_WIDGET";
84 /// standard style id for Tab with Up alignment
85 immutable string STYLE_TAB_UP = "TAB_UP";
86 /// standard style id for button of Tab with Up alignment
87 immutable string STYLE_TAB_UP_BUTTON = "TAB_UP_BUTTON";
88 /// standard style id for button of Tab with Up alignment
89 immutable string STYLE_TAB_UP_BUTTON_TEXT = "TAB_UP_BUTTON_TEXT";
90 /// standard style id for TabHost
91 immutable string STYLE_TAB_HOST = "TAB_HOST";
92 /// standard style id for PopupMenu
93 immutable string STYLE_POPUP_MENU = "POPUP_MENU";
94 /// standard style id for menu item
95 immutable string STYLE_MENU_ITEM = "MENU_ITEM";
96 /// standard style id for menu item label
97 immutable string STYLE_MENU_LABEL = "MENU_LABEL";
98 /// standard style id for menu item icon
99 immutable string STYLE_MENU_ICON = "MENU_ICON";
100 /// standard style id for menu item accelerators label
101 immutable string STYLE_MENU_ACCEL = "MENU_ACCEL";
102 /// standard style id for main menu item
103 immutable string STYLE_MAIN_MENU_ITEM = "MAIN_MENU_ITEM";
104 /// standard style id for main menu item label
105 immutable string STYLE_MAIN_MENU_LABEL = "MAIN_MENU_LABEL";
106 /// standard style id for main menu
107 immutable string STYLE_MAIN_MENU = "MAIN_MENU";
108 /// standard style id for list items
109 immutable string STYLE_LIST_ITEM = "LIST_ITEM";
110 /// standard style id for EditLine
111 immutable string STYLE_EDIT_LINE = "EDIT_LINE";
112 /// standard style id for EditBox
113 immutable string STYLE_EDIT_BOX = "EDIT_BOX";
114 /// standard style id for LogWidget
115 immutable string STYLE_LOG_WIDGET = "LOG_WIDGET";
116 /// standard style id for lists
117 immutable string STYLE_LIST_BOX = "LIST_BOX";
118 /// standard style id for StringGrid
119 immutable string STYLE_STRING_GRID = "STRING_GRID";
120 /// standard style id for FileDialog StringGrid
121 immutable string STYLE_FILE_DIALOG_GRID = "FILE_DIALOG_GRID";
122 /// standard style id for background similar to transparent button
123 immutable string STYLE_TRANSPARENT_BUTTON_BACKGROUND = "TRANSPARENT_BUTTON_BACKGROUND";
124 /// standard style id for GroupBox
125 immutable string STYLE_GROUP_BOX = "GROUP_BOX";
126 /// standard style id for GroupBox caption
127 immutable string STYLE_GROUP_BOX_CAPTION = "GROUP_BOX_CAPTION";
128 /// standard style id for ProgressBarWidget caption
129 immutable string STYLE_PROGRESS_BAR = "PROGRESS_BAR";
130 
131 /// standard style id for tree item
132 immutable string STYLE_TREE_ITEM = "TREE_ITEM";
133 /// standard style id for tree item body (icon + label)
134 immutable string STYLE_TREE_ITEM_BODY = "TREE_ITEM_BODY";
135 /// standard style id for tree item label
136 immutable string STYLE_TREE_ITEM_LABEL = "TREE_ITEM_LABEL";
137 /// standard style id for tree item icon
138 immutable string STYLE_TREE_ITEM_ICON = "TREE_ITEM_ICON";
139 /// standard style id for tree item expand icon
140 immutable string STYLE_TREE_ITEM_EXPAND_ICON = "TREE_ITEM_EXPAND_ICON";
141 /// standard style id for combo box
142 immutable string STYLE_COMBO_BOX = "COMBO_BOX";
143 /// standard style id for combo box button
144 immutable string STYLE_COMBO_BOX_BUTTON = "COMBO_BOX_BUTTON";
145 /// standard style id for combo box body (current item)
146 immutable string STYLE_COMBO_BOX_BODY = "COMBO_BOX_BODY";
147 /// standard style id for app frame status line
148 immutable string STYLE_STATUS_LINE = "STATUS_LINE";
149 
150 /// standard style id for dock host
151 immutable string STYLE_DOCK_HOST = "DOCK_HOST";
152 /// standard style id for dock host body
153 immutable string STYLE_DOCK_HOST_BODY = "DOCK_HOST_BODY";
154 /// standard style id for dock window caption
155 immutable string STYLE_DOCK_WINDOW_CAPTION = "DOCK_WINDOW_CAPTION";
156 /// standard style id for dock window
157 immutable string STYLE_DOCK_WINDOW = "DOCK_WINDOW";
158 /// standard style id for dock window caption label
159 immutable string STYLE_DOCK_WINDOW_CAPTION_LABEL = "DOCK_WINDOW_CAPTION_LABEL";
160 /// standard style id for dock window body
161 immutable string STYLE_DOCK_WINDOW_BODY = "DOCK_WINDOW_BODY";
162 /// standard style id for toolbar separator
163 immutable string STYLE_FLOATING_WINDOW = "FLOATING_WINDOW";
164 
165 /// standard style id for tab control in dock frame
166 immutable string STYLE_TAB_UP_DARK = "TAB_UP_DARK";
167 /// standard style id for tab control tab button in dock frame
168 immutable string STYLE_TAB_UP_BUTTON_DARK = "TAB_UP_BUTTON_DARK";
169 /// standard style id for tab control tab button text in dock frame
170 immutable string STYLE_TAB_UP_BUTTON_DARK_TEXT = "TAB_UP_BUTTON_DARK_TEXT";
171 /// standard style id for tab control in dock frame
172 immutable string STYLE_TAB_DOWN_DARK = "TAB_DOWN_DARK";
173 /// standard style id for tab control tab button in dock frame
174 immutable string STYLE_TAB_DOWN_BUTTON_DARK = "TAB_DOWN_BUTTON_DARK";
175 /// standard style id for tab control tab button text in dock frame
176 immutable string STYLE_TAB_DOWN_BUTTON_DARK_TEXT = "TAB_DOWN_BUTTON_DARK_TEXT";
177 
178 /// standard style id for tooltip popup
179 immutable string STYLE_TOOLTIP = "TOOLTIP";
180 
181 
182 /// standard style id for toolbars layout
183 immutable string STYLE_TOOLBAR_HOST = "TOOLBAR_HOST";
184 /// standard style id for toolbars
185 immutable string STYLE_TOOLBAR = "TOOLBAR";
186 /// standard style id for toolbar button
187 immutable string STYLE_TOOLBAR_BUTTON = "TOOLBAR_BUTTON";
188 /// standard style id for toolbar control, e.g. combobox
189 immutable string STYLE_TOOLBAR_CONTROL = "TOOLBAR_CONTROL";
190 /// standard style id for toolbar separator
191 immutable string STYLE_TOOLBAR_SEPARATOR = "TOOLBAR_SEPARATOR";
192 
193 /// standard style id for settings dialog tree
194 immutable string STYLE_SETTINGS_TREE = "SETTINGS_TREE";
195 /// standard style id for settings dialog content pages frame
196 immutable string STYLE_SETTINGS_PAGES = "SETTINGS_PAGES";
197 /// standard style id for settings dialog page title
198 immutable string STYLE_SETTINGS_PAGE_TITLE = "SETTINGS_PAGE_TITLE";
199 
200 /// window background color resource id
201 immutable string STYLE_COLOR_WINDOW_BACKGROUND = "window_background";
202 /// dialog background color resource id
203 immutable string STYLE_COLOR_DIALOG_BACKGROUND = "dialog_background";
204 
205 
206 
207 // Other style constants
208 
209 /// unspecified align - to take parent's value instead
210 enum ubyte ALIGN_UNSPECIFIED = 0;
211 /// unspecified font size constant - to take parent style property value
212 enum ushort FONT_SIZE_UNSPECIFIED = 0xFFFF;
213 /// unspecified font weight constant - to take parent style property value
214 enum ushort FONT_WEIGHT_UNSPECIFIED = 0x0000;
215 /// unspecified font style constant - to take parent style property value
216 enum ubyte FONT_STYLE_UNSPECIFIED = 0xFF;
217 /// normal font style constant
218 enum ubyte FONT_STYLE_NORMAL = 0x00;
219 /// italic font style constant
220 enum ubyte FONT_STYLE_ITALIC = 0x01;
221 /// use text flags from parent style
222 enum uint TEXT_FLAGS_UNSPECIFIED = uint.max;
223 /// use text flags from parent widget
224 enum uint TEXT_FLAGS_USE_PARENT = uint.max - 1;
225 /// to take layout weight from parent
226 enum int WEIGHT_UNSPECIFIED = -1;
227 
228 /// Align option bit constants
229 enum Align : ubyte {
230     /// alignment is not specified
231     Unspecified = ALIGN_UNSPECIFIED,
232     /// horizontally align to the left of box
233     Left = 1,
234     /// horizontally align to the right of box
235     Right = 2,
236     /// horizontally align to the center of box
237     HCenter = 1 | 2,
238     /// vertically align to the top of box
239     Top = 4,
240     /// vertically align to the bottom of box
241     Bottom = 8,
242     /// vertically align to the center of box
243     VCenter = 4 | 8,
244     /// align to the center of box (VCenter | HCenter)
245     Center = VCenter | HCenter,
246     /// align to the top left corner of box (Left | Top)
247     TopLeft = Left | Top,
248 }
249 
250 /// text drawing flag bits
251 enum TextFlag : uint {
252     /// text contains hot key prefixed with & char (e.g. "&File")
253     HotKeys = 1,
254     /// underline hot key when drawing
255     UnderlineHotKeys = 2,
256     /// underline hot key when drawing
257     UnderlineHotKeysWhenAltPressed = 4,
258     /// underline text when drawing
259     Underline = 8,
260     /// strikethrough text when drawing
261     StrikeThrough = 16 // TODO:
262 }
263 
264 struct DrawableAttributeList {
265     DrawableAttribute[string] _customDrawables;
266     ~this() {
267         clear();
268     }
269     void clear() {
270         foreach(key, ref value; _customDrawables) {
271             if (value) {
272                 destroy(value);
273                 value = null;
274             }
275         }
276         destroy(_customDrawables);
277         _customDrawables = null;
278     }
279     bool hasKey(string key) const {
280         return (key in _customDrawables) !is null;
281     }
282     ref DrawableRef drawable(string id) const {
283         return _customDrawables[id].drawable;
284     }
285     /// get custom drawable attribute
286     string drawableId(string id) const {
287         return _customDrawables[id].drawableId;
288     }
289     void set(string id, string resourceId) {
290         if (id in _customDrawables) {
291             _customDrawables[id].drawableId = resourceId;
292         } else {
293             _customDrawables[id] = new DrawableAttribute(id, resourceId);
294         }
295     }
296     void copyFrom(ref DrawableAttributeList v) {
297         clear();
298         foreach(key, value; v._customDrawables) {
299             set(key, value.drawableId);
300         }
301     }
302     void onThemeChanged() {
303         foreach(key, ref value; _customDrawables) {
304             if (value) {
305                 value.onThemeChanged();
306             }
307         }
308     }
309 }
310 
311 /// style properties
312 class Style {
313 protected:
314     string _id;
315     Theme _theme;
316     Style _parentStyle;
317     string _parentId;
318     uint _stateMask;
319     uint _stateValue;
320     ubyte _align = Align.TopLeft;
321     ubyte _fontStyle = FONT_STYLE_UNSPECIFIED;
322     FontFamily _fontFamily = FontFamily.Unspecified;
323     ushort _fontWeight = FONT_WEIGHT_UNSPECIFIED;
324     int _fontSize = FONT_SIZE_UNSPECIFIED;
325     uint _backgroundColor = COLOR_UNSPECIFIED;
326     uint _textColor = COLOR_UNSPECIFIED;
327     uint _textFlags = 0;
328     uint _alpha;
329     string _fontFace;
330     string _backgroundImageId;
331     string _boxShadow;
332     string _border;
333     Rect _padding;
334     Rect _margins;
335     int _minWidth = SIZE_UNSPECIFIED;
336     int _maxWidth = SIZE_UNSPECIFIED;
337     int _minHeight = SIZE_UNSPECIFIED;
338     int _maxHeight = SIZE_UNSPECIFIED;
339     int _layoutWidth = SIZE_UNSPECIFIED;
340     int _layoutHeight = SIZE_UNSPECIFIED;
341     int _layoutWeight = WEIGHT_UNSPECIFIED;
342     int _maxLines = SIZE_UNSPECIFIED;
343 
344     uint[] _focusRectColors;
345 
346     Style[] _substates;
347     Style[] _children;
348 
349     DrawableAttributeList _customDrawables;
350     uint[string] _customColors;
351     uint[string] _customLength;
352 
353     FontRef _font;
354     DrawableRef _backgroundDrawable;
355 
356 public:
357     void onThemeChanged() {
358         _font.clear();
359         _backgroundDrawable.clear();
360         foreach(s; _substates)
361             s.onThemeChanged();
362         foreach(s; _children)
363             s.onThemeChanged();
364         _customDrawables.onThemeChanged();
365     }
366 
367     @property const(Theme) theme() const {
368         if (_theme !is null)
369             return _theme;
370         return currentTheme;
371     }
372 
373     @property Theme theme() {
374         if (_theme !is null)
375             return _theme;
376         return currentTheme;
377     }
378 
379     @property string id() const { return _id; }
380     @property Style id(string id) {
381         this._id = id;
382         return this;
383     }
384 
385     /// access to parent style for this style
386     @property const(Style) parentStyle() const {
387         if (_parentStyle !is null)
388             return _parentStyle;
389         if (_parentId !is null && currentTheme !is null)
390             return currentTheme.get(_parentId);
391         return currentTheme;
392     }
393 
394     /// access to parent style for this style
395     @property Style parentStyle() {
396         if (_parentStyle !is null)
397             return _parentStyle;
398         if (_parentId !is null && currentTheme !is null)
399             return currentTheme.get(_parentId);
400         return currentTheme;
401     }
402 
403     @property string parentStyleId() {
404         return _parentId;
405     }
406 
407     @property Style parentStyleId(string id) {
408         _parentId = id;
409         if (_parentStyle)
410             if (currentTheme) {
411                 _parentStyle = currentTheme.get(_parentId);
412             }
413         return this;
414     }
415 
416     @property ref DrawableRef backgroundDrawable() const {
417         if (!(cast(Style)this)._backgroundDrawable.isNull)
418             return (cast(Style)this)._backgroundDrawable;
419         string image = backgroundImageId;
420         uint color = backgroundColor;
421         string borders = border;
422         string shadows = boxShadow;
423         if (borders !is null || shadows !is null) {
424             (cast(Style)this)._backgroundDrawable = new CombinedDrawable(color, image, borders, shadows);
425         } else if (image !is null) {
426             (cast(Style)this)._backgroundDrawable = drawableCache.get(image);
427         } else {
428             (cast(Style)this)._backgroundDrawable = isFullyTransparentColor(color) ? new EmptyDrawable() : new SolidFillDrawable(color);
429         }
430         return (cast(Style)this)._backgroundDrawable;
431     }
432 
433     /// get custom drawable attribute
434     ref DrawableRef customDrawable(string id) const {
435         if (_customDrawables.hasKey(id))
436             return _customDrawables.drawable(id);
437         return parentStyle ? parentStyle.customDrawable(id) : currentTheme.customDrawable(id);
438     }
439 
440     /// get custom drawable attribute
441     string customDrawableId(string id) const {
442         if (_customDrawables.hasKey(id))
443             return _customDrawables.drawableId(id);
444         return parentStyle ? parentStyle.customDrawableId(id) : currentTheme.customDrawableId(id);
445     }
446 
447     /// sets custom drawable attribute for style
448     Style setCustomDrawable(string id, string resourceId) {
449         _customDrawables.set(id, resourceId);
450         return this;
451     }
452 
453     /// get custom color attribute
454     uint customColor(string id, uint defColor = COLOR_TRANSPARENT) const {
455         if (id in _customColors)
456             return _customColors[id];
457         return parentStyle ? parentStyle.customColor(id, defColor) : currentTheme.customColor(id, defColor);
458     }
459 
460     /// sets custom color attribute for style
461     Style setCustomColor(string id, uint color) {
462         _customColors[id] = color;
463         return this;
464     }
465 
466     /// get custom length attribute
467     uint customLength(string id, uint defLength = 0) const {
468         if (id in _customLength)
469             return _customLength[id];
470         return parentStyle ? parentStyle.customLength(id, defLength) : currentTheme.customLength(id, defLength);
471     }
472 
473     /// sets custom length attribute for style
474     Style setCustomLength(string id, uint value) {
475         _customLength[id] = value;
476         return this;
477     }
478 
479     void clearCachedObjects() {
480         onThemeChanged();
481     }
482 
483     //===================================================
484     // font properties
485 
486     @property ref FontRef font() const {
487         if (!(cast(Style)this)._font.isNull)
488             return (cast(Style)this)._font;
489         string face = fontFace;
490         int size = fontSize;
491         ushort weight = fontWeight;
492         bool italic = fontItalic;
493         FontFamily family = fontFamily;
494         (cast(Style)this)._font = FontManager.instance.getFont(size, weight, italic, family, face);
495         return (cast(Style)this)._font;
496     }
497 
498     /// font size
499     @property FontFamily fontFamily() const {
500         if (_fontFamily != FontFamily.Unspecified)
501             return _fontFamily;
502         else
503             return parentStyle.fontFamily;
504     }
505 
506     /// font size
507     @property string fontFace() const {
508         if (_fontFace !is null)
509             return _fontFace;
510         else
511             return parentStyle.fontFace;
512     }
513 
514     /// font style - italic
515     @property bool fontItalic() const {
516         if (_fontStyle != FONT_STYLE_UNSPECIFIED)
517             return _fontStyle == FONT_STYLE_ITALIC;
518         else
519             return parentStyle.fontItalic;
520     }
521 
522     /// font weight
523     @property ushort fontWeight() const {
524         if (_fontWeight != FONT_WEIGHT_UNSPECIFIED)
525             return _fontWeight;
526         else
527             return parentStyle.fontWeight;
528     }
529 
530     /// font size
531     @property int fontSize() const {
532         if (_fontSize != FONT_SIZE_UNSPECIFIED) {
533             if (_fontSize & SIZE_IN_PERCENTS_FLAG)
534                 return parentStyle.fontSize * (_fontSize ^ SIZE_IN_PERCENTS_FLAG) / 10000;
535             return toPixels(_fontSize);
536         } else
537             return parentStyle.fontSize;
538     }
539 
540     /// box shadow
541     @property string boxShadow() const {
542         if (_boxShadow !is null)
543             return _boxShadow;
544         else {
545             return parentStyle.boxShadow;
546         }
547     }
548 
549     /// border
550     @property string border() const {
551         if (_border !is null)
552             return _border;
553         else {
554             return parentStyle.border;
555         }
556     }
557 
558     //===================================================
559     // layout parameters: margins / padding
560 
561     /// padding
562     @property const(Rect) padding() const {
563         if (_stateMask || _padding.left == SIZE_UNSPECIFIED)
564             return toPixels(parentStyle._padding);
565         return toPixels(_padding);
566     }
567 
568     /// margins
569     @property const(Rect) margins() const {
570         if (_stateMask || _margins.left == SIZE_UNSPECIFIED)
571             return toPixels(parentStyle._margins);
572         return toPixels(_margins);
573     }
574 
575     /// alpha (0=opaque .. 255=transparent)
576     @property uint alpha() const {
577         if (_alpha != COLOR_UNSPECIFIED)
578             return _alpha;
579         else
580             return parentStyle.alpha;
581     }
582 
583     /// text color
584     @property uint textColor() const {
585         if (_textColor != COLOR_UNSPECIFIED)
586             return _textColor;
587         else
588             return parentStyle.textColor;
589     }
590 
591     /// text color
592     @property int maxLines() const {
593         if (_maxLines != SIZE_UNSPECIFIED)
594             return _maxLines;
595         else
596             return parentStyle.maxLines;
597     }
598 
599     /// text flags
600     @property uint textFlags() const {
601         if (_textFlags != TEXT_FLAGS_UNSPECIFIED)
602             return _textFlags;
603         else
604             return parentStyle.textFlags;
605     }
606 
607     //===================================================
608     // background
609 
610     /// background color
611     @property uint backgroundColor() const {
612         if (_backgroundColor != COLOR_UNSPECIFIED)
613             return _backgroundColor;
614         else
615             return parentStyle.backgroundColor;
616     }
617 
618     /// background image id
619     @property string backgroundImageId() const {
620         if (_backgroundImageId == COLOR_DRAWABLE)
621             return null;
622         else if (_backgroundImageId !is null)
623             return _backgroundImageId;
624         else
625             return parentStyle.backgroundImageId;
626     }
627 
628     //===================================================
629     // size restrictions
630 
631     /// minimal width constraint, 0 if limit is not set
632     @property uint minWidth() const {
633         if (_minWidth != SIZE_UNSPECIFIED)
634             return toPixels(_minWidth);
635         else
636             return parentStyle.minWidth;
637     }
638     /// max width constraint, returns SIZE_UNSPECIFIED if limit is not set
639     @property uint maxWidth() const {
640         if (_maxWidth != SIZE_UNSPECIFIED)
641             return toPixels(_maxWidth);
642         else
643             return parentStyle.maxWidth;
644     }
645     /// minimal height constraint, 0 if limit is not set
646     @property uint minHeight() const {
647         if (_minHeight != SIZE_UNSPECIFIED)
648             return toPixels(_minHeight);
649         else
650             return parentStyle.minHeight;
651     }
652     /// max height constraint, SIZE_UNSPECIFIED if limit is not set
653     @property uint maxHeight() const {
654         if (_maxHeight != SIZE_UNSPECIFIED)
655             return toPixels(_maxHeight);
656         else
657             return parentStyle.maxHeight;
658     }
659     /// set min width constraint
660     @property Style minWidth(int value) {
661         _minWidth = value;
662         return this;
663     }
664     /// set max width constraint
665     @property Style maxWidth(int value) {
666         _maxWidth = value;
667         return this;
668     }
669     /// set min height constraint
670     @property Style minHeight(int value) {
671         _minHeight = value;
672         return this;
673     }
674     /// set max height constraint
675     @property Style maxHeight(int value) {
676         _maxHeight = value;
677         return this;
678     }
679 
680 
681     /// layout width parameter
682     @property uint layoutWidth() const {
683         if (_layoutWidth != SIZE_UNSPECIFIED)
684             return _layoutWidth;
685         else
686             return parentStyle.layoutWidth;
687     }
688 
689     /// layout height parameter
690     @property uint layoutHeight() const {
691         if (_layoutHeight != SIZE_UNSPECIFIED)
692             return _layoutHeight;
693         else
694             return parentStyle.layoutHeight;
695     }
696 
697     /// layout weight parameter
698     @property uint layoutWeight() const {
699         if (_layoutWeight != WEIGHT_UNSPECIFIED)
700             return _layoutWeight;
701         else
702             return parentStyle.layoutWeight;
703     }
704 
705     /// set layout height
706     @property Style layoutHeight(int value) {
707         _layoutHeight = value;
708         return this;
709     }
710     /// set layout width
711     @property Style layoutWidth(int value) {
712         _layoutWidth = value;
713         return this;
714     }
715     /// set layout weight
716     @property Style layoutWeight(int value) {
717         _layoutWeight = value;
718         return this;
719     }
720 
721     //===================================================
722     // alignment
723 
724     /// get full alignment (both vertical and horizontal)
725     @property ubyte alignment() const {
726         if (_align != Align.Unspecified)
727             return _align;
728         else
729             return parentStyle.alignment;
730     }
731     /// vertical alignment: Top / VCenter / Bottom
732     @property ubyte valign() const { return alignment & Align.VCenter; }
733     /// horizontal alignment: Left / HCenter / Right
734     @property ubyte halign() const { return alignment & Align.HCenter; }
735 
736     /// set alignment
737     @property Style alignment(ubyte value) {
738         _align = value;
739         return this;
740     }
741 
742     @property Style fontFace(string face) {
743         if (_fontFace != face)
744             clearCachedObjects();
745         _fontFace = face;
746         return this;
747     }
748 
749     @property Style fontFamily(FontFamily family) {
750         if (_fontFamily != family)
751             clearCachedObjects();
752         _fontFamily = family;
753         return this;
754     }
755 
756     @property Style fontStyle(ubyte style) {
757         if (_fontStyle != style)
758             clearCachedObjects();
759         _fontStyle = style;
760         return this;
761     }
762 
763     @property Style fontWeight(ushort weight) {
764         if (_fontWeight != weight)
765             clearCachedObjects();
766         _fontWeight = weight;
767         return this;
768     }
769 
770     @property Style fontSize(int size) {
771         if (_fontSize != size)
772             clearCachedObjects();
773         _fontSize = size;
774         return this;
775     }
776 
777     @property Style textColor(uint color) {
778         _textColor = color;
779         return this;
780     }
781 
782     @property Style maxLines(int lines) {
783         _maxLines = lines;
784         return this;
785     }
786 
787     @property Style alpha(uint alpha) {
788         _alpha = alpha;
789         return this;
790     }
791 
792     @property Style textFlags(uint flags) {
793         _textFlags = flags;
794         return this;
795     }
796 
797     @property Style backgroundColor(uint color) {
798         _backgroundColor = color;
799         _backgroundImageId = COLOR_DRAWABLE;
800         _backgroundDrawable.clear();
801         return this;
802     }
803 
804     @property Style backgroundImageId(string image) {
805         _backgroundImageId = image;
806         _backgroundDrawable.clear();
807         return this;
808     }
809 
810     @property Style boxShadow(string s) {
811         _boxShadow = s;
812         _backgroundDrawable.clear();
813         return this;
814     }
815 
816     @property Style border(string s) {
817         _border = s;
818         _backgroundDrawable.clear();
819         return this;
820     }
821 
822     @property Style margins(Rect rc) {
823         _margins = rc;
824         return this;
825     }
826 
827     Style setMargins(int left, int top, int right, int bottom) {
828         _margins.left = left;
829         _margins.top = top;
830         _margins.right = right;
831         _margins.bottom = bottom;
832         return this;
833     }
834 
835     @property Style padding(Rect rc) {
836         _padding = rc;
837         return this;
838     }
839 
840     /// returns colors to draw focus rectangle (one for solid, two for vertical gradient) or null if no focus rect should be drawn for style
841     @property const(uint[]) focusRectColors() const {
842         if (_focusRectColors) {
843             if (_focusRectColors.length == 1 && _focusRectColors[0] == COLOR_UNSPECIFIED)
844                 return null;
845             return cast(const)_focusRectColors;
846         }
847         return parentStyle.focusRectColors;
848     }
849 
850     /// sets colors to draw focus rectangle or null if no focus rect should be drawn for style
851     @property Style focusRectColors(uint[] colors) {
852         _focusRectColors = colors;
853         return this;
854     }
855 
856     Style setPadding(int left, int top, int right, int bottom) {
857         _padding.left = left;
858         _padding.top = top;
859         _padding.right = right;
860         _padding.bottom = bottom;
861         return this;
862     }
863 
864     debug private static __gshared int _instanceCount;
865     debug @property static int instanceCount() { return _instanceCount; }
866 
867     this(Theme theme, string id) {
868         _theme = theme;
869         _parentStyle = theme;
870         _id = id;
871         debug _instanceCount++;
872         //Log.d("Created style ", _id, ", count=", ++_instanceCount);
873     }
874 
875 
876     ~this() {
877         foreach(ref Style item; _substates) {
878             //Log.d("Destroying substate");
879             destroy(item);
880             item = null;
881         }
882         _substates.destroy();
883         foreach(ref Style item; _children) {
884             destroy(item);
885             item = null;
886         }
887         _children.destroy();
888         _backgroundDrawable.clear();
889         _font.clear();
890         destroy(_customDrawables);
891         debug _instanceCount--;
892         //Log.d("Destroyed style ", _id, ", parentId=", _parentId, ", state=", _stateMask, ", count=", --_instanceCount);
893     }
894 
895     /// create named substyle of this style
896     Style createSubstyle(string id) {
897         Style child = (_theme !is null ? _theme : currentTheme).createSubstyle(id);
898         child._parentStyle = this;
899         _children ~= child;
900         return child;
901     }
902 
903     /// create state substyle for this style
904     Style createState(uint stateMask = 0, uint stateValue = 0) {
905         assert(stateMask != 0);
906         debug(styles) Log.d("Creating substate ", stateMask);
907         Style child = (_theme !is null ? _theme : currentTheme).createSubstyle(null);
908         child._parentStyle = this;
909         child._stateMask = stateMask;
910         child._stateValue = stateValue;
911         child._backgroundColor = COLOR_UNSPECIFIED;
912         child._textColor = COLOR_UNSPECIFIED;
913         child._textFlags = TEXT_FLAGS_UNSPECIFIED;
914         _substates ~= child;
915         return child;
916     }
917 
918     Style clone() {
919         Style res = new Style(_theme, null);
920         res._stateMask = _stateMask;
921         res._stateValue = _stateValue;
922         res._align = _align;
923         res._fontStyle = _fontStyle;
924         res._fontFamily = _fontFamily;
925         res._fontWeight = _fontWeight;
926         res._fontSize = _fontSize;
927         res._backgroundColor = _backgroundColor;
928         res._textColor = _textColor;
929         res._textFlags = _textFlags;
930         res._alpha = _alpha;
931         res._fontFace = _fontFace;
932         res._backgroundImageId = _backgroundImageId;
933         res._boxShadow = _boxShadow;
934         res._border = _border;
935         res._padding = _padding;
936         res._margins = _margins;
937         res._minWidth = _minWidth;
938         res._maxWidth = _maxWidth;
939         res._minHeight = _minHeight;
940         res._maxHeight = _maxHeight;
941         res._layoutWidth = _layoutWidth;
942         res._layoutHeight = _layoutHeight;
943         res._layoutWeight = _layoutWeight;
944         res._maxLines = _maxLines;
945 
946         res._focusRectColors = _focusRectColors.dup;
947 
948         res._customDrawables.copyFrom(_customDrawables);
949         res._customColors = _customColors.dup;
950         res._customLength = _customLength.dup;
951         return res;
952     }
953 
954     /// find exact existing state style or create new if no matched styles found
955     Style getOrCreateState(uint stateMask = 0, uint stateValue = 0) {
956         if (stateValue == State.Normal)
957             return this;
958         foreach(item; _substates) {
959             if ((item._stateMask == stateMask) && (item._stateValue == stateValue))
960                 return item;
961         }
962         return createState(stateMask, stateValue);
963     }
964 
965     /// find substyle based on widget state (e.g. focused, pressed, ...)
966     Style forState(uint state) {
967         if (state == State.Normal)
968             return this;
969         //Log.d("forState ", state, " styleId=", _id, " substates=", _substates.length);
970         if (parentStyle !is null && _substates.length == 0 && parentStyle._substates.length > 0) //id is null &&
971             return parentStyle.forState(state);
972         foreach(item; _substates) {
973             if ((item._stateMask & state) == item._stateValue)
974                 return item;
975         }
976         return this; // fallback to current style
977     }
978 
979     /// find substyle based on widget state (e.g. focused, pressed, ...)
980     const(Style) forState(uint state) const {
981         if (state == State.Normal)
982             return this;
983         //Log.d("forState ", state, " styleId=", _id, " substates=", _substates.length);
984         if (parentStyle !is null && _substates.length == 0 && parentStyle._substates.length > 0) //id is null &&
985             return parentStyle.forState(state);
986         foreach(item; _substates) {
987             if ((item._stateMask & state) == item._stateValue)
988                 return item;
989         }
990         return this; // fallback to current style
991     }
992 
993 }
994 
995 /// Theme - root for style hierarhy.
996 class Theme : Style {
997     protected Style[string] _byId;
998 
999     this(string id) {
1000         super(this, id);
1001         _parentStyle = null;
1002         _backgroundColor = COLOR_TRANSPARENT; // transparent
1003         _textColor = 0x000000; // black
1004         _maxLines = 1;
1005         _align = Align.TopLeft;
1006         _fontSize = 9 | SIZE_IN_POINTS_FLAG; // TODO: from settings or screen properties / DPI
1007         _fontStyle = FONT_STYLE_NORMAL;
1008         _fontWeight = 400;
1009         _fontFace = "Arial"; // TODO: from settings
1010         //_fontFace = "Verdana"; // TODO: from settings
1011         _fontFamily = FontFamily.SansSerif;
1012         _minHeight = 0;
1013         _minWidth = 0;
1014         _layoutWidth = WRAP_CONTENT;
1015         _layoutHeight = WRAP_CONTENT;
1016         _layoutWeight = 1;
1017     }
1018 
1019     ~this() {
1020         //Log.d("Theme destructor");
1021         if (unknownStyleIds.length > 0) {
1022             Log.e("Unknown style statistics: ", unknownStyleIds);
1023         }
1024         foreach(ref Style item; _byId) {
1025             destroy(item);
1026             item = null;
1027         }
1028         _byId.destroy();
1029     }
1030 
1031     override void onThemeChanged() {
1032         super.onThemeChanged();
1033         foreach(key, value; _byId) {
1034             value.onThemeChanged();
1035         }
1036     }
1037 
1038     /// create wrapper style which will have currentTheme.get(id) as parent instead of fixed parent - to modify some base style properties in widget
1039     Style modifyStyle(string id) {
1040         Style style = new Style(null, null);
1041         style._parentId = id;
1042         style._align = Align.Unspecified; // inherit
1043         style._padding.left = SIZE_UNSPECIFIED; // inherit
1044         style._margins.left = SIZE_UNSPECIFIED; // inherit
1045         style._textColor = COLOR_UNSPECIFIED; // inherit
1046         style._textFlags = TEXT_FLAGS_UNSPECIFIED; // inherit
1047         Style parent = get(id);
1048         if (parent) {
1049             foreach(item; parent._substates) {
1050                 Style substate = item.clone();
1051                 substate._parentStyle = style;
1052                 style._substates ~= substate;
1053             }
1054         }
1055         return style;
1056     }
1057 
1058     // ================================================
1059     // override to avoid infinite recursion
1060     /// font size
1061     @property override string backgroundImageId() const {
1062         return _backgroundImageId;
1063     }
1064     /// box shadow
1065     @property override string boxShadow() const {
1066         return _boxShadow;
1067     }
1068     /// border
1069     @property override string border() const {
1070         return _border;
1071     }
1072     /// minimal width constraint, 0 if limit is not set
1073     @property override uint minWidth() const {
1074         return _minWidth;
1075     }
1076     /// max width constraint, returns SIZE_UNSPECIFIED if limit is not set
1077     @property override uint maxWidth() const {
1078         return _maxWidth;
1079     }
1080     /// minimal height constraint, 0 if limit is not set
1081     @property override uint minHeight() const {
1082         return _minHeight;
1083     }
1084     /// max height constraint, SIZE_UNSPECIFIED if limit is not set
1085     @property override uint maxHeight() const {
1086         return _maxHeight;
1087     }
1088 
1089     private DrawableRef _emptyDrawable;
1090     override ref DrawableRef customDrawable(string id) const {
1091         if (_customDrawables.hasKey(id))
1092             return _customDrawables.drawable(id);
1093         return (cast(Theme)this)._emptyDrawable;
1094     }
1095 
1096     override string customDrawableId(string id) const {
1097         if (_customDrawables.hasKey(id))
1098             return _customDrawables.drawableId(id);
1099         return null;
1100     }
1101 
1102     /// get custom color attribute - transparent by default
1103     override uint customColor(string id, uint defColor = COLOR_TRANSPARENT) const {
1104         if (id in _customColors)
1105             return _customColors[id];
1106         return defColor;
1107     }
1108 
1109     /// get custom color attribute - transparent by default
1110     override uint customLength(string id, uint defValue = 0) const {
1111         if (id in _customLength)
1112             return _customLength[id];
1113         return defValue;
1114     }
1115 
1116     /// returns colors to draw focus rectangle or null if no focus rect should be drawn for style
1117     @property override const(uint[]) focusRectColors() const {
1118         if (_focusRectColors)
1119             return _focusRectColors;
1120         return null;
1121     }
1122 
1123     /// create new named style or get existing
1124     override Style createSubstyle(string id) {
1125         if (id !is null && id in _byId)
1126             return _byId[id]; // already exists
1127         Style style = new Style(this, id);
1128         if (id !is null)
1129             _byId[id] = style;
1130         style._parentStyle = this; // as initial value, use theme as parent
1131         return style;
1132     }
1133 
1134     /// to track unknown styles refernced from code
1135     int[string] unknownStyleIds;
1136     /// find style by id, returns theme if not style with specified ID is not found
1137     @property Style get(string id) {
1138         if (id is null)
1139             return this;
1140         if (id in _byId)
1141             return _byId[id];
1142         // track unknown style ID references
1143         if (id in unknownStyleIds)
1144             unknownStyleIds[id] = unknownStyleIds[id] + 1;
1145         else {
1146             Log.e("Unknown style ID requested: ", id);
1147             unknownStyleIds[id] = 1;
1148         }
1149         return this;
1150     }
1151 
1152     /// find substyle based on widget state (e.g. focused, pressed, ...)
1153     override const(Style) forState(uint state) const {
1154         return this;
1155     }
1156 
1157     /// find substyle based on widget state (e.g. focused, pressed, ...)
1158     override Style forState(uint state) {
1159         return this;
1160     }
1161 
1162     void dumpStats() {
1163         Log.d("Theme ", _id, ": children:", _children.length, ", substates:", _substates.length, ", mapsize:", _byId.length);
1164     }
1165 }
1166 
1167 /// to access current theme
1168 private __gshared Theme _currentTheme;
1169 /// current theme accessor
1170 @property Theme currentTheme() { return _currentTheme; }
1171 /// set new current theme
1172 @property void currentTheme(Theme theme) {
1173     if (_currentTheme !is null) {
1174         destroy(_currentTheme);
1175     }
1176     _currentTheme = theme;
1177 }
1178 
1179 immutable ATTR_SCROLLBAR_BUTTON_UP = "scrollbar_button_up";
1180 immutable ATTR_SCROLLBAR_BUTTON_DOWN = "scrollbar_button_down";
1181 immutable ATTR_SCROLLBAR_BUTTON_LEFT = "scrollbar_button_left";
1182 immutable ATTR_SCROLLBAR_BUTTON_RIGHT = "scrollbar_button_right";
1183 immutable ATTR_SCROLLBAR_INDICATOR_VERTICAL = "scrollbar_indicator_vertical";
1184 immutable ATTR_SCROLLBAR_INDICATOR_HORIZONTAL = "scrollbar_indicator_horizontal";
1185 
1186 Theme createDefaultTheme() {
1187     Log.d("Creating default theme");
1188     Theme res = new Theme("default");
1189     //res.fontSize(14);
1190     version (Windows) {
1191         res.fontFace = "Verdana";
1192     }
1193     //res.fontFace = "Arial Narrow";
1194     static if (WIDGET_STYLE_CONSOLE) {
1195         res.fontSize = 1;
1196         res.textColor = 0xFFFFFF;
1197         Style button = res.createSubstyle(STYLE_BUTTON).backgroundColor(0x808080).alignment(Align.Center).setMargins(0, 0, 0, 0).textColor(0x000000);
1198         //button.createState(State.Selected, State.Selected).backgroundColor(0xFFFFFF);
1199         button.createState(State.Pressed, State.Pressed).backgroundColor(0xFFFF00);
1200         button.createState(State.Focused|State.Hovered, State.Focused|State.Hovered).textColor(0x800000).backgroundColor(0xFFFFFF);
1201         button.createState(State.Focused, State.Focused).backgroundColor(0xFFFFFF).textColor(0x000080);
1202         button.createState(State.Hovered, State.Hovered).textColor(0x800000);
1203         Style buttonLabel = res.createSubstyle(STYLE_BUTTON_LABEL).layoutWidth(FILL_PARENT).alignment(Align.Left|Align.VCenter);
1204         //buttonLabel.createState(State.Hovered, State.Hovered).textColor(0x800000);
1205         //buttonLabel.createState(State.Focused, State.Focused).textColor(0x000080);
1206         res.createSubstyle(STYLE_BUTTON_TRANSPARENT).backgroundImageId("btn_background_transparent").alignment(Align.Center);
1207         res.createSubstyle(STYLE_BUTTON_IMAGE).alignment(Align.Center).textColor(0x000000);
1208         res.createSubstyle(STYLE_TEXT).setMargins(0, 0, 0, 0).setPadding(0, 0, 0, 0);
1209         res.createSubstyle(STYLE_HSPACER).layoutWidth(FILL_PARENT).minWidth(5).layoutWeight(100);
1210         res.createSubstyle(STYLE_VSPACER).layoutHeight(FILL_PARENT).minHeight(5).layoutWeight(100);
1211         res.createSubstyle(STYLE_BUTTON_NOMARGINS).alignment(Align.Center); // .setMargins(5,5,5,5)
1212         //button.createState(State.Enabled | State.Focused, State.Focused).backgroundImageId("btn_default_small_normal_disable_focused");
1213         //button.createState(State.Enabled, 0).backgroundImageId("btn_default_small_normal_disable");
1214         //button.createState(State.Pressed, State.Pressed).backgroundImageId("btn_default_small_pressed");
1215         //button.createState(State.Focused, State.Focused).backgroundImageId("btn_default_small_selected");
1216         //button.createState(State.Hovered, State.Hovered).backgroundImageId("btn_default_small_normal_hover");
1217         res.setCustomDrawable(ATTR_SCROLLBAR_BUTTON_UP, "scrollbar_btn_up");
1218         res.setCustomDrawable(ATTR_SCROLLBAR_BUTTON_DOWN, "scrollbar_btn_down");
1219         res.setCustomDrawable(ATTR_SCROLLBAR_BUTTON_LEFT, "scrollbar_btn_left");
1220         res.setCustomDrawable(ATTR_SCROLLBAR_BUTTON_RIGHT, "scrollbar_btn_right");
1221         res.setCustomDrawable(ATTR_SCROLLBAR_INDICATOR_VERTICAL, "scrollbar_indicator_vertical");
1222         res.setCustomDrawable(ATTR_SCROLLBAR_INDICATOR_HORIZONTAL, "scrollbar_indicator_horizontal");
1223 
1224         Style scrollbar = res.createSubstyle(STYLE_SCROLLBAR);
1225         scrollbar.backgroundColor(0xC0808080);
1226         Style scrollbarButton = button.createSubstyle(STYLE_SCROLLBAR_BUTTON);
1227         Style scrollbarSlider = res.createSubstyle(STYLE_SLIDER);
1228         Style scrollbarPage = res.createSubstyle(STYLE_PAGE_SCROLL).backgroundColor(COLOR_TRANSPARENT);
1229         scrollbarPage.createState(State.Pressed, State.Pressed).backgroundColor(0xC0404080);
1230         scrollbarPage.createState(State.Hovered, State.Hovered).backgroundColor(0xF0404080);
1231 
1232         Style tabUp = res.createSubstyle(STYLE_TAB_UP);
1233         tabUp.backgroundImageId("tab_up_background");
1234         tabUp.layoutWidth(FILL_PARENT);
1235         tabUp.createState(State.Selected, State.Selected).backgroundImageId("tab_up_backgrond_selected");
1236         Style tabUpButtonText = res.createSubstyle(STYLE_TAB_UP_BUTTON_TEXT);
1237         tabUpButtonText.textColor(0x000000).alignment(Align.Center);
1238         tabUpButtonText.createState(State.Selected, State.Selected).textColor(0x000000);
1239         tabUpButtonText.createState(State.Selected|State.Focused, State.Selected|State.Focused).textColor(0x000000);
1240         tabUpButtonText.createState(State.Focused, State.Focused).textColor(0x000000);
1241         tabUpButtonText.createState(State.Hovered, State.Hovered).textColor(0xFFE0E0);
1242         Style tabUpButton = res.createSubstyle(STYLE_TAB_UP_BUTTON);
1243         tabUpButton.backgroundImageId("tab_btn_up");
1244         //tabUpButton.backgroundImageId("tab_btn_up_normal");
1245         //tabUpButton.createState(State.Selected, State.Selected).backgroundImageId("tab_btn_up_selected");
1246         //tabUpButton.createState(State.Selected|State.Focused, State.Selected|State.Focused).backgroundImageId("tab_btn_up_focused_selected");
1247         //tabUpButton.createState(State.Focused, State.Focused).backgroundImageId("tab_btn_up_focused");
1248         //tabUpButton.createState(State.Hovered, State.Hovered).backgroundImageId("tab_btn_up_hover");
1249         Style tabHost = res.createSubstyle(STYLE_TAB_HOST);
1250         tabHost.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
1251         tabHost.backgroundColor(0xF0F0F0);
1252         Style tabWidget = res.createSubstyle(STYLE_TAB_WIDGET);
1253         tabWidget.setPadding(3,3,3,3).backgroundColor(0xEEEEEE);
1254         //tabWidget.backgroundImageId("frame_blue");
1255         //res.dumpStats();
1256 
1257         Style mainMenu = res.createSubstyle(STYLE_MAIN_MENU).backgroundColor(0xEFEFF2).layoutWidth(FILL_PARENT);
1258         Style mainMenuItem = res.createSubstyle(STYLE_MAIN_MENU_ITEM).setPadding(4,2,4,2).backgroundImageId("main_menu_item_background").textFlags(TEXT_FLAGS_USE_PARENT);
1259         Style menuItem = res.createSubstyle(STYLE_MENU_ITEM).setPadding(4,2,4,2); //.backgroundColor(0xE0E080)   ;
1260         menuItem.createState(State.Focused, State.Focused).backgroundColor(0x40C0C000);
1261         menuItem.createState(State.Pressed, State.Pressed).backgroundColor(0x4080C000);
1262         menuItem.createState(State.Selected, State.Selected).backgroundColor(0x00F8F9Fa);
1263         menuItem.createState(State.Hovered, State.Hovered).backgroundColor(0xC0FFFF00);
1264         res.createSubstyle(STYLE_MENU_ICON).setMargins(2,2,2,2).alignment(Align.VCenter|Align.Left).createState(State.Enabled,0).alpha(0xA0);
1265         res.createSubstyle(STYLE_MENU_LABEL).setMargins(4,2,4,2).alignment(Align.VCenter|Align.Left).textFlags(TextFlag.UnderlineHotKeys).createState(State.Enabled,0).textColor(0x80404040);
1266         res.createSubstyle(STYLE_MAIN_MENU_LABEL).setMargins(4,2,4,2).alignment(Align.VCenter|Align.Left).textFlags(TEXT_FLAGS_USE_PARENT).createState(State.Enabled,0).textColor(0x80404040);
1267         res.createSubstyle(STYLE_MENU_ACCEL).setMargins(4,2,4,2).alignment(Align.VCenter|Align.Left).createState(State.Enabled,0).textColor(0x80404040);
1268 
1269         Style transparentButtonBackground = res.createSubstyle(STYLE_TRANSPARENT_BUTTON_BACKGROUND).backgroundImageId("transparent_button_background").setPadding(4,2,4,2); //.backgroundColor(0xE0E080)   ;
1270         //transparentButtonBackground.createState(State.Focused, State.Focused).backgroundColor(0xC0C0C000);
1271         //transparentButtonBackground.createState(State.Pressed, State.Pressed).backgroundColor(0x4080C000);
1272         //transparentButtonBackground.createState(State.Selected, State.Selected).backgroundColor(0x00F8F9Fa);
1273         //transparentButtonBackground.createState(State.Hovered, State.Hovered).backgroundColor(0xD0FFFF00);
1274 
1275         Style poopupMenu = res.createSubstyle(STYLE_POPUP_MENU).backgroundImageId("popup_menu_background_normal");
1276 
1277         Style listItem = res.createSubstyle(STYLE_LIST_ITEM).backgroundImageId("list_item_background");
1278         //listItem.createState(State.Selected, State.Selected).backgroundColor(0xC04040FF).textColor(0x000000);
1279         //listItem.createState(State.Enabled, 0).textColor(0x80000000); // half transparent text for disabled item
1280 
1281         Style editLine = res.createSubstyle(STYLE_EDIT_LINE).backgroundImageId(q{
1282                 {
1283                     text: [
1284                        "╔═╗",
1285                        "║ ║",
1286                        "╚═╝"],
1287                     backgroundColor: [0x000080],
1288                     textColor: [0xFF0000],
1289                     ninepatch: [1,1,1,1]
1290                 }
1291             })
1292             .setPadding(0,0,0,0).setMargins(0,0,0,0).minWidth(20)
1293             .fontFace("Arial").fontFamily(FontFamily.SansSerif).fontSize(1);
1294         Style editBox = res.createSubstyle(STYLE_EDIT_BOX).backgroundImageId("editbox_background")
1295             .setPadding(0,0,0,0).setMargins(0,0,0,0).minWidth(30).minHeight(8).layoutHeight(FILL_PARENT).layoutWidth(FILL_PARENT)
1296             .fontFace("Courier New").fontFamily(FontFamily.MonoSpace).fontSize(1);
1297     } else {
1298         res.fontSize = 15; // TODO: choose based on DPI
1299         Style button = res.createSubstyle(STYLE_BUTTON).backgroundImageId("btn_background").alignment(Align.Center).setMargins(5,5,5,5);
1300         res.createSubstyle(STYLE_BUTTON_TRANSPARENT).backgroundImageId("btn_background_transparent").alignment(Align.Center);
1301         res.createSubstyle(STYLE_BUTTON_LABEL).layoutWidth(FILL_PARENT).alignment(Align.Left|Align.VCenter);
1302         res.createSubstyle(STYLE_BUTTON_IMAGE).alignment(Align.Center);
1303         res.createSubstyle(STYLE_TEXT).setMargins(2,2,2,2).setPadding(1,1,1,1);
1304         res.createSubstyle(STYLE_HSPACER).layoutWidth(FILL_PARENT).minWidth(5).layoutWeight(100);
1305         res.createSubstyle(STYLE_VSPACER).layoutHeight(FILL_PARENT).minHeight(5).layoutWeight(100);
1306         res.createSubstyle(STYLE_BUTTON_NOMARGINS).backgroundImageId("btn_background").alignment(Align.Center); // .setMargins(5,5,5,5)
1307         //button.createState(State.Enabled | State.Focused, State.Focused).backgroundImageId("btn_default_small_normal_disable_focused");
1308         //button.createState(State.Enabled, 0).backgroundImageId("btn_default_small_normal_disable");
1309         //button.createState(State.Pressed, State.Pressed).backgroundImageId("btn_default_small_pressed");
1310         //button.createState(State.Focused, State.Focused).backgroundImageId("btn_default_small_selected");
1311         //button.createState(State.Hovered, State.Hovered).backgroundImageId("btn_default_small_normal_hover");
1312         res.setCustomDrawable(ATTR_SCROLLBAR_BUTTON_UP, "scrollbar_btn_up");
1313         res.setCustomDrawable(ATTR_SCROLLBAR_BUTTON_DOWN, "scrollbar_btn_down");
1314         res.setCustomDrawable(ATTR_SCROLLBAR_BUTTON_LEFT, "scrollbar_btn_left");
1315         res.setCustomDrawable(ATTR_SCROLLBAR_BUTTON_RIGHT, "scrollbar_btn_right");
1316         res.setCustomDrawable(ATTR_SCROLLBAR_INDICATOR_VERTICAL, "scrollbar_indicator_vertical");
1317         res.setCustomDrawable(ATTR_SCROLLBAR_INDICATOR_HORIZONTAL, "scrollbar_indicator_horizontal");
1318 
1319         Style scrollbar = res.createSubstyle(STYLE_SCROLLBAR);
1320         scrollbar.backgroundColor(0xC0808080);
1321         Style scrollbarButton = button.createSubstyle(STYLE_SCROLLBAR_BUTTON);
1322         Style scrollbarSlider = res.createSubstyle(STYLE_SLIDER);
1323         Style scrollbarPage = res.createSubstyle(STYLE_PAGE_SCROLL).backgroundColor(COLOR_TRANSPARENT);
1324         scrollbarPage.createState(State.Pressed, State.Pressed).backgroundColor(0xC0404080);
1325         scrollbarPage.createState(State.Hovered, State.Hovered).backgroundColor(0xF0404080);
1326 
1327         Style tabUp = res.createSubstyle(STYLE_TAB_UP);
1328         tabUp.backgroundImageId("tab_up_background");
1329         tabUp.layoutWidth(FILL_PARENT);
1330         tabUp.createState(State.Selected, State.Selected).backgroundImageId("tab_up_backgrond_selected");
1331         Style tabUpButtonText = res.createSubstyle(STYLE_TAB_UP_BUTTON_TEXT);
1332         tabUpButtonText.textColor(0x000000).fontSize(12).alignment(Align.Center);
1333         tabUpButtonText.createState(State.Selected, State.Selected).textColor(0x000000);
1334         tabUpButtonText.createState(State.Selected|State.Focused, State.Selected|State.Focused).textColor(0x000000);
1335         tabUpButtonText.createState(State.Focused, State.Focused).textColor(0x000000);
1336         tabUpButtonText.createState(State.Hovered, State.Hovered).textColor(0xFFE0E0);
1337         Style tabUpButton = res.createSubstyle(STYLE_TAB_UP_BUTTON);
1338         tabUpButton.backgroundImageId("tab_btn_up");
1339         //tabUpButton.backgroundImageId("tab_btn_up_normal");
1340         //tabUpButton.createState(State.Selected, State.Selected).backgroundImageId("tab_btn_up_selected");
1341         //tabUpButton.createState(State.Selected|State.Focused, State.Selected|State.Focused).backgroundImageId("tab_btn_up_focused_selected");
1342         //tabUpButton.createState(State.Focused, State.Focused).backgroundImageId("tab_btn_up_focused");
1343         //tabUpButton.createState(State.Hovered, State.Hovered).backgroundImageId("tab_btn_up_hover");
1344         Style tabHost = res.createSubstyle(STYLE_TAB_HOST);
1345         tabHost.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
1346         tabHost.backgroundColor(0xF0F0F0);
1347         Style tabWidget = res.createSubstyle(STYLE_TAB_WIDGET);
1348         tabWidget.setPadding(3,3,3,3).backgroundColor(0xEEEEEE);
1349         //tabWidget.backgroundImageId("frame_blue");
1350         //res.dumpStats();
1351 
1352         Style mainMenu = res.createSubstyle(STYLE_MAIN_MENU).backgroundColor(0xEFEFF2).layoutWidth(FILL_PARENT);
1353         Style mainMenuItem = res.createSubstyle(STYLE_MAIN_MENU_ITEM).setPadding(4,2,4,2).backgroundImageId("main_menu_item_background").textFlags(TEXT_FLAGS_USE_PARENT);
1354         Style menuItem = res.createSubstyle(STYLE_MENU_ITEM).setPadding(4,2,4,2); //.backgroundColor(0xE0E080)   ;
1355         menuItem.createState(State.Focused, State.Focused).backgroundColor(0x40C0C000);
1356         menuItem.createState(State.Pressed, State.Pressed).backgroundColor(0x4080C000);
1357         menuItem.createState(State.Selected, State.Selected).backgroundColor(0x00F8F9Fa);
1358         menuItem.createState(State.Hovered, State.Hovered).backgroundColor(0xC0FFFF00);
1359         res.createSubstyle(STYLE_MENU_ICON).setMargins(2,2,2,2).alignment(Align.VCenter|Align.Left).createState(State.Enabled,0).alpha(0xA0);
1360         res.createSubstyle(STYLE_MENU_LABEL).setMargins(4,2,4,2).alignment(Align.VCenter|Align.Left).textFlags(TextFlag.UnderlineHotKeys).createState(State.Enabled,0).textColor(0x80404040);
1361         res.createSubstyle(STYLE_MAIN_MENU_LABEL).setMargins(4,2,4,2).alignment(Align.VCenter|Align.Left).textFlags(TEXT_FLAGS_USE_PARENT).createState(State.Enabled,0).textColor(0x80404040);
1362         res.createSubstyle(STYLE_MENU_ACCEL).setMargins(4,2,4,2).alignment(Align.VCenter|Align.Left).createState(State.Enabled,0).textColor(0x80404040);
1363 
1364         Style transparentButtonBackground = res.createSubstyle(STYLE_TRANSPARENT_BUTTON_BACKGROUND).backgroundImageId("transparent_button_background").setPadding(4,2,4,2); //.backgroundColor(0xE0E080)   ;
1365         //transparentButtonBackground.createState(State.Focused, State.Focused).backgroundColor(0xC0C0C000);
1366         //transparentButtonBackground.createState(State.Pressed, State.Pressed).backgroundColor(0x4080C000);
1367         //transparentButtonBackground.createState(State.Selected, State.Selected).backgroundColor(0x00F8F9Fa);
1368         //transparentButtonBackground.createState(State.Hovered, State.Hovered).backgroundColor(0xD0FFFF00);
1369 
1370         Style poopupMenu = res.createSubstyle(STYLE_POPUP_MENU).backgroundImageId("popup_menu_background_normal");
1371 
1372         Style listItem = res.createSubstyle(STYLE_LIST_ITEM).backgroundImageId("list_item_background");
1373         //listItem.createState(State.Selected, State.Selected).backgroundColor(0xC04040FF).textColor(0x000000);
1374         //listItem.createState(State.Enabled, 0).textColor(0x80000000); // half transparent text for disabled item
1375 
1376         Style editLine = res.createSubstyle(STYLE_EDIT_LINE).backgroundImageId("editbox_background")
1377             .setPadding(5,6,5,6).setMargins(2,2,2,2).minWidth(40)
1378             .fontFace("Arial").fontFamily(FontFamily.SansSerif).fontSize(16);
1379         Style editBox = res.createSubstyle(STYLE_EDIT_BOX).backgroundImageId("editbox_background")
1380             .setPadding(5,6,5,6).setMargins(2,2,2,2).minWidth(100).minHeight(60).layoutHeight(FILL_PARENT).layoutWidth(FILL_PARENT)
1381             .fontFace("Courier New").fontFamily(FontFamily.MonoSpace).fontSize(16);
1382     }
1383 
1384     return res;
1385 }
1386 
1387 /// decode comma delimited dimension list or single value - and put to Rect
1388 Rect decodeRect(string s) {
1389     uint[6] values;
1390     int valueCount = 0;
1391     int start = 0;
1392     for (int i = 0; i <= s.length; i++) {
1393         if (i == s.length || s[i] == ',') {
1394             if (i > start) {
1395                 string item = s[start .. i];
1396                 values[valueCount++] = decodeDimension(item);
1397                 if (valueCount >= 6)
1398                     break;
1399             }
1400             start = i + 1;
1401         }
1402     }
1403     if (valueCount == 1) // same value for all dimensions
1404         return Rect(values[0], values[0], values[0], values[0]);
1405     else if (valueCount == 2) // one value of horizontal, and one for vertical
1406         return Rect(values[0], values[1], values[0], values[1]);
1407     else if (valueCount == 4) // separate left, top, right, bottom
1408         return Rect(values[0], values[1], values[2], values[3]);
1409     Log.e("Invalid rect attribute value ", s);
1410     return Rect(0,0,0,0);
1411 }
1412 
1413 private import std.array : split;
1414 
1415 /// Decode color list attribute, e.g.: "#84A, #99FFFF" -> [0x8844aa, 0x99ffff]
1416 uint[] decodeFocusRectColors(string s) {
1417     if (s.equal("@null"))
1418         return [COLOR_UNSPECIFIED];
1419     string[] colors = split(s, ",");
1420     if (colors.length < 1)
1421         return null;
1422     uint[] res = new uint[colors.length];
1423     for (int i = 0; i < colors.length; i++) {
1424         uint cl = decodeHexColor(colors[i], COLOR_UNSPECIFIED);
1425         if (cl == COLOR_UNSPECIFIED)
1426             return null;
1427         res[i] = cl;
1428     }
1429     return res;
1430 }
1431 
1432 /// parses string like "Left|VCenter" to bit set of Align flags
1433 ubyte decodeAlignment(string s) {
1434     ubyte res = 0;
1435     int start = 0;
1436     for (int i = 0; i <= s.length; i++) {
1437         if (i == s.length || s[i] == '|') {
1438             if (i > start) {
1439                 string item = s[start .. i];
1440                 if (item.equal("Left"))
1441                     res |= Align.Left;
1442                 else if (item.equal("Right"))
1443                     res |= Align.Right;
1444                 else if (item.equal("Top"))
1445                     res |= Align.Top;
1446                 else if (item.equal("Bottom"))
1447                     res |= Align.Bottom;
1448                 else if (item.equal("HCenter"))
1449                     res |= Align.HCenter;
1450                 else if (item.equal("VCenter"))
1451                     res |= Align.VCenter;
1452                 else if (item.equal("Center"))
1453                     res |= Align.Center;
1454                 else if (item.equal("TopLeft"))
1455                     res |= Align.TopLeft;
1456                 else
1457                     Log.e("unknown Align value: ", item);
1458             }
1459             start = i + 1;
1460         }
1461     }
1462     return res;
1463 }
1464 
1465 /// parses string like "HotKeys|UnderlineHotKeysWhenAltPressed" to bit set of TextFlag flags
1466 uint decodeTextFlags(string s) {
1467     uint res = 0;
1468     int start = 0;
1469     for (int i = 0; i <= s.length; i++) {
1470         if (i == s.length || s[i] == '|') {
1471             if (i > start) {
1472                 string item = s[start .. i];
1473                 if (item.equal("HotKeys"))
1474                     res |= TextFlag.HotKeys;
1475                 else if (item.equal("UnderlineHotKeys"))
1476                     res |= TextFlag.UnderlineHotKeys;
1477                 else if (item.equal("UnderlineHotKeysWhenAltPressed"))
1478                     res |= TextFlag.UnderlineHotKeysWhenAltPressed;
1479                 else if (item.equal("Underline"))
1480                     res |= TextFlag.Underline;
1481                 else if (item.equal("Unspecified"))
1482                     res = TEXT_FLAGS_UNSPECIFIED;
1483                 else if (item.equal("Parent"))
1484                     res = TEXT_FLAGS_USE_PARENT;
1485                 else
1486                     Log.e("unknown text flag value: ", item);
1487             }
1488             start = i + 1;
1489         }
1490     }
1491     return res;
1492 }
1493 
1494 /// decode FontFamily item name to value
1495 FontFamily decodeFontFamily(string s) {
1496     if (s.equal("SansSerif"))
1497         return FontFamily.SansSerif;
1498     if (s.equal("Serif"))
1499         return FontFamily.Serif;
1500     if (s.equal("Cursive"))
1501         return FontFamily.Cursive;
1502     if (s.equal("Fantasy"))
1503         return FontFamily.Fantasy;
1504     if (s.equal("MonoSpace"))
1505         return FontFamily.MonoSpace;
1506     if (s.equal("Unspecified"))
1507         return FontFamily.Unspecified;
1508     Log.e("unknown font family ", s);
1509     return FontFamily.SansSerif;
1510 }
1511 
1512 /// decode FontWeight item name to value
1513 FontWeight decodeFontWeight(string s) {
1514     if (s.equal("bold"))
1515         return FontWeight.Bold;
1516     if (s.equal("normal"))
1517         return FontWeight.Normal;
1518     Log.e("unknown font weight ", s);
1519     return FontWeight.Normal;
1520 }
1521 
1522 /// decode layout dimension (FILL_PARENT, WRAP_CONTENT, or just size)
1523 int decodeLayoutDimension(string s) {
1524     if (s.equal("FILL_PARENT") || s.equal("fill"))
1525         return FILL_PARENT;
1526     if (s.equal("WRAP_CONTENT") || s.equal("wrap"))
1527         return WRAP_CONTENT;
1528     return decodeDimension(s);
1529 }
1530 
1531 /// remove superfluous space characters from a border property
1532 string sanitizeBorderProperty(string s) pure {
1533     string[] parts = s.split(',');
1534     foreach (ref part; parts)
1535         part = part.strip();
1536     string joined = parts.join(',');
1537 
1538     char[] res;
1539     // replace repeating space characters with one space
1540     import std.ascii : isWhite;
1541     bool isSpace;
1542     foreach (c; joined) {
1543         if (isWhite(c)) {
1544             if (!isSpace) {
1545                 res ~= ' ';
1546                 isSpace = true;
1547             }
1548         } else {
1549             res ~= c;
1550             isSpace = false;
1551         }
1552     }
1553 
1554     return cast(string)res;
1555 }
1556 
1557 /// remove superfluous space characters from a box shadow property
1558 string sanitizeBoxShadowProperty(string s) pure {
1559     return sanitizeBorderProperty(s);
1560 }
1561 
1562 /// load style attributes from XML element
1563 bool loadStyleAttributes(Style style, Element elem, bool allowStates) {
1564     //Log.d("Theme: loadStyleAttributes ", style.id, " ", elem.tag.attr);
1565     if ("backgroundImageId" in elem.tag.attr)
1566         style.backgroundImageId = elem.tag.attr["backgroundImageId"];
1567     if ("backgroundColor" in elem.tag.attr)
1568         style.backgroundColor = decodeHexColor(elem.tag.attr["backgroundColor"]);
1569     if ("textColor" in elem.tag.attr)
1570         style.textColor = decodeHexColor(elem.tag.attr["textColor"]);
1571     if ("margins" in elem.tag.attr)
1572         style.margins = decodeRect(elem.tag.attr["margins"]);
1573     if ("padding" in elem.tag.attr)
1574         style.padding = decodeRect(elem.tag.attr["padding"]);
1575     if ("border" in elem.tag.attr)
1576         style.border = sanitizeBorderProperty(elem.tag.attr["border"]);
1577     if ("boxShadow" in elem.tag.attr)
1578         style.boxShadow = sanitizeBoxShadowProperty(elem.tag.attr["boxShadow"]);
1579     if ("align" in elem.tag.attr)
1580         style.alignment = decodeAlignment(elem.tag.attr["align"]);
1581     if ("minWidth" in elem.tag.attr)
1582         style.minWidth = decodeDimension(elem.tag.attr["minWidth"]);
1583     if ("maxWidth" in elem.tag.attr)
1584         style.maxWidth = decodeDimension(elem.tag.attr["maxWidth"]);
1585     if ("minHeight" in elem.tag.attr)
1586         style.minHeight = decodeDimension(elem.tag.attr["minHeight"]);
1587     if ("maxHeight" in elem.tag.attr)
1588         style.maxHeight = decodeDimension(elem.tag.attr["maxHeight"]);
1589     if ("maxLines" in elem.tag.attr)
1590         style.maxLines = decodeDimension(elem.tag.attr["maxLines"]);
1591     if ("fontFace" in elem.tag.attr)
1592         style.fontFace = elem.tag.attr["fontFace"];
1593     if ("fontFamily" in elem.tag.attr)
1594         style.fontFamily = decodeFontFamily(elem.tag.attr["fontFamily"]);
1595     if ("fontSize" in elem.tag.attr)
1596         style.fontSize = cast(int)decodeDimension(elem.tag.attr["fontSize"]);
1597     if ("fontWeight" in elem.tag.attr)
1598         style.fontWeight = cast(ushort)decodeFontWeight(elem.tag.attr["fontWeight"]);
1599     if ("layoutWidth" in elem.tag.attr)
1600         style.layoutWidth = decodeLayoutDimension(elem.tag.attr["layoutWidth"]);
1601     if ("layoutHeight" in elem.tag.attr)
1602         style.layoutHeight = decodeLayoutDimension(elem.tag.attr["layoutHeight"]);
1603     if ("alpha" in elem.tag.attr)
1604         style.alpha = decodeDimension(elem.tag.attr["alpha"]);
1605     if ("textFlags" in elem.tag.attr)
1606         style.textFlags = decodeTextFlags(elem.tag.attr["textFlags"]);
1607     if ("focusRectColors" in elem.tag.attr)
1608         style.focusRectColors = decodeFocusRectColors(elem.tag.attr["focusRectColors"]);
1609     foreach(item; elem.elements) {
1610         if (allowStates && item.tag.name.equal("state")) {
1611             uint stateMask = 0;
1612             uint stateValue = 0;
1613             extractStateFlags(item.tag.attr, stateMask, stateValue);
1614             if (stateMask) {
1615                 Style state = style.getOrCreateState(stateMask, stateValue);
1616                 loadStyleAttributes(state, item, false);
1617             }
1618         } else if (item.tag.name.equal("drawable")) {
1619             // <drawable id="scrollbar_button_up" value="scrollbar_btn_up"/>
1620             string drawableid = attrValue(item, "id");
1621             string drawablevalue = attrValue(item, "value");
1622             if (drawableid)
1623                 style.setCustomDrawable(drawableid, drawablevalue);
1624         } else if (item.tag.name.equal("color")) {
1625             // <color id="buttons_panel_color" value="#303080"/>
1626             string colorid = attrValue(item, "id");
1627             string colorvalue = attrValue(item, "value");
1628             uint color = decodeHexColor(colorvalue, COLOR_TRANSPARENT);
1629             if (colorid)
1630                 style.setCustomColor(colorid, color);
1631         } else if (item.tag.name.equal("length")) {
1632             // <length id="overlap" value="2"/>
1633             string lenid = attrValue(item, "id");
1634             string lenvalue = attrValue(item, "value");
1635             uint len = decodeDimension(lenvalue);
1636             if (lenid.length > 0 && len > 0)
1637                 style.setCustomLength(lenid, len);
1638         }
1639     }
1640     return true;
1641 }
1642 
1643 /**
1644  * load theme from XML document
1645  *
1646  * Sample:
1647  * ---
1648  * <?xml version="1.0" encoding="utf-8"?>
1649  * <theme id="theme_custom" parent="theme_default">
1650  *       <style id="BUTTON"
1651  *             backgroundImageId="btn_background"
1652  *          >
1653  *       </style>
1654  * </theme>
1655  * ---
1656  *
1657  */
1658 bool loadTheme(Theme theme, Element doc, int level = 0) {
1659     if (!doc.tag.name.equal("theme")) {
1660         Log.e("<theme> element should be main in theme file!");
1661         return false;
1662     }
1663     // <theme>
1664     string id = attrValue(doc, "id");
1665     string parent = attrValue(doc, "parent");
1666     theme.id = id;
1667     if (parent.length > 0) {
1668         // load base theme
1669         if (level < 3) // to prevent infinite recursion
1670             loadTheme(theme, parent, level + 1);
1671     }
1672     loadStyleAttributes(theme, doc, false);
1673     foreach(styleitem; doc.elements) {
1674         if (styleitem.tag.name.equal("style")) {
1675             // load <style>
1676             string styleid = attrValue(styleitem, "id");
1677             string styleparent = attrValue(styleitem, "parent");
1678             if (styleid.length) {
1679                 // create new style
1680                 Style parentStyle = null;
1681                 parentStyle = theme.get(styleparent);
1682                 Style style = parentStyle.createSubstyle(styleid);
1683                 loadStyleAttributes(style, styleitem, true);
1684             } else {
1685                 Log.e("style without ID in theme file");
1686             }
1687         }
1688     }
1689     return true;
1690 }
1691 
1692 /// load theme from file
1693 bool loadTheme(Theme theme, string resourceId, int level = 0) {
1694 
1695     string filename;
1696     try {
1697         filename = drawableCache.findResource(WIDGET_STYLE_CONSOLE ? "console_" ~ resourceId : resourceId);
1698         if (!filename || !filename.endsWith(".xml"))
1699             return false;
1700         string s = cast(string)loadResourceBytes(filename);
1701         if (!s) {
1702             Log.e("Cannot read XML resource ", resourceId, " from file ", filename);
1703             return false;
1704         }
1705 
1706         // Check for well-formedness
1707         //check(s);
1708 
1709         // Make a DOM tree
1710         auto doc = new Document(s);
1711 
1712         return loadTheme(theme, doc);
1713     } catch (CheckException e) {
1714         Log.e("Invalid XML resource ", resourceId);
1715         return false;
1716     }
1717 }
1718 
1719 /// load theme from XML file (null if failed)
1720 Theme loadTheme(string resourceId) {
1721     Theme res = new Theme(resourceId);
1722     if (loadTheme(res, resourceId)) {
1723         res.id = resourceId;
1724         return res;
1725     }
1726     destroy(res);
1727     return null;
1728 }
1729 
1730 /// custom drawable attribute container for styles
1731 class DrawableAttribute {
1732 protected:
1733     string _id;
1734     string _drawableId;
1735     DrawableRef _drawable;
1736     bool _initialized;
1737 
1738 public:
1739     this(string id, string drawableId) {
1740         _id = id;
1741         _drawableId = drawableId;
1742     }
1743     ~this() {
1744         clear();
1745     }
1746     @property string id() const { return _id; }
1747     @property string drawableId() const { return _drawableId; }
1748     @property void drawableId(string newDrawable) { _drawableId = newDrawable; clear(); }
1749     @property ref DrawableRef drawable() const {
1750         if (!_drawable.isNull)
1751             return (cast(DrawableAttribute)this)._drawable;
1752         (cast(DrawableAttribute)this)._drawable = drawableCache.get(_drawableId);
1753         (cast(DrawableAttribute)this)._initialized = true;
1754         return (cast(DrawableAttribute)this)._drawable;
1755     }
1756     void clear() {
1757         _drawable.clear();
1758         _initialized = false;
1759     }
1760     void onThemeChanged() {
1761         if (!_drawableId) {
1762             _drawable.clear();
1763             _initialized = false;
1764         }
1765     }
1766 }
1767 
1768 /// returns custom drawable replacement id for specified id from current theme, or returns passed value if not found or no current theme
1769 string overrideCustomDrawableId(string id) {
1770     string res = currentTheme ? currentTheme.customDrawableId(id) : id;
1771     return !res ? id : res;
1772 }
1773 
1774 shared static ~this() {
1775     currentTheme = null;
1776 }
1777 
1778 
1779 
1780 
1781 unittest {
1782     assert(sanitizeBorderProperty("   #aaa, 2  ") == "#aaa,2");
1783     assert(sanitizeBorderProperty("   #aaa, 2, 2, 2, 4") == "#aaa,2,2,2,4");
1784     assert(sanitizeBorderProperty("   #a aa  ,  2   4  ") == "#a aa,2 4");
1785 }