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