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