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