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