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