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