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     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     return attrValue(item.attrs, attrname, attrname2);
989 }
990 
991 string attrValue(AttributeSet attr, string attrname, string attrname2 = null) {
992     if (attr.get(attrname) !is null) // TODO_GRIM: Add support of in to arsd.dom?
993         return attr.get(attrname);
994     if (attrname2 !is null && attr.get(attrname2) !is null)
995         return attr.get(attrname2);
996     return null;
997 }
998 
999 void extractStateFlag(ref AttributeSet attr, string attrName, string attrName2, State state, ref uint stateMask, ref uint stateValue) {
1000     string value = attrValue(attr, attrName, attrName2);
1001     if (value !is null) {
1002         if (value.equal("true"))
1003             stateValue |= state;
1004         stateMask |= state;
1005     }
1006 }
1007 
1008 /// converts XML attribute name to State (see http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList)
1009 void extractStateFlags(AttributeSet attr, ref uint stateMask, ref uint stateValue) {
1010     extractStateFlag(attr, "state_pressed", "android:state_pressed", State.Pressed, stateMask, stateValue);
1011     extractStateFlag(attr, "state_focused", "android:state_focused", State.Focused, stateMask, stateValue);
1012     extractStateFlag(attr, "state_default", "android:state_default", State.Default, stateMask, stateValue);
1013     extractStateFlag(attr, "state_hovered", "android:state_hovered", State.Hovered, stateMask, stateValue);
1014     extractStateFlag(attr, "state_selected", "android:state_selected", State.Selected, stateMask, stateValue);
1015     extractStateFlag(attr, "state_checkable", "android:state_checkable", State.Checkable, stateMask, stateValue);
1016     extractStateFlag(attr, "state_checked", "android:state_checked", State.Checked, stateMask, stateValue);
1017     extractStateFlag(attr, "state_enabled", "android:state_enabled", State.Enabled, stateMask, stateValue);
1018     extractStateFlag(attr, "state_activated", "android:state_activated", State.Activated, stateMask, stateValue);
1019     extractStateFlag(attr, "state_window_focused", "android:state_window_focused", State.WindowFocused, stateMask, stateValue);
1020 }
1021 
1022 /*
1023 sample:
1024 (prefix android: is optional)
1025 
1026 <?xml version="1.0" encoding="utf-8"?>
1027 <selector xmlns:android="http://schemas.android.com/apk/res/android"
1028 android:constantSize=["true" | "false"]
1029 android:dither=["true" | "false"]
1030 android:variablePadding=["true" | "false"] >
1031 <item
1032 android:drawable="@[package:]drawable/drawable_resource"
1033 android:state_pressed=["true" | "false"]
1034 android:state_focused=["true" | "false"]
1035 android:state_hovered=["true" | "false"]
1036 android:state_selected=["true" | "false"]
1037 android:state_checkable=["true" | "false"]
1038 android:state_checked=["true" | "false"]
1039 android:state_enabled=["true" | "false"]
1040 android:state_activated=["true" | "false"]
1041 android:state_window_focused=["true" | "false"] />
1042 </selector>
1043 */
1044 
1045 /// Drawable which is drawn depending on state (see http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList)
1046 class StateDrawable : Drawable {
1047 
1048     static class StateItem {
1049         uint stateMask;
1050         uint stateValue;
1051         ColorTransform transform;
1052         DrawableRef drawable;
1053         @property bool matchState(uint state) {
1054             return (stateMask & state) == stateValue;
1055         }
1056     }
1057     // list of states
1058     protected StateItem[] _stateList;
1059     // max paddings for all states
1060     protected Rect _paddings;
1061     // max drawable size for all states
1062     protected Point _size;
1063 
1064     ~this() {
1065         foreach(ref item; _stateList)
1066             destroy(item);
1067         _stateList = null;
1068     }
1069 
1070     void addState(uint stateMask, uint stateValue, string resourceId, ref ColorTransform transform) {
1071         StateItem item = new StateItem();
1072         item.stateMask = stateMask;
1073         item.stateValue = stateValue;
1074         item.drawable = drawableCache.get(resourceId, transform);
1075         itemAdded(item);
1076     }
1077 
1078     void addState(uint stateMask, uint stateValue, DrawableRef drawable) {
1079         StateItem item = new StateItem();
1080         item.stateMask = stateMask;
1081         item.stateValue = stateValue;
1082         item.drawable = drawable;
1083         itemAdded(item);
1084     }
1085 
1086     private void itemAdded(StateItem item) {
1087         _stateList ~= item;
1088         if (!item.drawable.isNull) {
1089             if (_size.x < item.drawable.width)
1090                 _size.x = item.drawable.width;
1091             if (_size.y < item.drawable.height)
1092                 _size.y = item.drawable.height;
1093             _paddings.setMax(item.drawable.padding);
1094         }
1095     }
1096 
1097     /// parse 4 comma delimited integers
1098     static bool parseList4(T)(string value, ref T[4] items) {
1099         int index = 0;
1100         int p = 0;
1101         int start = 0;
1102         for (;p < value.length && index < 4; p++) {
1103             while (p < value.length && value[p] != ',')
1104                 p++;
1105             if (p > start) {
1106                 int end = p;
1107                 string s = value[start .. end];
1108                 items[index++] = to!T(s);
1109                 start = p + 1;
1110             }
1111         }
1112         return index == 4;
1113     }
1114     private static uint colorTransformFromStringAdd(string value) {
1115         if (value is null)
1116             return COLOR_TRANSFORM_OFFSET_NONE;
1117         int [4]n;
1118         if (!parseList4(value, n))
1119             return COLOR_TRANSFORM_OFFSET_NONE;
1120         foreach (ref item; n) {
1121             item = item / 2 + 0x80;
1122             if (item < 0)
1123                 item = 0;
1124             if (item > 0xFF)
1125                 item = 0xFF;
1126         }
1127         return (n[0] << 24) | (n[1] << 16) | (n[2] << 8) | (n[3] << 0);
1128     }
1129     private static uint colorTransformFromStringMult(string value) {
1130         if (value is null)
1131             return COLOR_TRANSFORM_MULTIPLY_NONE;
1132         float[4] n;
1133         uint[4] nn;
1134         if (!parseList4!float(value, n))
1135             return COLOR_TRANSFORM_MULTIPLY_NONE;
1136         foreach(i; 0 .. 4) {
1137             int res = cast(int)(n[i] * 0x40);
1138             if (res < 0)
1139                 res = 0;
1140             if (res > 0xFF)
1141                 res = 0xFF;
1142             nn[i] = res;
1143         }
1144         return (nn[0] << 24) | (nn[1] << 16) | (nn[2] << 8) | (nn[3] << 0);
1145     }
1146 
1147     bool load(XmlDocument document) {
1148         foreach(item; document.root.children) {
1149             if (item.tagName.equal("item")) {
1150                 string drawableId = attrValue(item, "drawable", "android:drawable");
1151                 if (drawableId.startsWith("@drawable/"))
1152                     drawableId = drawableId[10 .. $];
1153                 ColorTransform transform;
1154                 transform.addBefore = colorTransformFromStringAdd(attrValue(item, "color_transform_add1", "android:transform_color_add1"));
1155                 transform.multiply = colorTransformFromStringMult(attrValue(item, "color_transform_mul", "android:transform_color_mul"));
1156                 transform.addAfter = colorTransformFromStringAdd(attrValue(item, "color_transform_add2", "android:transform_color_add2"));
1157                 if (drawableId !is null) {
1158                     uint stateMask, stateValue;
1159                     extractStateFlags(item.attrs, stateMask, stateValue);
1160                     if (drawableId !is null) {
1161                         addState(stateMask, stateValue, drawableId, transform);
1162                     }
1163                 }
1164             }
1165         }
1166         return _stateList.length > 0;
1167     }
1168 
1169     /// load from XML file
1170     bool load(string filename) {
1171         try {
1172             string s = cast(string)loadResourceBytes(filename);
1173             if (!s) {
1174                 Log.e("Cannot read drawable resource from file ", filename);
1175                 return false;
1176             }
1177 
1178             // Check for well-formedness
1179             //check(s);
1180 
1181             // Make a DOM tree
1182             auto doc = new XmlDocument(s);
1183 
1184             return load(doc);
1185         } catch (Exception e) {
1186             Log.e("Invalid XML file ", filename);
1187             return false;
1188         }
1189     }
1190 
1191     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
1192         foreach(ref item; _stateList)
1193             if (item.matchState(state)) {
1194                 if (!item.drawable.isNull) {
1195                     item.drawable.drawTo(buf, rc, state, tilex0, tiley0);
1196                 }
1197                 return;
1198             }
1199     }
1200 
1201     @property override int width() {
1202         return _size.x;
1203     }
1204     @property override int height() {
1205         return _size.y;
1206     }
1207     @property override Rect padding() {
1208         return _paddings;
1209     }
1210 }
1211 
1212 /// Drawable which allows to combine together background image, gradient, borders, box shadows, etc.
1213 class CombinedDrawable : Drawable {
1214 
1215     DrawableRef boxShadow;
1216     DrawableRef background;
1217     DrawableRef border;
1218 
1219     this(uint backgroundColor, string backgroundImageId, string borderDescription, string boxShadowDescription) {
1220         boxShadow = boxShadowDescription !is null ? drawableCache.get("#box-shadow," ~ boxShadowDescription) : new EmptyDrawable;
1221         background =
1222             (backgroundImageId !is null) ? drawableCache.get(backgroundImageId) :
1223             (!backgroundColor.isFullyTransparentColor) ? new SolidFillDrawable(backgroundColor) : null;
1224         if (background is null)
1225             background = new EmptyDrawable;
1226         border = borderDescription !is null ? drawableCache.get("#border," ~ borderDescription) : new EmptyDrawable;
1227     }
1228 
1229     override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
1230         boxShadow.drawTo(buf, rc, state, tilex0, tiley0);
1231         // make background image smaller to fit borders
1232         Rect backrc = rc;
1233         backrc.left += border.padding.left;
1234         backrc.top += border.padding.top;
1235         backrc.right -= border.padding.right;
1236         backrc.bottom -= border.padding.bottom;
1237         background.drawTo(buf, backrc, state, tilex0, tiley0);
1238         border.drawTo(buf, rc, state, tilex0, tiley0);
1239     }
1240 
1241     @property override int width() { return background.width + border.padding.left + border.padding.right; }
1242     @property override int height() { return background.height + border.padding.top + border.padding.bottom; }
1243     @property override Rect padding() {
1244         return Rect(background.padding.left + border.padding.left, background.padding.top + border.padding.top,
1245             background.padding.right + border.padding.right, background.padding.bottom + border.padding.bottom);
1246     }
1247 }
1248 
1249 
1250 alias DrawableRef = Ref!Drawable;
1251 
1252 
1253 
1254 
1255 static if (BACKEND_GUI) {
1256 /// decoded raster images cache (png, jpeg) -- access by filenames
1257 class ImageCache {
1258 
1259     static class ImageCacheItem {
1260         string _filename;
1261         DrawBufRef _drawbuf;
1262         DrawBufRef[ColorTransform] _transformMap;
1263 
1264         bool _error; // flag to avoid loading of file if it has been failed once
1265         bool _used;
1266 
1267         this(string filename) {
1268             _filename = filename;
1269         }
1270 
1271         /// get normal image
1272         @property ref DrawBufRef get() {
1273             if (!_drawbuf.isNull || _error) {
1274                 _used = true;
1275                 return _drawbuf;
1276             }
1277             immutable ubyte[] data = loadResourceBytes(_filename);
1278             if (data) {
1279                 _drawbuf = loadImage(data, _filename);
1280                 if (_filename.endsWith(".9.png"))
1281                     _drawbuf.detectNinePatch();
1282                 _used = true;
1283             }
1284             if (_drawbuf.isNull)
1285                 _error = true;
1286             return _drawbuf;
1287         }
1288         /// get color transformed image
1289         @property ref DrawBufRef get(ref ColorTransform transform) {
1290             if (transform.empty)
1291                 return get();
1292             if (transform in _transformMap)
1293                 return _transformMap[transform];
1294             DrawBufRef src = get();
1295             if (src.isNull)
1296                 _transformMap[transform] = src;
1297             else {
1298                 DrawBufRef t = src.transformColors(transform);
1299                 _transformMap[transform] = t;
1300             }
1301             return _transformMap[transform];
1302         }
1303 
1304         /// remove from memory, will cause reload on next access
1305         void compact() {
1306             if (!_drawbuf.isNull)
1307                 _drawbuf.clear();
1308         }
1309         /// mark as not used
1310         void checkpoint() {
1311             _used = false;
1312         }
1313         /// cleanup if unused since last checkpoint
1314         void cleanup() {
1315             if (!_used)
1316                 compact();
1317         }
1318     }
1319     ImageCacheItem[string] _map;
1320 
1321     /// get and cache image
1322     ref DrawBufRef get(string filename) {
1323         if (filename in _map) {
1324             return _map[filename].get;
1325         }
1326         ImageCacheItem item = new ImageCacheItem(filename);
1327         _map[filename] = item;
1328         return item.get;
1329     }
1330 
1331     /// get and cache color transformed image
1332     ref DrawBufRef get(string filename, ref ColorTransform transform) {
1333         if (transform.empty)
1334             return get(filename);
1335         if (filename in _map) {
1336             return _map[filename].get(transform);
1337         }
1338         ImageCacheItem item = new ImageCacheItem(filename);
1339         _map[filename] = item;
1340         return item.get(transform);
1341     }
1342     // clear usage flags for all entries
1343     void checkpoint() {
1344         foreach (item; _map)
1345             item.checkpoint();
1346     }
1347     // removes entries not used after last call of checkpoint() or cleanup()
1348     void cleanup() {
1349         foreach (item; _map)
1350             item.cleanup();
1351     }
1352 
1353     this() {
1354         debug Log.i("Creating ImageCache");
1355     }
1356     ~this() {
1357         debug Log.i("Destroying ImageCache");
1358         foreach (ref item; _map) {
1359             destroy(item);
1360             item = null;
1361         }
1362         _map.destroy();
1363     }
1364 }
1365 
1366 __gshared ImageCache _imageCache;
1367 /// image cache singleton
1368 @property ImageCache imageCache() { return _imageCache; }
1369 /// image cache singleton
1370 @property void imageCache(ImageCache cache) {
1371     if (_imageCache !is null)
1372         destroy(_imageCache);
1373     _imageCache = cache;
1374 }
1375 }
1376 
1377 __gshared DrawableCache _drawableCache;
1378 /// drawable cache singleton
1379 @property DrawableCache drawableCache() { return _drawableCache; }
1380 /// drawable cache singleton
1381 @property void drawableCache(DrawableCache cache) {
1382     if (_drawableCache !is null)
1383         destroy(_drawableCache);
1384     _drawableCache = cache;
1385 }
1386 
1387 class DrawableCache {
1388     static class DrawableCacheItem {
1389         string _id;
1390         string _filename;
1391         bool _tiled;
1392         DrawableRef _drawable;
1393         DrawableRef[ColorTransform] _transformed;
1394 
1395         bool _error; // flag to avoid loading of file if it has been failed once
1396         bool _used;
1397 
1398         this(string id, string filename, bool tiled) {
1399             _id = id;
1400             _filename = filename;
1401             _tiled = tiled;
1402             _error = filename is null;
1403             debug ++_instanceCount;
1404             debug(resalloc) Log.d("Created DrawableCacheItem, count=", _instanceCount);
1405         }
1406         debug private static __gshared int _instanceCount;
1407         debug @property static int instanceCount() { return _instanceCount; }
1408 
1409         ~this() {
1410             _drawable.clear();
1411             foreach(ref t; _transformed)
1412                 t.clear();
1413             _transformed.destroy();
1414             debug --_instanceCount;
1415             debug(resalloc) Log.d("Destroyed DrawableCacheItem, count=", _instanceCount);
1416         }
1417         /// remove from memory, will cause reload on next access
1418         void compact() {
1419             if (!_drawable.isNull)
1420                 _drawable.clear();
1421             foreach(t; _transformed)
1422                 t.clear();
1423             _transformed.destroy();
1424         }
1425         /// mark as not used
1426         void checkpoint() {
1427             _used = false;
1428         }
1429         /// cleanup if unused since last checkpoint
1430         void cleanup() {
1431             if (!_used)
1432                 compact();
1433         }
1434 
1435         /// returns drawable (loads from file if necessary)
1436         @property ref DrawableRef drawable(in ColorTransform transform = ColorTransform()) {
1437             _used = true;
1438             if (!transform.empty && transform in _transformed)
1439                 return _transformed[transform];
1440             if (!_drawable.isNull || _error)
1441                 return _drawable;
1442 
1443             // not in cache - create it
1444             Drawable dr = makeDrawableFromId(_filename, _tiled, transform);
1445             _error = dr is null;
1446             if (transform.empty) {
1447                 _drawable = dr;
1448                 return _drawable;
1449             } else {
1450                 _transformed[transform] = dr;
1451                 return _transformed[transform];
1452             }
1453         }
1454     }
1455 
1456     void clear() {
1457         Log.d("DrawableCache.clear()");
1458         _idToFileMap.destroy();
1459         foreach(DrawableCacheItem item; _idToDrawableMap)
1460             item.drawable.clear();
1461         _idToDrawableMap.destroy();
1462     }
1463     // clear usage flags for all entries
1464     void checkpoint() {
1465         foreach (item; _idToDrawableMap)
1466             item.checkpoint();
1467     }
1468     // removes entries not used after last call of checkpoint() or cleanup()
1469     void cleanup() {
1470         foreach (item; _idToDrawableMap)
1471             item.cleanup();
1472     }
1473 
1474     string[] _resourcePaths;
1475     string[string] _idToFileMap;
1476     DrawableCacheItem[string] _idToDrawableMap;
1477     DrawableRef _nullDrawable;
1478 
1479     ref DrawableRef get(string id, in ColorTransform transform = ColorTransform()) {
1480         id = id.strip;
1481         if (id.equal("@null"))
1482             return _nullDrawable;
1483         if (id in _idToDrawableMap)
1484             return _idToDrawableMap[id].drawable(transform);
1485         // not found - create it
1486         string resourceId = id;
1487         bool tiled = false;
1488         if (id.endsWith(".tiled")) {
1489             resourceId = id[0..$-6]; // remove .tiled
1490             tiled = true;
1491         }
1492         string filename = findResource(resourceId);
1493         auto item = new DrawableCacheItem(id, filename, tiled);
1494         _idToDrawableMap[id] = item;
1495         return item.drawable(transform);
1496     }
1497 
1498     @property string[] resourcePaths() {
1499         return _resourcePaths;
1500     }
1501     /// set resource directory paths as variable number of parameters
1502     void setResourcePaths(string[] paths ...) {
1503         resourcePaths(paths);
1504     }
1505     /// set resource directory paths array (only existing dirs will be added)
1506     @property void resourcePaths(string[] paths) {
1507         string[] existingPaths;
1508         foreach(path; paths) {
1509             if (exists(path) && isDir(path)) {
1510                 existingPaths ~= path;
1511                 Log.d("DrawableCache: adding path ", path, " to resource dir list.");
1512             } else {
1513                 Log.d("DrawableCache: path ", path, " does not exist.");
1514             }
1515         }
1516         _resourcePaths = existingPaths;
1517         clear();
1518     }
1519 
1520     /// concatenates path with resource id and extension, returns pathname if there is such file, null if file does not exist
1521     private string checkFileName(string path, string id, string extension) {
1522         char[] fn = path.dup;
1523         fn ~= id;
1524         fn ~= extension;
1525         if (exists(fn) && isFile(fn))
1526             return fn.dup;
1527         return null;
1528     }
1529 
1530     /// get resource file full pathname by resource id, null if not found
1531     string findResource(string id) {
1532         if (id.startsWith("#") || id.startsWith("{"))
1533             return id; // it's not a file name
1534         if (id in _idToFileMap)
1535             return _idToFileMap[id];
1536         EmbeddedResource * embedded = embeddedResourceList.findAutoExtension(id);
1537         if (embedded) {
1538             string fn = EMBEDDED_RESOURCE_PREFIX ~ embedded.name;
1539             _idToFileMap[id] = fn;
1540             return fn;
1541         }
1542         foreach(string path; _resourcePaths) {
1543             string fn;
1544             fn = checkFileName(path, id, ".xml");
1545             if (fn is null && WIDGET_STYLE_CONSOLE)
1546                 fn = checkFileName(path, id, ".tim");
1547             if (fn is null)
1548                 fn = checkFileName(path, id, ".png");
1549             if (fn is null)
1550                 fn = checkFileName(path, id, ".9.png");
1551             if (fn is null)
1552                 fn = checkFileName(path, id, ".jpg");
1553             if (fn !is null) {
1554                 _idToFileMap[id] = fn;
1555                 return fn;
1556             }
1557         }
1558         Log.w("resource ", id, " is not found");
1559         return null;
1560     }
1561     static if (BACKEND_GUI) {
1562         /// get image (DrawBuf) from imageCache by resource id
1563         DrawBufRef getImage(string id) {
1564             DrawBufRef res;
1565             string fname = findResource(id);
1566             if (fname.endsWith(".png") || fname.endsWith(".jpg"))
1567                 return imageCache.get(fname);
1568             return res;
1569         }
1570     }
1571     this() {
1572         debug Log.i("Creating DrawableCache");
1573     }
1574     ~this() {
1575         debug(resalloc) Log.e("Drawable instace count before destroying of DrawableCache: ", ImageDrawable.instanceCount);
1576 
1577         //Log.i("Destroying DrawableCache _idToDrawableMap.length=", _idToDrawableMap.length);
1578         Log.i("Destroying DrawableCache");
1579         foreach (ref item; _idToDrawableMap) {
1580             destroy(item);
1581             item = null;
1582         }
1583         _idToDrawableMap.destroy();
1584         debug if(ImageDrawable.instanceCount) Log.e("Drawable instace count after destroying of DrawableCache: ", ImageDrawable.instanceCount);
1585     }
1586 }
1587 
1588 
1589 /// This function takes an id and creates a drawable
1590 /// id may be a name of file, #directive, color or json
1591 private Drawable makeDrawableFromId(in string id, in bool tiled, ColorTransform transform = ColorTransform()) {
1592     if (id !is null) {
1593         if (id.endsWith(".xml") || id.endsWith(".XML")) {
1594             // XML drawables support
1595             auto d = new StateDrawable;
1596             if (!d.load(id)) {
1597                 Log.e("failed to load .xml drawable from ", id);
1598                 destroy(d);
1599                 return null;
1600             } else {
1601                 Log.d("loaded .xml drawable from ", id);
1602                 return d;
1603             }
1604         } else if (id.endsWith(".tim") || id.endsWith(".TIM")) {
1605             static if (WIDGET_STYLE_CONSOLE) {
1606                 try {
1607                     // .tim (text image) drawables support
1608                     string s = cast(string)loadResourceBytes(id);
1609                     if (s.length) {
1610                         auto d = new TextDrawable(s);
1611                         if (d.width && d.height) {
1612                             return d;
1613                         }
1614                     }
1615                 } catch (Exception e) {
1616                     // cannot find drawable file
1617                 }
1618             }
1619         } else if (id.startsWith("#")) {
1620             // color reference #AARRGGBB, e.g. #5599AA, a gradient, border description, etc.
1621             return createColorDrawable(id);
1622         } else if (id.startsWith("{")) {
1623             // json in {} with text drawable description
1624             static if (WIDGET_STYLE_CONSOLE) {
1625                return createTextDrawable(id);
1626             }
1627         } else {
1628             static if (BACKEND_GUI) {
1629                 // PNG/JPEG drawables support
1630                 DrawBufRef image = transform.empty ? imageCache.get(id) : imageCache.get(id, transform);
1631                 if (!image.isNull) {
1632                     bool ninePatch = id.endsWith(".9.png") ||  id.endsWith(".9.PNG");
1633                     return new ImageDrawable(image, tiled, ninePatch);
1634                 } else
1635                     Log.e("Failed to load image from ", id);
1636             }
1637         }
1638     }
1639     return null;
1640 }
1641 
1642 
1643 /// load text resource
1644 string loadTextResource(string resourceId) {
1645     string filename = drawableCache.findResource(resourceId);
1646     if (!filename) {
1647         Log.e("Object resource file not found for resourceId ", resourceId);
1648         assert(false);
1649     }
1650     string s = cast(string)loadResourceBytes(filename);
1651     if (!s) {
1652         Log.e("Cannot read text resource ", resourceId, " from file ", filename);
1653         assert(false);
1654     }
1655     return s;
1656 }