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 undead.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 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 if (attrname in item.tag.attr) 989 return item.tag.attr[attrname]; 990 if (attrname2 && attrname2 in item.tag.attr) 991 return item.tag.attr[attrname2]; 992 return null; 993 } 994 995 string attrValue(ref string[string] attr, string attrname, string attrname2 = null) { 996 if (attrname in attr) 997 return attr[attrname]; 998 if (attrname2 && attrname2 in attr) 999 return attr[attrname2]; 1000 return null; 1001 } 1002 1003 void extractStateFlag(ref string[string] attr, string attrName, string attrName2, State state, ref uint stateMask, ref uint stateValue) { 1004 string value = attrValue(attr, attrName, attrName2); 1005 if (value !is null) { 1006 if (value.equal("true")) 1007 stateValue |= state; 1008 stateMask |= state; 1009 } 1010 } 1011 1012 /// converts XML attribute name to State (see http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList) 1013 void extractStateFlags(ref string[string] attr, ref uint stateMask, ref uint stateValue) { 1014 extractStateFlag(attr, "state_pressed", "android:state_pressed", State.Pressed, stateMask, stateValue); 1015 extractStateFlag(attr, "state_focused", "android:state_focused", State.Focused, stateMask, stateValue); 1016 extractStateFlag(attr, "state_default", "android:state_default", State.Default, stateMask, stateValue); 1017 extractStateFlag(attr, "state_hovered", "android:state_hovered", State.Hovered, stateMask, stateValue); 1018 extractStateFlag(attr, "state_selected", "android:state_selected", State.Selected, stateMask, stateValue); 1019 extractStateFlag(attr, "state_checkable", "android:state_checkable", State.Checkable, stateMask, stateValue); 1020 extractStateFlag(attr, "state_checked", "android:state_checked", State.Checked, stateMask, stateValue); 1021 extractStateFlag(attr, "state_enabled", "android:state_enabled", State.Enabled, stateMask, stateValue); 1022 extractStateFlag(attr, "state_activated", "android:state_activated", State.Activated, stateMask, stateValue); 1023 extractStateFlag(attr, "state_window_focused", "android:state_window_focused", State.WindowFocused, stateMask, stateValue); 1024 } 1025 1026 /* 1027 sample: 1028 (prefix android: is optional) 1029 1030 <?xml version="1.0" encoding="utf-8"?> 1031 <selector xmlns:android="http://schemas.android.com/apk/res/android" 1032 android:constantSize=["true" | "false"] 1033 android:dither=["true" | "false"] 1034 android:variablePadding=["true" | "false"] > 1035 <item 1036 android:drawable="@[package:]drawable/drawable_resource" 1037 android:state_pressed=["true" | "false"] 1038 android:state_focused=["true" | "false"] 1039 android:state_hovered=["true" | "false"] 1040 android:state_selected=["true" | "false"] 1041 android:state_checkable=["true" | "false"] 1042 android:state_checked=["true" | "false"] 1043 android:state_enabled=["true" | "false"] 1044 android:state_activated=["true" | "false"] 1045 android:state_window_focused=["true" | "false"] /> 1046 </selector> 1047 */ 1048 1049 /// Drawable which is drawn depending on state (see http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList) 1050 class StateDrawable : Drawable { 1051 1052 static class StateItem { 1053 uint stateMask; 1054 uint stateValue; 1055 ColorTransform transform; 1056 DrawableRef drawable; 1057 @property bool matchState(uint state) { 1058 return (stateMask & state) == stateValue; 1059 } 1060 } 1061 // list of states 1062 protected StateItem[] _stateList; 1063 // max paddings for all states 1064 protected Rect _paddings; 1065 // max drawable size for all states 1066 protected Point _size; 1067 1068 ~this() { 1069 foreach(ref item; _stateList) 1070 destroy(item); 1071 _stateList = null; 1072 } 1073 1074 void addState(uint stateMask, uint stateValue, string resourceId, ref ColorTransform transform) { 1075 StateItem item = new StateItem(); 1076 item.stateMask = stateMask; 1077 item.stateValue = stateValue; 1078 item.drawable = drawableCache.get(resourceId, transform); 1079 itemAdded(item); 1080 } 1081 1082 void addState(uint stateMask, uint stateValue, DrawableRef drawable) { 1083 StateItem item = new StateItem(); 1084 item.stateMask = stateMask; 1085 item.stateValue = stateValue; 1086 item.drawable = drawable; 1087 itemAdded(item); 1088 } 1089 1090 private void itemAdded(StateItem item) { 1091 _stateList ~= item; 1092 if (!item.drawable.isNull) { 1093 if (_size.x < item.drawable.width) 1094 _size.x = item.drawable.width; 1095 if (_size.y < item.drawable.height) 1096 _size.y = item.drawable.height; 1097 _paddings.setMax(item.drawable.padding); 1098 } 1099 } 1100 1101 /// parse 4 comma delimited integers 1102 static bool parseList4(T)(string value, ref T[4] items) { 1103 int index = 0; 1104 int p = 0; 1105 int start = 0; 1106 for (;p < value.length && index < 4; p++) { 1107 while (p < value.length && value[p] != ',') 1108 p++; 1109 if (p > start) { 1110 int end = p; 1111 string s = value[start .. end]; 1112 items[index++] = to!T(s); 1113 start = p + 1; 1114 } 1115 } 1116 return index == 4; 1117 } 1118 private static uint colorTransformFromStringAdd(string value) { 1119 if (value is null) 1120 return COLOR_TRANSFORM_OFFSET_NONE; 1121 int [4]n; 1122 if (!parseList4(value, n)) 1123 return COLOR_TRANSFORM_OFFSET_NONE; 1124 foreach (ref item; n) { 1125 item = item / 2 + 0x80; 1126 if (item < 0) 1127 item = 0; 1128 if (item > 0xFF) 1129 item = 0xFF; 1130 } 1131 return (n[0] << 24) | (n[1] << 16) | (n[2] << 8) | (n[3] << 0); 1132 } 1133 private static uint colorTransformFromStringMult(string value) { 1134 if (value is null) 1135 return COLOR_TRANSFORM_MULTIPLY_NONE; 1136 float[4] n; 1137 uint[4] nn; 1138 if (!parseList4!float(value, n)) 1139 return COLOR_TRANSFORM_MULTIPLY_NONE; 1140 foreach(i; 0 .. 4) { 1141 int res = cast(int)(n[i] * 0x40); 1142 if (res < 0) 1143 res = 0; 1144 if (res > 0xFF) 1145 res = 0xFF; 1146 nn[i] = res; 1147 } 1148 return (nn[0] << 24) | (nn[1] << 16) | (nn[2] << 8) | (nn[3] << 0); 1149 } 1150 1151 bool load(Element element) { 1152 foreach(item; element.elements) { 1153 if (item.tag.name.equal("item")) { 1154 string drawableId = attrValue(item, "drawable", "android:drawable"); 1155 if (drawableId.startsWith("@drawable/")) 1156 drawableId = drawableId[10 .. $]; 1157 ColorTransform transform; 1158 transform.addBefore = colorTransformFromStringAdd(attrValue(item, "color_transform_add1", "android:transform_color_add1")); 1159 transform.multiply = colorTransformFromStringMult(attrValue(item, "color_transform_mul", "android:transform_color_mul")); 1160 transform.addAfter = colorTransformFromStringAdd(attrValue(item, "color_transform_add2", "android:transform_color_add2")); 1161 if (drawableId !is null) { 1162 uint stateMask, stateValue; 1163 extractStateFlags(item.tag.attr, stateMask, stateValue); 1164 if (drawableId !is null) { 1165 addState(stateMask, stateValue, drawableId, transform); 1166 } 1167 } 1168 } 1169 } 1170 return _stateList.length > 0; 1171 } 1172 1173 /// load from XML file 1174 bool load(string filename) { 1175 try { 1176 string s = cast(string)loadResourceBytes(filename); 1177 if (!s) { 1178 Log.e("Cannot read drawable resource from file ", filename); 1179 return false; 1180 } 1181 1182 // Check for well-formedness 1183 //check(s); 1184 1185 // Make a DOM tree 1186 auto doc = new Document(s); 1187 1188 return load(doc); 1189 } catch (CheckException e) { 1190 Log.e("Invalid XML file ", filename); 1191 return false; 1192 } 1193 } 1194 1195 override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) { 1196 foreach(ref item; _stateList) 1197 if (item.matchState(state)) { 1198 if (!item.drawable.isNull) { 1199 item.drawable.drawTo(buf, rc, state, tilex0, tiley0); 1200 } 1201 return; 1202 } 1203 } 1204 1205 @property override int width() { 1206 return _size.x; 1207 } 1208 @property override int height() { 1209 return _size.y; 1210 } 1211 @property override Rect padding() { 1212 return _paddings; 1213 } 1214 } 1215 1216 /// Drawable which allows to combine together background image, gradient, borders, box shadows, etc. 1217 class CombinedDrawable : Drawable { 1218 1219 DrawableRef boxShadow; 1220 DrawableRef background; 1221 DrawableRef border; 1222 1223 this(uint backgroundColor, string backgroundImageId, string borderDescription, string boxShadowDescription) { 1224 boxShadow = boxShadowDescription !is null ? drawableCache.get("#box-shadow," ~ boxShadowDescription) : new EmptyDrawable; 1225 background = 1226 (backgroundImageId !is null) ? drawableCache.get(backgroundImageId) : 1227 (!backgroundColor.isFullyTransparentColor) ? new SolidFillDrawable(backgroundColor) : null; 1228 if (background is null) 1229 background = new EmptyDrawable; 1230 border = borderDescription !is null ? drawableCache.get("#border," ~ borderDescription) : new EmptyDrawable; 1231 } 1232 1233 override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) { 1234 boxShadow.drawTo(buf, rc, state, tilex0, tiley0); 1235 // make background image smaller to fit borders 1236 Rect backrc = rc; 1237 backrc.left += border.padding.left; 1238 backrc.top += border.padding.top; 1239 backrc.right -= border.padding.right; 1240 backrc.bottom -= border.padding.bottom; 1241 background.drawTo(buf, backrc, state, tilex0, tiley0); 1242 border.drawTo(buf, rc, state, tilex0, tiley0); 1243 } 1244 1245 @property override int width() { return background.width + border.padding.left + border.padding.right; } 1246 @property override int height() { return background.height + border.padding.top + border.padding.bottom; } 1247 @property override Rect padding() { 1248 return Rect(background.padding.left + border.padding.left, background.padding.top + border.padding.top, 1249 background.padding.right + border.padding.right, background.padding.bottom + border.padding.bottom); 1250 } 1251 } 1252 1253 1254 alias DrawableRef = Ref!Drawable; 1255 1256 1257 1258 1259 static if (BACKEND_GUI) { 1260 /// decoded raster images cache (png, jpeg) -- access by filenames 1261 class ImageCache { 1262 1263 static class ImageCacheItem { 1264 string _filename; 1265 DrawBufRef _drawbuf; 1266 DrawBufRef[ColorTransform] _transformMap; 1267 1268 bool _error; // flag to avoid loading of file if it has been failed once 1269 bool _used; 1270 1271 this(string filename) { 1272 _filename = filename; 1273 } 1274 1275 /// get normal image 1276 @property ref DrawBufRef get() { 1277 if (!_drawbuf.isNull || _error) { 1278 _used = true; 1279 return _drawbuf; 1280 } 1281 immutable ubyte[] data = loadResourceBytes(_filename); 1282 if (data) { 1283 _drawbuf = loadImage(data, _filename); 1284 if (_filename.endsWith(".9.png")) 1285 _drawbuf.detectNinePatch(); 1286 _used = true; 1287 } 1288 if (_drawbuf.isNull) 1289 _error = true; 1290 return _drawbuf; 1291 } 1292 /// get color transformed image 1293 @property ref DrawBufRef get(ref ColorTransform transform) { 1294 if (transform.empty) 1295 return get(); 1296 if (transform in _transformMap) 1297 return _transformMap[transform]; 1298 DrawBufRef src = get(); 1299 if (src.isNull) 1300 _transformMap[transform] = src; 1301 else { 1302 DrawBufRef t = src.transformColors(transform); 1303 _transformMap[transform] = t; 1304 } 1305 return _transformMap[transform]; 1306 } 1307 1308 /// remove from memory, will cause reload on next access 1309 void compact() { 1310 if (!_drawbuf.isNull) 1311 _drawbuf.clear(); 1312 } 1313 /// mark as not used 1314 void checkpoint() { 1315 _used = false; 1316 } 1317 /// cleanup if unused since last checkpoint 1318 void cleanup() { 1319 if (!_used) 1320 compact(); 1321 } 1322 } 1323 ImageCacheItem[string] _map; 1324 1325 /// get and cache image 1326 ref DrawBufRef get(string filename) { 1327 if (filename in _map) { 1328 return _map[filename].get; 1329 } 1330 ImageCacheItem item = new ImageCacheItem(filename); 1331 _map[filename] = item; 1332 return item.get; 1333 } 1334 1335 /// get and cache color transformed image 1336 ref DrawBufRef get(string filename, ref ColorTransform transform) { 1337 if (transform.empty) 1338 return get(filename); 1339 if (filename in _map) { 1340 return _map[filename].get(transform); 1341 } 1342 ImageCacheItem item = new ImageCacheItem(filename); 1343 _map[filename] = item; 1344 return item.get(transform); 1345 } 1346 // clear usage flags for all entries 1347 void checkpoint() { 1348 foreach (item; _map) 1349 item.checkpoint(); 1350 } 1351 // removes entries not used after last call of checkpoint() or cleanup() 1352 void cleanup() { 1353 foreach (item; _map) 1354 item.cleanup(); 1355 } 1356 1357 this() { 1358 debug Log.i("Creating ImageCache"); 1359 } 1360 ~this() { 1361 debug Log.i("Destroying ImageCache"); 1362 foreach (ref item; _map) { 1363 destroy(item); 1364 item = null; 1365 } 1366 _map.destroy(); 1367 } 1368 } 1369 1370 __gshared ImageCache _imageCache; 1371 /// image cache singleton 1372 @property ImageCache imageCache() { return _imageCache; } 1373 /// image cache singleton 1374 @property void imageCache(ImageCache cache) { 1375 if (_imageCache !is null) 1376 destroy(_imageCache); 1377 _imageCache = cache; 1378 } 1379 } 1380 1381 __gshared DrawableCache _drawableCache; 1382 /// drawable cache singleton 1383 @property DrawableCache drawableCache() { return _drawableCache; } 1384 /// drawable cache singleton 1385 @property void drawableCache(DrawableCache cache) { 1386 if (_drawableCache !is null) 1387 destroy(_drawableCache); 1388 _drawableCache = cache; 1389 } 1390 1391 class DrawableCache { 1392 static class DrawableCacheItem { 1393 string _id; 1394 string _filename; 1395 bool _tiled; 1396 DrawableRef _drawable; 1397 DrawableRef[ColorTransform] _transformed; 1398 1399 bool _error; // flag to avoid loading of file if it has been failed once 1400 bool _used; 1401 1402 this(string id, string filename, bool tiled) { 1403 _id = id; 1404 _filename = filename; 1405 _tiled = tiled; 1406 _error = filename is null; 1407 debug ++_instanceCount; 1408 debug(resalloc) Log.d("Created DrawableCacheItem, count=", _instanceCount); 1409 } 1410 debug private static __gshared int _instanceCount; 1411 debug @property static int instanceCount() { return _instanceCount; } 1412 1413 ~this() { 1414 _drawable.clear(); 1415 foreach(ref t; _transformed) 1416 t.clear(); 1417 _transformed.destroy(); 1418 debug --_instanceCount; 1419 debug(resalloc) Log.d("Destroyed DrawableCacheItem, count=", _instanceCount); 1420 } 1421 /// remove from memory, will cause reload on next access 1422 void compact() { 1423 if (!_drawable.isNull) 1424 _drawable.clear(); 1425 foreach(t; _transformed) 1426 t.clear(); 1427 _transformed.destroy(); 1428 } 1429 /// mark as not used 1430 void checkpoint() { 1431 _used = false; 1432 } 1433 /// cleanup if unused since last checkpoint 1434 void cleanup() { 1435 if (!_used) 1436 compact(); 1437 } 1438 1439 /// returns drawable (loads from file if necessary) 1440 @property ref DrawableRef drawable(in ColorTransform transform = ColorTransform()) { 1441 _used = true; 1442 if (!transform.empty && transform in _transformed) 1443 return _transformed[transform]; 1444 if (!_drawable.isNull || _error) 1445 return _drawable; 1446 1447 // not in cache - create it 1448 Drawable dr = makeDrawableFromId(_filename, _tiled, transform); 1449 _error = dr is null; 1450 if (transform.empty) { 1451 _drawable = dr; 1452 return _drawable; 1453 } else { 1454 _transformed[transform] = dr; 1455 return _transformed[transform]; 1456 } 1457 } 1458 } 1459 1460 void clear() { 1461 Log.d("DrawableCache.clear()"); 1462 _idToFileMap.destroy(); 1463 foreach(DrawableCacheItem item; _idToDrawableMap) 1464 item.drawable.clear(); 1465 _idToDrawableMap.destroy(); 1466 } 1467 // clear usage flags for all entries 1468 void checkpoint() { 1469 foreach (item; _idToDrawableMap) 1470 item.checkpoint(); 1471 } 1472 // removes entries not used after last call of checkpoint() or cleanup() 1473 void cleanup() { 1474 foreach (item; _idToDrawableMap) 1475 item.cleanup(); 1476 } 1477 1478 string[] _resourcePaths; 1479 string[string] _idToFileMap; 1480 DrawableCacheItem[string] _idToDrawableMap; 1481 DrawableRef _nullDrawable; 1482 1483 ref DrawableRef get(string id, in ColorTransform transform = ColorTransform()) { 1484 id = id.strip; 1485 if (id.equal("@null")) 1486 return _nullDrawable; 1487 if (id in _idToDrawableMap) 1488 return _idToDrawableMap[id].drawable(transform); 1489 // not found - create it 1490 string resourceId = id; 1491 bool tiled = false; 1492 if (id.endsWith(".tiled")) { 1493 resourceId = id[0..$-6]; // remove .tiled 1494 tiled = true; 1495 } 1496 string filename = findResource(resourceId); 1497 auto item = new DrawableCacheItem(id, filename, tiled); 1498 _idToDrawableMap[id] = item; 1499 return item.drawable(transform); 1500 } 1501 1502 @property string[] resourcePaths() { 1503 return _resourcePaths; 1504 } 1505 /// set resource directory paths as variable number of parameters 1506 void setResourcePaths(string[] paths ...) { 1507 resourcePaths(paths); 1508 } 1509 /// set resource directory paths array (only existing dirs will be added) 1510 @property void resourcePaths(string[] paths) { 1511 string[] existingPaths; 1512 foreach(path; paths) { 1513 if (exists(path) && isDir(path)) { 1514 existingPaths ~= path; 1515 Log.d("DrawableCache: adding path ", path, " to resource dir list."); 1516 } else { 1517 Log.d("DrawableCache: path ", path, " does not exist."); 1518 } 1519 } 1520 _resourcePaths = existingPaths; 1521 clear(); 1522 } 1523 1524 /// concatenates path with resource id and extension, returns pathname if there is such file, null if file does not exist 1525 private string checkFileName(string path, string id, string extension) { 1526 char[] fn = path.dup; 1527 fn ~= id; 1528 fn ~= extension; 1529 if (exists(fn) && isFile(fn)) 1530 return fn.dup; 1531 return null; 1532 } 1533 1534 /// get resource file full pathname by resource id, null if not found 1535 string findResource(string id) { 1536 if (id.startsWith("#") || id.startsWith("{")) 1537 return id; // it's not a file name 1538 if (id in _idToFileMap) 1539 return _idToFileMap[id]; 1540 EmbeddedResource * embedded = embeddedResourceList.findAutoExtension(id); 1541 if (embedded) { 1542 string fn = EMBEDDED_RESOURCE_PREFIX ~ embedded.name; 1543 _idToFileMap[id] = fn; 1544 return fn; 1545 } 1546 foreach(string path; _resourcePaths) { 1547 string fn; 1548 fn = checkFileName(path, id, ".xml"); 1549 if (fn is null && WIDGET_STYLE_CONSOLE) 1550 fn = checkFileName(path, id, ".tim"); 1551 if (fn is null) 1552 fn = checkFileName(path, id, ".png"); 1553 if (fn is null) 1554 fn = checkFileName(path, id, ".9.png"); 1555 if (fn is null) 1556 fn = checkFileName(path, id, ".jpg"); 1557 if (fn !is null) { 1558 _idToFileMap[id] = fn; 1559 return fn; 1560 } 1561 } 1562 Log.w("resource ", id, " is not found"); 1563 return null; 1564 } 1565 static if (BACKEND_GUI) { 1566 /// get image (DrawBuf) from imageCache by resource id 1567 DrawBufRef getImage(string id) { 1568 DrawBufRef res; 1569 string fname = findResource(id); 1570 if (fname.endsWith(".png") || fname.endsWith(".jpg")) 1571 return imageCache.get(fname); 1572 return res; 1573 } 1574 } 1575 this() { 1576 debug Log.i("Creating DrawableCache"); 1577 } 1578 ~this() { 1579 debug(resalloc) Log.e("Drawable instace count before destroying of DrawableCache: ", ImageDrawable.instanceCount); 1580 1581 //Log.i("Destroying DrawableCache _idToDrawableMap.length=", _idToDrawableMap.length); 1582 Log.i("Destroying DrawableCache"); 1583 foreach (ref item; _idToDrawableMap) { 1584 destroy(item); 1585 item = null; 1586 } 1587 _idToDrawableMap.destroy(); 1588 debug if(ImageDrawable.instanceCount) Log.e("Drawable instace count after destroying of DrawableCache: ", ImageDrawable.instanceCount); 1589 } 1590 } 1591 1592 1593 /// This function takes an id and creates a drawable 1594 /// id may be a name of file, #directive, color or json 1595 private Drawable makeDrawableFromId(in string id, in bool tiled, ColorTransform transform = ColorTransform()) { 1596 if (id !is null) { 1597 if (id.endsWith(".xml") || id.endsWith(".XML")) { 1598 // XML drawables support 1599 auto d = new StateDrawable; 1600 if (!d.load(id)) { 1601 Log.e("failed to load .xml drawable from ", id); 1602 destroy(d); 1603 return null; 1604 } else { 1605 Log.d("loaded .xml drawable from ", id); 1606 return d; 1607 } 1608 } else if (id.endsWith(".tim") || id.endsWith(".TIM")) { 1609 static if (WIDGET_STYLE_CONSOLE) { 1610 try { 1611 // .tim (text image) drawables support 1612 string s = cast(string)loadResourceBytes(id); 1613 if (s.length) { 1614 auto d = new TextDrawable(s); 1615 if (d.width && d.height) { 1616 return d; 1617 } 1618 } 1619 } catch (Exception e) { 1620 // cannot find drawable file 1621 } 1622 } 1623 } else if (id.startsWith("#")) { 1624 // color reference #AARRGGBB, e.g. #5599AA, a gradient, border description, etc. 1625 return createColorDrawable(id); 1626 } else if (id.startsWith("{")) { 1627 // json in {} with text drawable description 1628 static if (WIDGET_STYLE_CONSOLE) { 1629 return createTextDrawable(id); 1630 } 1631 } else { 1632 static if (BACKEND_GUI) { 1633 // PNG/JPEG drawables support 1634 DrawBufRef image = transform.empty ? imageCache.get(id) : imageCache.get(id, transform); 1635 if (!image.isNull) { 1636 bool ninePatch = id.endsWith(".9.png") || id.endsWith(".9.PNG"); 1637 return new ImageDrawable(image, tiled, ninePatch); 1638 } else 1639 Log.e("Failed to load image from ", id); 1640 } 1641 } 1642 } 1643 return null; 1644 } 1645 1646 1647 /// load text resource 1648 string loadTextResource(string resourceId) { 1649 string filename = drawableCache.findResource(resourceId); 1650 if (!filename) { 1651 Log.e("Object resource file not found for resourceId ", resourceId); 1652 assert(false); 1653 } 1654 string s = cast(string)loadResourceBytes(filename); 1655 if (!s) { 1656 Log.e("Cannot read text resource ", resourceId, " from file ", filename); 1657 assert(false); 1658 } 1659 return s; 1660 }