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