1 // Written in the D programming language.
2 
3 /**
4 This module contains resource management and drawables implementation.
5 
6 imageCache is RAM cache of decoded images (as DrawBuf).
7 
8 drawableCache is cache of Drawables.
9 
10 Supports nine-patch PNG images in .9.png files (like in Android).
11 
12 Supports state drawables using XML files similar to ones in Android.
13 
14 
15 
16 When your application uses custom resources, you can embed resources into executable and/or specify external resource directory(s).
17 
18 To embed resources, put them into views/res directory, and create file views/resources.list with list of all files to embed.
19 
20 Use following code to embed resources:
21 
22 ----
23 /// entry point for dlangui based application
24 extern (C) int UIAppMain(string[] args) {
25 
26     // embed non-standard resources listed in views/resources.list into executable
27     embeddedResourceList.addResources(embedResourcesFromList!("resources.list")());
28 
29     ...
30 ----
31 
32 Resource list resources.list file may look similar to following:
33 
34 ----
35 res/i18n/en.ini
36 res/i18n/ru.ini
37 res/mdpi/cr3_logo.png
38 res/mdpi/document-open.png
39 res/mdpi/document-properties.png
40 res/mdpi/document-save.png
41 res/mdpi/edit-copy.png
42 res/mdpi/edit-paste.png
43 res/mdpi/edit-undo.png
44 res/mdpi/tx_fabric.jpg
45 res/theme_custom1.xml
46 ----
47 
48 As well you can specify list of external directories to get resources from.
49 
50 ----
51 
52 /// entry point for dlangui based application
53 extern (C) int UIAppMain(string[] args) {
54     // resource directory search paths
55     string[] resourceDirs = [
56         appendPath(exePath, "../../../res/"),   // for Visual D and DUB builds
57         appendPath(exePath, "../../../res/mdpi/"),   // for Visual D and DUB builds
58         appendPath(exePath, "../../../../res/"),// for Mono-D builds
59         appendPath(exePath, "../../../../res/mdpi/"),// for Mono-D builds
60         appendPath(exePath, "res/"), // when res dir is located at the same directory as executable
61         appendPath(exePath, "../res/"), // when res dir is located at project directory
62         appendPath(exePath, "../../res/"), // when res dir is located at the same directory as executable
63         appendPath(exePath, "res/mdpi/"), // when res dir is located at the same directory as executable
64         appendPath(exePath, "../res/mdpi/"), // when res dir is located at project directory
65         appendPath(exePath, "../../res/mdpi/") // when res dir is located at the same directory as executable
66     ];
67     // setup resource directories - will use only existing directories
68     Platform.instance.resourceDirs = resourceDirs;
69 
70 ----
71 
72 When same file exists in both embedded and external resources, one from external resource directory will be used - it's useful for developing
73 and testing of resources.
74 
75 
76 Synopsis:
77 
78 ----
79 import dlangui.graphics.resources;
80 
81 // embed non-standard resources listed in views/resources.list into executable
82 embeddedResourceList.addResources(embedResourcesFromList!("resources.list")());
83 ----
84 
85 Copyright: Vadim Lopatin, 2014
86 License:   Boost License 1.0
87 Authors:   Vadim Lopatin, coolreader.org@gmail.com
88 
89 */
90 
91 module dlangui.graphics.resources;
92 
93 import dlangui.core.config;
94 
95 import dlangui.core.logger;
96 import dlangui.core.types;
97 static if (BACKEND_GUI) {
98     import dlangui.graphics.images;
99 }
100 import dlangui.graphics.colors;
101 import dlangui.graphics.drawbuf;
102 import std.file;
103 import std.algorithm;
104 import undead.xml;
105 import std.conv;
106 import std.string;
107 import std.path;
108 
109 /// filename prefix for embedded resources
110 immutable string EMBEDDED_RESOURCE_PREFIX = "@embedded@/";
111 
112 struct EmbeddedResource {
113     immutable string name;
114     immutable ubyte[] data;
115     immutable string dir;
116     this(immutable string name, immutable ubyte[] data, immutable string dir = null) {
117         this.name = name;
118         this.data = data;
119         this.dir = dir;
120     }
121 }
122 
123 struct EmbeddedResourceList {
124     private EmbeddedResource[] list;
125     void addResources(EmbeddedResource[] resources) {
126         list ~= resources;
127     }
128 
129     void dumpEmbeddedResources() {
130         foreach(r; list) {
131             Log.d("EmbeddedResource: ", r.name);
132         }
133     }
134 
135     /// find by exact file name
136     EmbeddedResource * find(string name) {
137         // search backwards to allow overriding standard resources (which are added first)
138         if (SCREEN_DPI > 110 && (name.endsWith(".png") || name.endsWith(".jpg") || name.endsWith(".jpeg"))) {
139             // HIGH DPI resources are in /hdpi/ directory and started with hdpi_ prefix
140             string prefixedName = "hdpi_" ~ name;
141             for (int i = cast(int)list.length - 1; i >= 0; i--)
142                 if (prefixedName.equal(list[i].name)) {
143                     Log.d("found hdpi resource ", prefixedName);
144                     return &list[i];
145                 }
146         }
147         for (int i = cast(int)list.length - 1; i >= 0; i--)
148             if (name.equal(list[i].name))
149                 return &list[i];
150         return null;
151     }
152 
153     /// find by name w/o extension
154     EmbeddedResource * findAutoExtension(string name) {
155         string xmlname = name ~ ".xml";
156         string pngname = name ~ ".png";
157         string png9name = name ~ ".9.png";
158         string jpgname = name ~ ".jpg";
159         string jpegname = name ~ ".jpeg";
160         string xpmname = name ~ ".xpm";
161         string timname = name ~ ".tim";
162         // search backwards to allow overriding standard resources (which are added first)
163         for (int i = cast(int)list.length - 1; i >= 0; i--) {
164             string s = list[i].name;
165             if (s.equal(name) || s.equal(xmlname) || s.equal(pngname) || s.equal(png9name)
166                     || s.equal(jpgname) || s.equal(jpegname) || s.equal(xpmname) || s.equal(timname))
167                 return &list[i];
168         }
169         return null;
170     }
171 }
172 
173 __gshared EmbeddedResourceList embeddedResourceList;
174 
175 //immutable string test_res = import("res/background.xml");
176 // Unfortunately, import with full pathes does not work on Windows
177 version = USE_FULL_PATH_FOR_RESOURCES;
178 
179 string resDirName(string fullname) {
180     immutable string step0 = fullname.dirName;
181     immutable string step1 = step0.startsWith("res/") ? step0[4 .. $] : step0;
182     return step1 == "." ? null : step1;
183 }
184 
185 EmbeddedResource[] embedResource(string resourceName)() {
186     static if (resourceName.startsWith("#")) {
187         return [];
188     } else {
189         version (USE_FULL_PATH_FOR_RESOURCES) {
190             immutable string name = resourceName;
191         } else {
192             immutable string name = baseName(resourceName);
193         }
194         static if (name.length > 0) {
195             immutable ubyte[] data = cast(immutable ubyte[])import(name);
196             immutable string resname = baseName(name);
197             immutable string resdir = resDirName(name);
198             static if (data.length > 0)
199                 return [EmbeddedResource(resname, data, resdir)];
200             else
201                 return [];
202         } else
203             return [];
204     }
205 }
206 
207 /// embed all resources from list
208 EmbeddedResource[] embedResources(string[] resourceNames)() {
209     static if (resourceNames.length == 0)
210         return [];
211     static if (resourceNames.length == 1)
212         return embedResource!(resourceNames[0])();
213     else
214         return embedResources!(resourceNames[0 .. $/2])() ~ embedResources!(resourceNames[$/2 .. $])();
215 }
216 
217 /// embed all resources from list
218 EmbeddedResource[] embedResourcesFromList(string resourceList)() {
219     static if (WIDGET_STYLE_CONSOLE) {
220         return embedResources!(splitLines(import("console_" ~ resourceList)))();
221     } else {
222         return embedResources!(splitLines(import(resourceList)))();
223     }
224 }
225 
226 
227 void embedStandardDlangUIResources() {
228     version (EmbedStandardResources) {
229         embeddedResourceList.addResources(embedResourcesFromList!("standard_resources.list")());
230     }
231 }
232 
233 /// load resource bytes from embedded resource or file
234 immutable(ubyte[]) loadResourceBytes(string filename) {
235     if (filename.startsWith(EMBEDDED_RESOURCE_PREFIX)) {
236         EmbeddedResource * embedded = embeddedResourceList.find(filename[EMBEDDED_RESOURCE_PREFIX.length .. $]);
237         if (embedded)
238             return embedded.data;
239         return null;
240     } else {
241         try {
242             immutable ubyte[] data = cast(immutable ubyte[])std.file.read(filename);
243             return data;
244         } catch (Exception e) {
245             Log.e("exception while loading file ", filename);
246             return null;
247         }
248     }
249 }
250 
251 /// Base class for all drawables
252 class Drawable : RefCountedObject {
253     debug static __gshared int _instanceCount;
254     debug @property static int instanceCount() { return _instanceCount; }
255 
256     this() {
257         debug ++_instanceCount;
258         //Log.d("Created drawable, count=", ++_instanceCount);
259     }
260     ~this() {
261         //Log.d("Destroyed drawable, count=", --_instanceCount);
262         debug --_instanceCount;
263     }
264     abstract void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0);
265     @property abstract int width();
266     @property abstract int height();
267     @property Rect padding() { return Rect(0,0,0,0); }
268 }
269 
270 static if (ENABLE_OPENGL) {
271     /// Custom drawing inside openGL
272     class OpenGLDrawable : Drawable {
273 
274         private OpenGLDrawableDelegate _drawHandler;
275 
276         @property OpenGLDrawableDelegate drawHandler() { return _drawHandler; }
277         @property OpenGLDrawable drawHandler(OpenGLDrawableDelegate handler) { _drawHandler = handler; return this; }
278 
279         this(OpenGLDrawableDelegate drawHandler = null) {
280             _drawHandler = drawHandler;
281         }
282 
283         void onDraw(Rect windowRect, Rect rc) {
284             // either override this method or assign draw handler
285             if (_drawHandler) {
286                 _drawHandler(windowRect, rc);
287             }
288         }
289 
290         override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
291             buf.drawCustomOpenGLScene(rc, &onDraw);
292         }
293 
294         override @property int width() {
295             return 20; // dummy size
296         }
297         override @property int height() {
298             return 20; // dummy size
299         }
300     }
301 }
302 
303 class EmptyDrawable : Drawable {
304     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
305     }
306     @property override int width() { return 0; }
307     @property override int height() { return 0; }
308 }
309 
310 class SolidFillDrawable : Drawable {
311     protected uint _color;
312     this(uint color) {
313         _color = color;
314     }
315     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
316         if (!_color.isFullyTransparentColor)
317             buf.fillRect(rc, _color);
318     }
319     @property override int width() { return 1; }
320     @property override int height() { return 1; }
321 }
322 
323 class GradientDrawable : Drawable {
324     protected uint _color1; // top left
325     protected uint _color2; // bottom left
326     protected uint _color3; // top right
327     protected uint _color4; // bottom right
328 
329     this(uint angle, uint color1, uint color2) {
330         // rotate a gradient; angle goes clockwise
331         import std.math;
332         float radians = angle * PI / 180;
333         float c = cos(radians);
334         float s = sin(radians);
335         if (s >= 0) {
336             if (c >= 0) {
337                 // 0-90 degrees
338                 _color1 = blendARGB(color1, color2, cast(uint)(255 * c));
339                 _color2 = color2;
340                 _color3 = color1;
341                 _color4 = blendARGB(color1, color2, cast(uint)(255 * s));
342             } else {
343                 // 90-180 degrees
344                 _color1 = color2;
345                 _color2 = blendARGB(color1, color2, cast(uint)(255 * -c));
346                 _color3 = blendARGB(color1, color2, cast(uint)(255 * s));
347                 _color4 = color1;
348             }
349         } else {
350             if (c < 0) {
351                 // 180-270 degrees
352                 _color1 = blendARGB(color1, color2, cast(uint)(255 * -s));
353                 _color2 = color1;
354                 _color3 = color2;
355                 _color4 = blendARGB(color1, color2, cast(uint)(255 * -c));
356             } else {
357                 // 270-360 degrees
358                 _color1 = color1;
359                 _color2 = blendARGB(color1, color2, cast(uint)(255 * -s));
360                 _color3 = blendARGB(color1, color2, cast(uint)(255 * c));
361                 _color4 = color2;
362             }
363         }
364     }
365 
366     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
367         buf.fillGradientRect(rc, _color1, _color2, _color3, _color4);
368     }
369 
370     @property override int width() { return 1; }
371     @property override int height() { return 1; }
372 }
373 
374 /// solid borders (may be of different width) and, optionally, solid inner area
375 class BorderDrawable : Drawable {
376     protected uint _borderColor;
377     protected Rect _borderWidths; // left, top, right, bottom border widths, in pixels
378     protected uint _middleColor; // middle area color (may be transparent)
379 
380     this(uint borderColor, Rect borderWidths, uint innerAreaColor = 0xFFFFFFFF) {
381         _borderColor = borderColor;
382         _borderWidths = borderWidths;
383         _middleColor = innerAreaColor;
384     }
385 
386     this(uint borderColor, int borderWidth, uint innerAreaColor = 0xFFFFFFFF) {
387         _borderColor = borderColor;
388         _borderWidths = Rect(borderWidth, borderWidth, borderWidth, borderWidth);
389         _middleColor = innerAreaColor;
390     }
391 
392     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
393         buf.drawFrame(rc, _borderColor, _borderWidths, _middleColor);
394     }
395 
396     @property override int width() { return 1 + _borderWidths.left + _borderWidths.right; }
397     @property override int height() { return 1 + _borderWidths.top + _borderWidths.bottom; }
398     @property override Rect padding() { return _borderWidths; }
399 }
400 deprecated alias FrameDrawable = BorderDrawable;
401 
402 /// box shadows, can be blurred
403 class BoxShadowDrawable : Drawable {
404     protected int _offsetX;
405     protected int _offsetY;
406     protected int _blurSize;
407     protected uint _color;
408     protected Ref!ColorDrawBuf texture;
409 
410     this(int offsetX, int offsetY, uint blurSize = 0, uint color = 0x0) {
411         _offsetX = offsetX;
412         _offsetY = offsetY;
413         _blurSize = blurSize;
414         _color = color;
415         // now create a texture which will contain the shadow
416         uint size = 4 * blurSize + 1;
417         texture = new ColorDrawBuf(size, size); // TODO: get from/put to cache
418         // clear
419         texture.fill(color | 0xFF000000);
420         // draw a square in center of the texture
421         texture.fillRect(Rect(blurSize, blurSize, size - blurSize, size - blurSize), color);
422         // blur the square
423         texture.blur(blurSize);
424     }
425 
426     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
427         // this is a size of blurred part
428         uint b = _blurSize + _blurSize / 2 + 1;
429         // move and expand the shadow
430         rc.left += _offsetX - b;
431         rc.top += _offsetY - b;
432         rc.right += _offsetX + b;
433         rc.bottom += _offsetY + b;
434 
435         // apply new clipping to the DrawBuf to draw outside of the widget
436         auto saver = ClipRectSaver(buf, rc, 0, false);
437 
438         if (_blurSize > 0) {
439             // Manual nine-patch
440             uint w = texture.width;
441             uint h = texture.height;
442 
443             buf.drawFragment(rc.left, rc.top, texture, Rect(0, 0, b, b)); // top left
444             buf.drawRescaled(Rect(rc.left + b, rc.top, rc.right - b, rc.top + b), texture, Rect(b, 0, w - b, b)); // top center
445             buf.drawFragment(rc.right - b, rc.top, texture, Rect(w - b, 0, w, b)); // top right
446 
447             buf.drawRescaled(Rect(rc.left, rc.top + b, rc.left + b, rc.bottom - b), texture, Rect(0, b, b, h - b)); // middle left
448             buf.drawRescaled(Rect(rc.left + b, rc.top + b, rc.right - b, rc.bottom - b), texture, Rect(b, b, w - b, h - b)); // middle center
449             buf.drawRescaled(Rect(rc.right - b, rc.top + b, rc.right, rc.bottom - b), texture, Rect(w - b, b, w, h - b)); // middle right
450 
451             buf.drawFragment(rc.left, rc.bottom - b, texture, Rect(0, h - b, b, h)); // bottom left
452             buf.drawRescaled(Rect(rc.left + b, rc.bottom - b, rc.right - b, rc.bottom), texture, Rect(b, h - b, w - b, h)); // bottom center
453             buf.drawFragment(rc.right - b, rc.bottom - b, texture, Rect(w - b, h - b, w, h)); // bottom right
454 
455             // debug
456             //~ buf.drawFragment(rc.left, rc.top, texture, Rect(0, 0, w, h));
457         } else {
458             buf.fillRect(rc, _color);
459         }
460     }
461 
462     @property override int width() { return 1; }
463     @property override int height() { return 1; }
464 }
465 
466 
467 enum DimensionUnits {
468     pixels,
469     points,
470     percents
471 }
472 
473 /// decode size string, e.g. 1px or 2 or 3pt
474 static uint decodeDimension(string s) {
475     uint value = 0;
476     DimensionUnits units = DimensionUnits.pixels;
477     bool dotFound = false;
478     uint afterPointValue = 0;
479     uint afterPointDivider = 1;
480     foreach(c; s) {
481         int digit = -1;
482         if (c >='0' && c <= '9')
483             digit = c - '0';
484         if (digit >= 0) {
485             if (dotFound) {
486                 afterPointValue = afterPointValue * 10 + digit;
487                 afterPointDivider *= 10;
488             } else {
489                 value = value * 10 + digit;
490             }
491         } else if (c == 't') // just test by containing 't' - for NNNpt
492             units = DimensionUnits.points; // "pt"
493         else if (c == '%')
494             units = DimensionUnits.percents;
495         else if (c == '.')
496             dotFound = true;
497     }
498     // TODO: convert points to pixels
499     switch(units) {
500     case DimensionUnits.points:
501         // need to convert points to pixels
502         value |= SIZE_IN_POINTS_FLAG;
503         break;
504     case DimensionUnits.percents:
505         // need to convert percents
506         value = ((value * 100) + (afterPointValue * 100 / afterPointDivider)) | SIZE_IN_PERCENTS_FLAG;
507         break;
508     default:
509         break;
510     }
511     return value;
512 }
513 
514 /// decode angle; only Ndeg format for now
515 static uint decodeAngle(string s) {
516     int angle;
517     if (s.endsWith("deg"))
518         angle = to!int(s[0 .. $ - 3]);
519     else
520         Log.e("Invalid angle format: ", s);
521 
522     // transform the angle to [0, 360)
523     return ((angle % 360) + 360) % 360;
524 }
525 
526 static if (WIDGET_STYLE_CONSOLE) {
527     /**
528     Sample format:
529     {
530         text: [
531             "╔═╗",
532             "║ ║",
533             "╚═╝"],
534         backgroundColor: [0x000080], // put more values for individual colors of cells
535         textColor: [0xFF0000], // put more values for individual colors of cells
536         ninepatch: [1,1,1,1]
537     }
538     */
539     static Drawable createTextDrawable(string s) {
540         TextDrawable drawable = new TextDrawable(s);
541         if (drawable.width == 0 || drawable.height == 0)
542             return null;
543         return drawable;
544     }
545 }
546 
547 /// decode solid color / gradient / border drawable from string like #AARRGGBB, e.g. #5599AA
548 ///
549 /// SolidFillDrawable: #AARRGGBB  - e.g. #8090A0 or #80ffffff
550 /// GradientDrawable: #linear,Ndeg,firstColor,secondColor
551 /// BorderDrawable: #border,borderColor,borderWidth[,middleColor]
552 ///             or #border,borderColor,leftBorderWidth,topBorderWidth,rightBorderWidth,bottomBorderWidth[,middleColor]
553 ///                e.g. #border,#000000,2,#C0FFFFFF - black border of width 2 with 75% transparent white middle
554 ///                e.g. #border,#0000FF,2,3,4,5,#FFFFFF - blue border with left,top,right,bottom borders of width 2,3,4,5 and white inner area
555 static Drawable createColorDrawable(string s) {
556     Log.d("creating color drawable ", s);
557 
558     enum DrawableType { SolidColor, LinearGradient, Border, BoxShadow }
559     auto type = DrawableType.SolidColor;
560 
561     string[] items = s.split(',');
562     uint[] values;
563     int[] ivalues;
564     if (items.length != 0) {
565         if (items[0] == "#linear")
566             type = DrawableType.LinearGradient;
567         else if (items[0] == "#border")
568             type = DrawableType.Border;
569         else if (items[0] == "#box-shadow")
570             type = DrawableType.BoxShadow;
571         else if (items[0].startsWith("#"))
572             values ~= decodeHexColor(items[0]);
573 
574         foreach (i, item; items[1 .. $]) {
575             if (item.startsWith("#"))
576                 values ~= decodeHexColor(item);
577             else if (item.endsWith("deg"))
578                 values ~= decodeAngle(item);
579             else if (type == DrawableType.BoxShadow) // offsets may be negative
580                 ivalues ~= item.startsWith("-") ? -decodeDimension(item) : decodeDimension(item);
581             else
582                 values ~= decodeDimension(item);
583             if (i >= 6)
584                 break;
585         }
586     }
587 
588     if (type == DrawableType.SolidColor && values.length == 1) // only color #AARRGGBB
589         return new SolidFillDrawable(values[0]);
590     else if (type == DrawableType.LinearGradient && values.length == 3) // angle and two gradient colors
591         return new GradientDrawable(values[0], values[1], values[2]);
592     else if (type == DrawableType.Border) {
593         if (values.length == 2) // border color and border width, with transparent inner area - #AARRGGBB,NN
594             return new BorderDrawable(values[0], values[1]);
595         else if (values.length == 3) // border color, border width, inner area color - #AARRGGBB,NN,#AARRGGBB
596             return new BorderDrawable(values[0], values[1], values[2]);
597         else if (values.length == 5) // border color, border widths for left,top,right,bottom and transparent inner area - #AARRGGBB,NNleft,NNtop,NNright,NNbottom
598             return new BorderDrawable(values[0], Rect(values[1], values[2], values[3], values[4]));
599         else if (values.length == 6) // border color, border widths for left,top,right,bottom, inner area color - #AARRGGBB,NNleft,NNtop,NNright,NNbottom,#AARRGGBB
600             return new BorderDrawable(values[0], Rect(values[1], values[2], values[3], values[4]), values[5]);
601     } else if (type == DrawableType.BoxShadow) {
602         if (ivalues.length == 2 && values.length == 0) // shadow X and Y offsets
603             return new BoxShadowDrawable(ivalues[0], ivalues[1]);
604         else if (ivalues.length == 3 && values.length == 0) // shadow offsets and blur size
605             return new BoxShadowDrawable(ivalues[0], ivalues[1], ivalues[2]);
606         else if (ivalues.length == 3 && values.length == 1) // shadow offsets, blur size and color
607             return new BoxShadowDrawable(ivalues[0], ivalues[1], ivalues[2], values[0]);
608     }
609     Log.e("Invalid drawable string format: ", s);
610     return new EmptyDrawable(); // invalid format - just return empty drawable
611 }
612 
613 static if (WIDGET_STYLE_CONSOLE) {
614     /**
615         Text image drawable.
616         Resource file extension: .tim
617         Image format is JSON based. Sample:
618                 {
619                     text: [
620                        "╔═╗",
621                        "║ ║",
622                        "╚═╝"],
623                     backgroundColor: [0x000080],
624                     textColor: [0xFF0000],
625                     ninepatch: [1,1,1,1]
626                 }
627 
628         Short form:
629 
630     {'╔═╗' '║ ║' '╚═╝' bc 0x000080 tc 0xFF0000 ninepatch 1 1 1 1}
631 
632     */
633 
634     abstract class ConsoleDrawBuf : DrawBuf
635     {
636         abstract void drawChar(int x, int y, dchar ch, uint color, uint bgcolor);
637     }
638 
639     class TextDrawable : Drawable {
640         private int _width;
641         private int _height;
642         private dchar[] _text;
643         private uint[] _bgColors;
644         private uint[] _textColors;
645         private Rect _padding;
646         private Rect _ninePatch;
647         private bool _tiled;
648         private bool _stretched;
649         private bool _hasNinePatch;
650         this(int dx, int dy, dstring text, uint textColor, uint bgColor) {
651             _width = dx;
652             _height = dy;
653             _text.assumeSafeAppend;
654             for (int i = 0; i < text.length && i < dx * dy; i++)
655                 _text ~= text[i];
656             for (int i = cast(int)_text.length; i < dx * dy; i++)
657                 _text ~= ' ';
658             _textColors.assumeSafeAppend;
659             _bgColors.assumeSafeAppend;
660             for (int i = 0; i < dx * dy; i++) {
661                 _textColors ~= textColor;
662                 _bgColors ~= bgColor;
663             }
664         }
665         this(string src) {
666             import std.utf;
667             this(toUTF32(src));
668         }
669         /**
670            Create from text drawable source file format:
671            {
672             text:
673            "text line 1"
674            "text line 2"
675            "text line 3"
676            backgroundColor: 0xFFFFFF [,0xFFFFFF]*
677            textColor: 0x000000, [,0x000000]*
678            ninepatch: left,top,right,bottom
679            padding: left,top,right,bottom
680             }
681 
682            Text lines may be in "" or '' or `` quotes.
683            bc can be used instead of backgroundColor, tc instead of textColor
684 
685            Sample short form:
686            { 'line1' 'line2' 'line3' bc 0xFFFFFFFF tc 0x808080 stretch }
687         */
688         this(dstring src) {
689             import dlangui.dml.tokenizer;
690             import std.utf;
691             Token[] tokens = tokenize(toUTF8(src), ["//"], true, true, true);
692             dstring[] lines;
693             enum Mode {
694                 None,
695                 Text,
696                 BackgroundColor,
697                 TextColor,
698                 Padding,
699                 NinePatch,
700             }
701             Mode mode = Mode.Text;
702             uint[] bg;
703             uint[] col;
704             uint[] pad;
705             uint[] nine;
706             for (int i; i < tokens.length; i++) {
707                 if (tokens[i].type == TokenType.ident) {
708                     if (tokens[i].text == "backgroundColor" || tokens[i].text == "bc")
709                         mode = Mode.BackgroundColor;
710                     else if (tokens[i].text == "textColor" || tokens[i].text == "tc")
711                         mode = Mode.TextColor;
712                     else if (tokens[i].text == "text")
713                         mode = Mode.Text;
714                     else if (tokens[i].text == "stretch")
715                         _stretched = true;
716                     else if (tokens[i].text == "tile")
717                         _tiled = true;
718                     else if (tokens[i].text == "padding") {
719                         mode = Mode.Padding;
720                     } else if (tokens[i].text == "ninepatch") {
721                         _hasNinePatch = true;
722                         mode = Mode.NinePatch;
723                     } else
724                         mode = Mode.None;
725                 } else if (tokens[i].type == TokenType.integer) {
726                     switch(mode) {
727                         case Mode.BackgroundColor: _bgColors ~= tokens[i].intvalue; break;
728                         case Mode.TextColor:
729                         case Mode.Text:
730                             _textColors ~= tokens[i].intvalue; break;
731                         case Mode.Padding: pad ~= tokens[i].intvalue; break;
732                         case Mode.NinePatch: nine ~= tokens[i].intvalue; break;
733                         default:
734                             break;
735                     }
736                 } else if (tokens[i].type == TokenType.str && mode == Mode.Text) {
737                     dstring line = toUTF32(tokens[i].text);
738                     lines ~= line;
739                     if (_width < line.length)
740                         _width = cast(int)line.length;
741                 }
742             }
743             // pad and convert text
744             _height = cast(int)lines.length;
745             if (!_height) {
746                 _width = 0;
747                 return;
748             }
749             for (int y = 0; y < _height; y++) {
750                 for (int x = 0; x < _width; x++) {
751                     if (x < lines[y].length)
752                         _text ~= lines[y][x];
753                     else
754                         _text ~= ' ';
755                 }
756             }
757             // pad padding and ninepatch
758             for (int k = 1; k <= 4; k++) {
759                 if (nine.length < k)
760                     nine ~= 0;
761                 if (pad.length < k)
762                     pad ~= 0;
763                 //if (pad[k-1] < nine[k-1])
764                 //    pad[k-1] = nine[k-1];
765             }
766             _padding = Rect(pad[0], pad[1], pad[2], pad[3]);
767             _ninePatch = Rect(nine[0], nine[1], nine[2], nine[3]);
768             // pad colors
769             for (int k = 1; k <= _width * _height; k++) {
770                 if (_textColors.length < k)
771                     _textColors ~= _textColors.length ? _textColors[$ - 1] : 0;
772                 if (_bgColors.length < k)
773                     _bgColors ~= _bgColors.length ? _bgColors[$ - 1] : 0xFFFFFFFF;
774             }
775         }
776         @property override int width() {
777             return _width;
778         }
779         @property override int height() {
780             return _height;
781         }
782         @property override Rect padding() {
783             return _padding;
784         }
785 
786         protected void drawChar(ConsoleDrawBuf buf, int srcx, int srcy, int dstx, int dsty) {
787             if (srcx < 0 || srcx >= _width || srcy < 0 || srcy >= _height)
788                 return;
789             int index = srcy * _width + srcx;
790             if (_textColors[index].isFullyTransparentColor && _bgColors[index].isFullyTransparentColor)
791                 return; // do not draw
792             buf.drawChar(dstx, dsty, _text[index], _textColors[index], _bgColors[index]);
793         }
794 
795         private static int wrapNinePatch(int v, int width, int ninewidth, int left, int right) {
796             if (v < left)
797                 return v;
798             if (v >= width - right)
799                 return v - (width - right) + (ninewidth - right);
800             return left + (ninewidth - left - right) * (v - left) / (width - left - right);
801         }
802 
803         override void drawTo(DrawBuf drawbuf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
804             if (!_width || !_height)
805                 return; // empty image
806             ConsoleDrawBuf buf = cast(ConsoleDrawBuf)drawbuf;
807             if (!buf) // wrong draw buffer
808                 return;
809             if (_hasNinePatch || _tiled || _stretched) {
810                 for (int y = 0; y < rc.height; y++) {
811                     for (int x = 0; x < rc.width; x++) {
812                         int srcx = wrapNinePatch(x, rc.width, _width, _ninePatch.left, _ninePatch.right);
813                         int srcy = wrapNinePatch(y, rc.height, _height, _ninePatch.top, _ninePatch.bottom);
814                         drawChar(buf, srcx, srcy, rc.left + x, rc.top + y);
815                     }
816                 }
817             } else {
818                 for (int y = 0; y < rc.height && y < _height; y++) {
819                     for (int x = 0; x < rc.width && x < _width; x++) {
820                         drawChar(buf, x, y, rc.left + x, rc.top + y);
821                     }
822                 }
823             }
824             //buf.drawImage(rc.left, rc.top, _image);
825         }
826     }
827 }
828 
829 class ImageDrawable : Drawable {
830     protected DrawBufRef _image;
831     protected bool _tiled;
832 
833     debug static __gshared int _instanceCount;
834     debug @property static int instanceCount() { return _instanceCount; }
835 
836     this(ref DrawBufRef image, bool tiled = false, bool ninePatch = false) {
837         _image = image;
838         _tiled = tiled;
839         if (ninePatch)
840             _image.detectNinePatch();
841         debug _instanceCount++;
842         debug(resalloc) Log.d("Created ImageDrawable, count=", _instanceCount);
843     }
844     ~this() {
845         _image.clear();
846         debug _instanceCount--;
847         debug(resalloc) Log.d("Destroyed ImageDrawable, count=", _instanceCount);
848     }
849 
850     @property override int width() {
851         if (_image.isNull)
852             return 0;
853         if (_image.hasNinePatch)
854             return _image.width - 2;
855         return _image.width;
856     }
857 
858     @property override int height() {
859         if (_image.isNull)
860             return 0;
861         if (_image.hasNinePatch)
862             return _image.height - 2;
863         return _image.height;
864     }
865 
866     @property override Rect padding() {
867         if (!_image.isNull && _image.hasNinePatch)
868             return _image.ninePatch.padding;
869         return Rect(0,0,0,0);
870     }
871 
872     private static void correctFrameBounds(ref int n1, ref int n2, ref int n3, ref int n4) {
873         if (n1 > n2) {
874             //assert(n2 - n1 == n4 - n3);
875             int middledist = (n1 + n2) / 2 - n1;
876             n1 = n2 = n1 + middledist;
877             n3 = n4 = n3 + middledist;
878         }
879     }
880 
881     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
882         if (_image.isNull)
883             return;
884         if (_image.hasNinePatch) {
885             // draw nine patch
886             const NinePatch * p = _image.ninePatch;
887             //Log.d("drawing nine patch image with frame ", p.frame, " padding ", p.padding);
888             int w = width;
889             int h = height;
890             Rect srcrect = Rect(1, 1, w + 1, h + 1);
891             if (true) { //buf.applyClipping(dstrect, srcrect)) {
892                 int x0 = srcrect.left;
893                 int x1 = srcrect.left + p.frame.left;
894                 int x2 = srcrect.right - p.frame.right;
895                 int x3 = srcrect.right;
896                 int y0 = srcrect.top;
897                 int y1 = srcrect.top + p.frame.top;
898                 int y2 = srcrect.bottom - p.frame.bottom;
899                 int y3 = srcrect.bottom;
900                 int dstx0 = rc.left;
901                 int dstx1 = rc.left + p.frame.left;
902                 int dstx2 = rc.right - p.frame.right;
903                 int dstx3 = rc.right;
904                 int dsty0 = rc.top;
905                 int dsty1 = rc.top + p.frame.top;
906                 int dsty2 = rc.bottom - p.frame.bottom;
907                 int dsty3 = rc.bottom;
908                 //Log.d("x bounds: ", x0, ", ", x1, ", ", x2, ", ", x3, " dst ", dstx0, ", ", dstx1, ", ", dstx2, ", ", dstx3);
909                 //Log.d("y bounds: ", y0, ", ", y1, ", ", y2, ", ", y3, " dst ", dsty0, ", ", dsty1, ", ", dsty2, ", ", dsty3);
910 
911                 correctFrameBounds(x1, x2, dstx1, dstx2);
912                 correctFrameBounds(y1, y2, dsty1, dsty2);
913 
914                 //correctFrameBounds(x1, x2);
915                 //correctFrameBounds(y1, y2);
916                 //correctFrameBounds(dstx1, dstx2);
917                 //correctFrameBounds(dsty1, dsty2);
918                 if (y0 < y1 && dsty0 < dsty1) {
919                     // top row
920                     if (x0 < x1 && dstx0 < dstx1)
921                         buf.drawFragment(dstx0, dsty0, _image.get, Rect(x0, y0, x1, y1)); // top left
922                     if (x1 < x2 && dstx1 < dstx2)
923                         buf.drawRescaled(Rect(dstx1, dsty0, dstx2, dsty1), _image.get, Rect(x1, y0, x2, y1)); // top center
924                     if (x2 < x3 && dstx2 < dstx3)
925                         buf.drawFragment(dstx2, dsty0, _image.get, Rect(x2, y0, x3, y1)); // top right
926                 }
927                 if (y1 < y2 && dsty1 < dsty2) {
928                     // middle row
929                     if (x0 < x1 && dstx0 < dstx1)
930                         buf.drawRescaled(Rect(dstx0, dsty1, dstx1, dsty2), _image.get, Rect(x0, y1, x1, y2)); // middle center
931                     if (x1 < x2 && dstx1 < dstx2)
932                         buf.drawRescaled(Rect(dstx1, dsty1, dstx2, dsty2), _image.get, Rect(x1, y1, x2, y2)); // center
933                     if (x2 < x3 && dstx2 < dstx3)
934                         buf.drawRescaled(Rect(dstx2, dsty1, dstx3, dsty2), _image.get, Rect(x2, y1, x3, y2)); // middle center
935                 }
936                 if (y2 < y3 && dsty2 < dsty3) {
937                     // bottom row
938                     if (x0 < x1 && dstx0 < dstx1)
939                         buf.drawFragment(dstx0, dsty2, _image.get, Rect(x0, y2, x1, y3)); // bottom left
940                     if (x1 < x2 && dstx1 < dstx2)
941                         buf.drawRescaled(Rect(dstx1, dsty2, dstx2, dsty3), _image.get, Rect(x1, y2, x2, y3)); // bottom center
942                     if (x2 < x3 && dstx2 < dstx3)
943                         buf.drawFragment(dstx2, dsty2, _image.get, Rect(x2, y2, x3, y3)); // bottom right
944                 }
945             }
946         } else if (_tiled) {
947             // tiled
948             int imgdx = _image.width;
949             int imgdy = _image.height;
950             tilex0 %= imgdx;
951             if (tilex0 < 0)
952                 tilex0 += imgdx;
953             tiley0 %= imgdy;
954             if (tiley0 < 0)
955                 tiley0 += imgdy;
956             int xx0 = rc.left;
957             int yy0 = rc.top;
958             if (tilex0)
959                 xx0 -= imgdx - tilex0;
960             if (tiley0)
961                 yy0 -= imgdy - tiley0;
962             for (int yy = yy0; yy < rc.bottom; yy += imgdy) {
963                 for (int xx = xx0; xx < rc.right; xx += imgdx) {
964                     Rect dst = Rect(xx, yy, xx + imgdx, yy + imgdy);
965                     Rect src = Rect(0, 0, imgdx, imgdy);
966                     if (dst.intersects(rc)) {
967                         Rect sr = src;
968                         if (dst.right > rc.right)
969                             sr.right -= dst.right - rc.right;
970                         if (dst.bottom > rc.bottom)
971                             sr.bottom -= dst.bottom - rc.bottom;
972                         if (!sr.empty)
973                             buf.drawFragment(dst.left, dst.top, _image.get, sr);
974                     }
975                 }
976             }
977         } else {
978             // rescaled or normal
979             if (rc.width != _image.width || rc.height != _image.height)
980                 buf.drawRescaled(rc, _image.get, Rect(0, 0, _image.width, _image.height));
981             else
982                 buf.drawImage(rc.left, rc.top, _image);
983         }
984     }
985 }
986 
987 string attrValue(Element item, string attrname, string attrname2 = null) {
988     if (attrname in item.tag.attr)
989         return item.tag.attr[attrname];
990     if (attrname2 && attrname2 in item.tag.attr)
991         return item.tag.attr[attrname2];
992     return null;
993 }
994 
995 string attrValue(ref string[string] attr, string attrname, string attrname2 = null) {
996     if (attrname in attr)
997         return attr[attrname];
998     if (attrname2 && attrname2 in attr)
999         return attr[attrname2];
1000     return null;
1001 }
1002 
1003 void extractStateFlag(ref string[string] attr, string attrName, string attrName2, State state, ref uint stateMask, ref uint stateValue) {
1004     string value = attrValue(attr, attrName, attrName2);
1005     if (value !is null) {
1006         if (value.equal("true"))
1007             stateValue |= state;
1008         stateMask |= state;
1009     }
1010 }
1011 
1012 /// converts XML attribute name to State (see http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList)
1013 void extractStateFlags(ref string[string] attr, ref uint stateMask, ref uint stateValue) {
1014     extractStateFlag(attr, "state_pressed", "android:state_pressed", State.Pressed, stateMask, stateValue);
1015     extractStateFlag(attr, "state_focused", "android:state_focused", State.Focused, stateMask, stateValue);
1016     extractStateFlag(attr, "state_default", "android:state_default", State.Default, stateMask, stateValue);
1017     extractStateFlag(attr, "state_hovered", "android:state_hovered", State.Hovered, stateMask, stateValue);
1018     extractStateFlag(attr, "state_selected", "android:state_selected", State.Selected, stateMask, stateValue);
1019     extractStateFlag(attr, "state_checkable", "android:state_checkable", State.Checkable, stateMask, stateValue);
1020     extractStateFlag(attr, "state_checked", "android:state_checked", State.Checked, stateMask, stateValue);
1021     extractStateFlag(attr, "state_enabled", "android:state_enabled", State.Enabled, stateMask, stateValue);
1022     extractStateFlag(attr, "state_activated", "android:state_activated", State.Activated, stateMask, stateValue);
1023     extractStateFlag(attr, "state_window_focused", "android:state_window_focused", State.WindowFocused, stateMask, stateValue);
1024 }
1025 
1026 /*
1027 sample:
1028 (prefix android: is optional)
1029 
1030 <?xml version="1.0" encoding="utf-8"?>
1031 <selector xmlns:android="http://schemas.android.com/apk/res/android"
1032 android:constantSize=["true" | "false"]
1033 android:dither=["true" | "false"]
1034 android:variablePadding=["true" | "false"] >
1035 <item
1036 android:drawable="@[package:]drawable/drawable_resource"
1037 android:state_pressed=["true" | "false"]
1038 android:state_focused=["true" | "false"]
1039 android:state_hovered=["true" | "false"]
1040 android:state_selected=["true" | "false"]
1041 android:state_checkable=["true" | "false"]
1042 android:state_checked=["true" | "false"]
1043 android:state_enabled=["true" | "false"]
1044 android:state_activated=["true" | "false"]
1045 android:state_window_focused=["true" | "false"] />
1046 </selector>
1047 */
1048 
1049 /// Drawable which is drawn depending on state (see http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList)
1050 class StateDrawable : Drawable {
1051 
1052     static class StateItem {
1053         uint stateMask;
1054         uint stateValue;
1055         ColorTransform transform;
1056         DrawableRef drawable;
1057         @property bool matchState(uint state) {
1058             return (stateMask & state) == stateValue;
1059         }
1060     }
1061     // list of states
1062     protected StateItem[] _stateList;
1063     // max paddings for all states
1064     protected Rect _paddings;
1065     // max drawable size for all states
1066     protected Point _size;
1067 
1068     ~this() {
1069         foreach(ref item; _stateList)
1070             destroy(item);
1071         _stateList = null;
1072     }
1073 
1074     void addState(uint stateMask, uint stateValue, string resourceId, ref ColorTransform transform) {
1075         StateItem item = new StateItem();
1076         item.stateMask = stateMask;
1077         item.stateValue = stateValue;
1078         item.drawable = drawableCache.get(resourceId, transform);
1079         itemAdded(item);
1080     }
1081 
1082     void addState(uint stateMask, uint stateValue, DrawableRef drawable) {
1083         StateItem item = new StateItem();
1084         item.stateMask = stateMask;
1085         item.stateValue = stateValue;
1086         item.drawable = drawable;
1087         itemAdded(item);
1088     }
1089 
1090     private void itemAdded(StateItem item) {
1091         _stateList ~= item;
1092         if (!item.drawable.isNull) {
1093             if (_size.x < item.drawable.width)
1094                 _size.x = item.drawable.width;
1095             if (_size.y < item.drawable.height)
1096                 _size.y = item.drawable.height;
1097             _paddings.setMax(item.drawable.padding);
1098         }
1099     }
1100 
1101     /// parse 4 comma delimited integers
1102     static bool parseList4(T)(string value, ref T[4] items) {
1103         int index = 0;
1104         int p = 0;
1105         int start = 0;
1106         for (;p < value.length && index < 4; p++) {
1107             while (p < value.length && value[p] != ',')
1108                 p++;
1109             if (p > start) {
1110                 int end = p;
1111                 string s = value[start .. end];
1112                 items[index++] = to!T(s);
1113                 start = p + 1;
1114             }
1115         }
1116         return index == 4;
1117     }
1118     private static uint colorTransformFromStringAdd(string value) {
1119         if (value is null)
1120             return COLOR_TRANSFORM_OFFSET_NONE;
1121         int [4]n;
1122         if (!parseList4(value, n))
1123             return COLOR_TRANSFORM_OFFSET_NONE;
1124         foreach (ref item; n) {
1125             item = item / 2 + 0x80;
1126             if (item < 0)
1127                 item = 0;
1128             if (item > 0xFF)
1129                 item = 0xFF;
1130         }
1131         return (n[0] << 24) | (n[1] << 16) | (n[2] << 8) | (n[3] << 0);
1132     }
1133     private static uint colorTransformFromStringMult(string value) {
1134         if (value is null)
1135             return COLOR_TRANSFORM_MULTIPLY_NONE;
1136         float[4] n;
1137         uint[4] nn;
1138         if (!parseList4!float(value, n))
1139             return COLOR_TRANSFORM_MULTIPLY_NONE;
1140         foreach(i; 0 .. 4) {
1141             int res = cast(int)(n[i] * 0x40);
1142             if (res < 0)
1143                 res = 0;
1144             if (res > 0xFF)
1145                 res = 0xFF;
1146             nn[i] = res;
1147         }
1148         return (nn[0] << 24) | (nn[1] << 16) | (nn[2] << 8) | (nn[3] << 0);
1149     }
1150 
1151     bool load(Element element) {
1152         foreach(item; element.elements) {
1153             if (item.tag.name.equal("item")) {
1154                 string drawableId = attrValue(item, "drawable", "android:drawable");
1155                 if (drawableId.startsWith("@drawable/"))
1156                     drawableId = drawableId[10 .. $];
1157                 ColorTransform transform;
1158                 transform.addBefore = colorTransformFromStringAdd(attrValue(item, "color_transform_add1", "android:transform_color_add1"));
1159                 transform.multiply = colorTransformFromStringMult(attrValue(item, "color_transform_mul", "android:transform_color_mul"));
1160                 transform.addAfter = colorTransformFromStringAdd(attrValue(item, "color_transform_add2", "android:transform_color_add2"));
1161                 if (drawableId !is null) {
1162                     uint stateMask, stateValue;
1163                     extractStateFlags(item.tag.attr, stateMask, stateValue);
1164                     if (drawableId !is null) {
1165                         addState(stateMask, stateValue, drawableId, transform);
1166                     }
1167                 }
1168             }
1169         }
1170         return _stateList.length > 0;
1171     }
1172 
1173     /// load from XML file
1174     bool load(string filename) {
1175         try {
1176             string s = cast(string)loadResourceBytes(filename);
1177             if (!s) {
1178                 Log.e("Cannot read drawable resource from file ", filename);
1179                 return false;
1180             }
1181 
1182             // Check for well-formedness
1183             //check(s);
1184 
1185             // Make a DOM tree
1186             auto doc = new Document(s);
1187 
1188             return load(doc);
1189         } catch (CheckException e) {
1190             Log.e("Invalid XML file ", filename);
1191             return false;
1192         }
1193     }
1194 
1195     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
1196         foreach(ref item; _stateList)
1197             if (item.matchState(state)) {
1198                 if (!item.drawable.isNull) {
1199                     item.drawable.drawTo(buf, rc, state, tilex0, tiley0);
1200                 }
1201                 return;
1202             }
1203     }
1204 
1205     @property override int width() {
1206         return _size.x;
1207     }
1208     @property override int height() {
1209         return _size.y;
1210     }
1211     @property override Rect padding() {
1212         return _paddings;
1213     }
1214 }
1215 
1216 /// Drawable which allows to combine together background image, gradient, borders, box shadows, etc.
1217 class CombinedDrawable : Drawable {
1218 
1219     DrawableRef boxShadow;
1220     DrawableRef background;
1221     DrawableRef border;
1222 
1223     this(uint backgroundColor, string backgroundImageId, string borderDescription, string boxShadowDescription) {
1224         boxShadow = boxShadowDescription !is null ? drawableCache.get("#box-shadow," ~ boxShadowDescription) : new EmptyDrawable;
1225         background =
1226             (backgroundImageId !is null) ? drawableCache.get(backgroundImageId) :
1227             (!backgroundColor.isFullyTransparentColor) ? new SolidFillDrawable(backgroundColor) : null;
1228         if (background is null)
1229             background = new EmptyDrawable;
1230         border = borderDescription !is null ? drawableCache.get("#border," ~ borderDescription) : new EmptyDrawable;
1231     }
1232 
1233     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
1234         boxShadow.drawTo(buf, rc, state, tilex0, tiley0);
1235         // make background image smaller to fit borders
1236         Rect backrc = rc;
1237         backrc.left += border.padding.left;
1238         backrc.top += border.padding.top;
1239         backrc.right -= border.padding.right;
1240         backrc.bottom -= border.padding.bottom;
1241         background.drawTo(buf, backrc, state, tilex0, tiley0);
1242         border.drawTo(buf, rc, state, tilex0, tiley0);
1243     }
1244 
1245     @property override int width() { return background.width + border.padding.left + border.padding.right; }
1246     @property override int height() { return background.height + border.padding.top + border.padding.bottom; }
1247     @property override Rect padding() {
1248         return Rect(background.padding.left + border.padding.left, background.padding.top + border.padding.top,
1249             background.padding.right + border.padding.right, background.padding.bottom + border.padding.bottom);
1250     }
1251 }
1252 
1253 
1254 alias DrawableRef = Ref!Drawable;
1255 
1256 
1257 
1258 
1259 static if (BACKEND_GUI) {
1260 /// decoded raster images cache (png, jpeg) -- access by filenames
1261 class ImageCache {
1262 
1263     static class ImageCacheItem {
1264         string _filename;
1265         DrawBufRef _drawbuf;
1266         DrawBufRef[ColorTransform] _transformMap;
1267 
1268         bool _error; // flag to avoid loading of file if it has been failed once
1269         bool _used;
1270 
1271         this(string filename) {
1272             _filename = filename;
1273         }
1274 
1275         /// get normal image
1276         @property ref DrawBufRef get() {
1277             if (!_drawbuf.isNull || _error) {
1278                 _used = true;
1279                 return _drawbuf;
1280             }
1281             immutable ubyte[] data = loadResourceBytes(_filename);
1282             if (data) {
1283                 _drawbuf = loadImage(data, _filename);
1284                 if (_filename.endsWith(".9.png"))
1285                     _drawbuf.detectNinePatch();
1286                 _used = true;
1287             }
1288             if (_drawbuf.isNull)
1289                 _error = true;
1290             return _drawbuf;
1291         }
1292         /// get color transformed image
1293         @property ref DrawBufRef get(ref ColorTransform transform) {
1294             if (transform.empty)
1295                 return get();
1296             if (transform in _transformMap)
1297                 return _transformMap[transform];
1298             DrawBufRef src = get();
1299             if (src.isNull)
1300                 _transformMap[transform] = src;
1301             else {
1302                 DrawBufRef t = src.transformColors(transform);
1303                 _transformMap[transform] = t;
1304             }
1305             return _transformMap[transform];
1306         }
1307 
1308         /// remove from memory, will cause reload on next access
1309         void compact() {
1310             if (!_drawbuf.isNull)
1311                 _drawbuf.clear();
1312         }
1313         /// mark as not used
1314         void checkpoint() {
1315             _used = false;
1316         }
1317         /// cleanup if unused since last checkpoint
1318         void cleanup() {
1319             if (!_used)
1320                 compact();
1321         }
1322     }
1323     ImageCacheItem[string] _map;
1324 
1325     /// get and cache image
1326     ref DrawBufRef get(string filename) {
1327         if (filename in _map) {
1328             return _map[filename].get;
1329         }
1330         ImageCacheItem item = new ImageCacheItem(filename);
1331         _map[filename] = item;
1332         return item.get;
1333     }
1334 
1335     /// get and cache color transformed image
1336     ref DrawBufRef get(string filename, ref ColorTransform transform) {
1337         if (transform.empty)
1338             return get(filename);
1339         if (filename in _map) {
1340             return _map[filename].get(transform);
1341         }
1342         ImageCacheItem item = new ImageCacheItem(filename);
1343         _map[filename] = item;
1344         return item.get(transform);
1345     }
1346     // clear usage flags for all entries
1347     void checkpoint() {
1348         foreach (item; _map)
1349             item.checkpoint();
1350     }
1351     // removes entries not used after last call of checkpoint() or cleanup()
1352     void cleanup() {
1353         foreach (item; _map)
1354             item.cleanup();
1355     }
1356 
1357     this() {
1358         debug Log.i("Creating ImageCache");
1359     }
1360     ~this() {
1361         debug Log.i("Destroying ImageCache");
1362         foreach (ref item; _map) {
1363             destroy(item);
1364             item = null;
1365         }
1366         _map.destroy();
1367     }
1368 }
1369 
1370 __gshared ImageCache _imageCache;
1371 /// image cache singleton
1372 @property ImageCache imageCache() { return _imageCache; }
1373 /// image cache singleton
1374 @property void imageCache(ImageCache cache) {
1375     if (_imageCache !is null)
1376         destroy(_imageCache);
1377     _imageCache = cache;
1378 }
1379 }
1380 
1381 __gshared DrawableCache _drawableCache;
1382 /// drawable cache singleton
1383 @property DrawableCache drawableCache() { return _drawableCache; }
1384 /// drawable cache singleton
1385 @property void drawableCache(DrawableCache cache) {
1386     if (_drawableCache !is null)
1387         destroy(_drawableCache);
1388     _drawableCache = cache;
1389 }
1390 
1391 class DrawableCache {
1392     static class DrawableCacheItem {
1393         string _id;
1394         string _filename;
1395         bool _tiled;
1396         DrawableRef _drawable;
1397         DrawableRef[ColorTransform] _transformed;
1398 
1399         bool _error; // flag to avoid loading of file if it has been failed once
1400         bool _used;
1401 
1402         this(string id, string filename, bool tiled) {
1403             _id = id;
1404             _filename = filename;
1405             _tiled = tiled;
1406             _error = filename is null;
1407             debug ++_instanceCount;
1408             debug(resalloc) Log.d("Created DrawableCacheItem, count=", _instanceCount);
1409         }
1410         debug private static __gshared int _instanceCount;
1411         debug @property static int instanceCount() { return _instanceCount; }
1412 
1413         ~this() {
1414             _drawable.clear();
1415             foreach(ref t; _transformed)
1416                 t.clear();
1417             _transformed.destroy();
1418             debug --_instanceCount;
1419             debug(resalloc) Log.d("Destroyed DrawableCacheItem, count=", _instanceCount);
1420         }
1421         /// remove from memory, will cause reload on next access
1422         void compact() {
1423             if (!_drawable.isNull)
1424                 _drawable.clear();
1425             foreach(t; _transformed)
1426                 t.clear();
1427             _transformed.destroy();
1428         }
1429         /// mark as not used
1430         void checkpoint() {
1431             _used = false;
1432         }
1433         /// cleanup if unused since last checkpoint
1434         void cleanup() {
1435             if (!_used)
1436                 compact();
1437         }
1438 
1439         /// returns drawable (loads from file if necessary)
1440         @property ref DrawableRef drawable(in ColorTransform transform = ColorTransform()) {
1441             _used = true;
1442             if (!transform.empty && transform in _transformed)
1443                 return _transformed[transform];
1444             if (!_drawable.isNull || _error)
1445                 return _drawable;
1446 
1447             // not in cache - create it
1448             Drawable dr = makeDrawableFromId(_filename, _tiled, transform);
1449             _error = dr is null;
1450             if (transform.empty) {
1451                 _drawable = dr;
1452                 return _drawable;
1453             } else {
1454                 _transformed[transform] = dr;
1455                 return _transformed[transform];
1456             }
1457         }
1458     }
1459 
1460     void clear() {
1461         Log.d("DrawableCache.clear()");
1462         _idToFileMap.destroy();
1463         foreach(DrawableCacheItem item; _idToDrawableMap)
1464             item.drawable.clear();
1465         _idToDrawableMap.destroy();
1466     }
1467     // clear usage flags for all entries
1468     void checkpoint() {
1469         foreach (item; _idToDrawableMap)
1470             item.checkpoint();
1471     }
1472     // removes entries not used after last call of checkpoint() or cleanup()
1473     void cleanup() {
1474         foreach (item; _idToDrawableMap)
1475             item.cleanup();
1476     }
1477 
1478     string[] _resourcePaths;
1479     string[string] _idToFileMap;
1480     DrawableCacheItem[string] _idToDrawableMap;
1481     DrawableRef _nullDrawable;
1482 
1483     ref DrawableRef get(string id, in ColorTransform transform = ColorTransform()) {
1484         id = id.strip;
1485         if (id.equal("@null"))
1486             return _nullDrawable;
1487         if (id in _idToDrawableMap)
1488             return _idToDrawableMap[id].drawable(transform);
1489         // not found - create it
1490         string resourceId = id;
1491         bool tiled = false;
1492         if (id.endsWith(".tiled")) {
1493             resourceId = id[0..$-6]; // remove .tiled
1494             tiled = true;
1495         }
1496         string filename = findResource(resourceId);
1497         auto item = new DrawableCacheItem(id, filename, tiled);
1498         _idToDrawableMap[id] = item;
1499         return item.drawable(transform);
1500     }
1501 
1502     @property string[] resourcePaths() {
1503         return _resourcePaths;
1504     }
1505     /// set resource directory paths as variable number of parameters
1506     void setResourcePaths(string[] paths ...) {
1507         resourcePaths(paths);
1508     }
1509     /// set resource directory paths array (only existing dirs will be added)
1510     @property void resourcePaths(string[] paths) {
1511         string[] existingPaths;
1512         foreach(path; paths) {
1513             if (exists(path) && isDir(path)) {
1514                 existingPaths ~= path;
1515                 Log.d("DrawableCache: adding path ", path, " to resource dir list.");
1516             } else {
1517                 Log.d("DrawableCache: path ", path, " does not exist.");
1518             }
1519         }
1520         _resourcePaths = existingPaths;
1521         clear();
1522     }
1523 
1524     /// concatenates path with resource id and extension, returns pathname if there is such file, null if file does not exist
1525     private string checkFileName(string path, string id, string extension) {
1526         char[] fn = path.dup;
1527         fn ~= id;
1528         fn ~= extension;
1529         if (exists(fn) && isFile(fn))
1530             return fn.dup;
1531         return null;
1532     }
1533 
1534     /// get resource file full pathname by resource id, null if not found
1535     string findResource(string id) {
1536         if (id.startsWith("#") || id.startsWith("{"))
1537             return id; // it's not a file name
1538         if (id in _idToFileMap)
1539             return _idToFileMap[id];
1540         EmbeddedResource * embedded = embeddedResourceList.findAutoExtension(id);
1541         if (embedded) {
1542             string fn = EMBEDDED_RESOURCE_PREFIX ~ embedded.name;
1543             _idToFileMap[id] = fn;
1544             return fn;
1545         }
1546         foreach(string path; _resourcePaths) {
1547             string fn;
1548             fn = checkFileName(path, id, ".xml");
1549             if (fn is null && WIDGET_STYLE_CONSOLE)
1550                 fn = checkFileName(path, id, ".tim");
1551             if (fn is null)
1552                 fn = checkFileName(path, id, ".png");
1553             if (fn is null)
1554                 fn = checkFileName(path, id, ".9.png");
1555             if (fn is null)
1556                 fn = checkFileName(path, id, ".jpg");
1557             if (fn !is null) {
1558                 _idToFileMap[id] = fn;
1559                 return fn;
1560             }
1561         }
1562         Log.w("resource ", id, " is not found");
1563         return null;
1564     }
1565     static if (BACKEND_GUI) {
1566         /// get image (DrawBuf) from imageCache by resource id
1567         DrawBufRef getImage(string id) {
1568             DrawBufRef res;
1569             string fname = findResource(id);
1570             if (fname.endsWith(".png") || fname.endsWith(".jpg"))
1571                 return imageCache.get(fname);
1572             return res;
1573         }
1574     }
1575     this() {
1576         debug Log.i("Creating DrawableCache");
1577     }
1578     ~this() {
1579         debug(resalloc) Log.e("Drawable instace count before destroying of DrawableCache: ", ImageDrawable.instanceCount);
1580 
1581         //Log.i("Destroying DrawableCache _idToDrawableMap.length=", _idToDrawableMap.length);
1582         Log.i("Destroying DrawableCache");
1583         foreach (ref item; _idToDrawableMap) {
1584             destroy(item);
1585             item = null;
1586         }
1587         _idToDrawableMap.destroy();
1588         debug if(ImageDrawable.instanceCount) Log.e("Drawable instace count after destroying of DrawableCache: ", ImageDrawable.instanceCount);
1589     }
1590 }
1591 
1592 
1593 /// This function takes an id and creates a drawable
1594 /// id may be a name of file, #directive, color or json
1595 private Drawable makeDrawableFromId(in string id, in bool tiled, ColorTransform transform = ColorTransform()) {
1596     if (id !is null) {
1597         if (id.endsWith(".xml") || id.endsWith(".XML")) {
1598             // XML drawables support
1599             auto d = new StateDrawable;
1600             if (!d.load(id)) {
1601                 Log.e("failed to load .xml drawable from ", id);
1602                 destroy(d);
1603                 return null;
1604             } else {
1605                 Log.d("loaded .xml drawable from ", id);
1606                 return d;
1607             }
1608         } else if (id.endsWith(".tim") || id.endsWith(".TIM")) {
1609             static if (WIDGET_STYLE_CONSOLE) {
1610                 try {
1611                     // .tim (text image) drawables support
1612                     string s = cast(string)loadResourceBytes(id);
1613                     if (s.length) {
1614                         auto d = new TextDrawable(s);
1615                         if (d.width && d.height) {
1616                             return d;
1617                         }
1618                     }
1619                 } catch (Exception e) {
1620                     // cannot find drawable file
1621                 }
1622             }
1623         } else if (id.startsWith("#")) {
1624             // color reference #AARRGGBB, e.g. #5599AA, a gradient, border description, etc.
1625             return createColorDrawable(id);
1626         } else if (id.startsWith("{")) {
1627             // json in {} with text drawable description
1628             static if (WIDGET_STYLE_CONSOLE) {
1629                return createTextDrawable(id);
1630             }
1631         } else {
1632             static if (BACKEND_GUI) {
1633                 // PNG/JPEG drawables support
1634                 DrawBufRef image = transform.empty ? imageCache.get(id) : imageCache.get(id, transform);
1635                 if (!image.isNull) {
1636                     bool ninePatch = id.endsWith(".9.png") ||  id.endsWith(".9.PNG");
1637                     return new ImageDrawable(image, tiled, ninePatch);
1638                 } else
1639                     Log.e("Failed to load image from ", id);
1640             }
1641         }
1642     }
1643     return null;
1644 }
1645 
1646 
1647 /// load text resource
1648 string loadTextResource(string resourceId) {
1649     string filename = drawableCache.findResource(resourceId);
1650     if (!filename) {
1651         Log.e("Object resource file not found for resourceId ", resourceId);
1652         assert(false);
1653     }
1654     string s = cast(string)loadResourceBytes(filename);
1655     if (!s) {
1656         Log.e("Cannot read text resource ", resourceId, " from file ", filename);
1657         assert(false);
1658     }
1659     return s;
1660 }