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