1 module fluid.backend.headless; 2 3 debug (Fluid_BuildMessages) { 4 pragma(msg, "Fluid: Building with headless backend"); 5 } 6 7 import std.math; 8 import std.array; 9 import std.range; 10 import std.string; 11 import std.sumtype; 12 import std.algorithm; 13 14 import fluid.backend; 15 16 17 @safe: 18 19 20 /// Rendering textures in SVG requires arsd.image 21 version (Have_arsd_official_image_files) 22 enum svgTextures = true; 23 else 24 enum svgTextures = false; 25 26 27 class HeadlessBackend : FluidBackend { 28 29 enum State { 30 31 up, 32 pressed, 33 repeated, 34 down, 35 released, 36 37 } 38 39 struct DrawnLine { 40 41 Vector2 start, end; 42 Color color; 43 44 } 45 46 struct DrawnTriangle { 47 48 Vector2 a, b, c; 49 Color color; 50 51 bool isClose(Vector2 a, Vector2 b, Vector2 c) const 52 => .isClose(a.x, this.a.x) 53 && .isClose(a.y, this.a.y) 54 && .isClose(b.x, this.b.x) 55 && .isClose(b.y, this.b.y) 56 && .isClose(c.x, this.c.x) 57 && .isClose(c.y, this.c.y); 58 59 60 } 61 62 struct DrawnRectangle { 63 64 Rectangle rectangle; 65 Color color; 66 67 alias rectangle this; 68 69 bool isClose(Rectangle rectangle) const 70 71 => isClose(rectangle.tupleof); 72 73 bool isClose(float x, float y, float width, float height) const 74 75 => .isClose(this.rectangle.x, x) 76 && .isClose(this.rectangle.y, y) 77 && .isClose(this.rectangle.width, width) 78 && .isClose(this.rectangle.height, height); 79 80 bool isStartClose(Vector2 start) const 81 82 => isStartClose(start.tupleof); 83 84 bool isStartClose(float x, float y) const 85 86 => .isClose(this.rectangle.x, x) 87 && .isClose(this.rectangle.y, y); 88 89 } 90 91 struct DrawnTexture { 92 93 uint id; 94 int width; 95 int height; 96 int dpiX; 97 int dpiY; 98 Rectangle rectangle; 99 Color tint; 100 101 alias drawnRectangle this; 102 103 this(Texture texture, Rectangle rectangle, Color tint) { 104 105 // Omit the "backend" Texture field to make `canvas` @safe 106 this.id = texture.id; 107 this.width = texture.width; 108 this.height = texture.height; 109 this.dpiX = texture.dpiX; 110 this.dpiY = texture.dpiY; 111 this.rectangle = rectangle; 112 this.tint = tint; 113 114 } 115 116 Vector2 position() const 117 118 => Vector2(rectangle.x, rectangle.y); 119 120 DrawnRectangle drawnRectangle() const 121 122 => DrawnRectangle(rectangle, tint); 123 124 alias isPositionClose = isStartClose; 125 126 bool isStartClose(Vector2 start) const 127 128 => isStartClose(start.tupleof); 129 130 bool isStartClose(float x, float y) const 131 132 => .isClose(rectangle.x, x) 133 && .isClose(rectangle.y, y); 134 135 } 136 137 alias Drawing = SumType!(DrawnLine, DrawnTriangle, DrawnRectangle, DrawnTexture); 138 139 private { 140 141 dstring characterQueue; 142 State[MouseButton.max+1] mouse; 143 State[KeyboardKey.max+1] keyboard; 144 State[GamepadButton.max+1] gamepad; 145 Vector2 _scroll; 146 Vector2 _mousePosition; 147 Vector2 _windowSize; 148 Vector2 _dpi = Vector2(96, 96); 149 float _scale = 1; 150 Rectangle _area; 151 FluidMouseCursor _cursor; 152 float _deltaTime = 1f / 60f; 153 bool _justResized; 154 bool _scissorsOn; 155 156 /// Currently allocated/used textures as URLs. 157 /// 158 /// Textures loaded from images are `null` if arsd.image isn't present. 159 string[uint] allocatedTextures; 160 161 /// Texture reaper. 162 TextureReaper _reaper; 163 164 /// Last used texture ID. 165 uint lastTextureID; 166 167 } 168 169 public { 170 171 /// All items drawn during the last frame 172 Appender!(Drawing[]) canvas; 173 174 } 175 176 this(Vector2 windowSize = Vector2(800, 600)) { 177 178 this._windowSize = windowSize; 179 180 } 181 182 /// Switch to the next frame. 183 void nextFrame(float deltaTime = 1f / 60f) { 184 185 deltaTime = deltaTime; 186 187 // Clear temporary data 188 characterQueue = null; 189 _justResized = false; 190 _scroll = Vector2(); 191 canvas.clear(); 192 193 // Update input 194 foreach (ref state; chain(mouse[], keyboard[], gamepad[])) { 195 196 final switch (state) { 197 198 case state.up: 199 case state.down: 200 break; 201 case state.pressed: 202 case state.repeated: 203 state = State.down; 204 break; 205 case state.released: 206 state = State.up; 207 break; 208 209 210 } 211 212 } 213 214 } 215 216 /// Resize the window. 217 void resize(Vector2 size) { 218 219 _windowSize = size; 220 _justResized = true; 221 222 } 223 224 /// Press the given key, and hold it until `release`. Marks as repeated if already down. 225 void press(KeyboardKey key) { 226 227 if (isDown(key)) 228 keyboard[key] = State.repeated; 229 else 230 keyboard[key] = State.pressed; 231 232 } 233 234 /// Release the given keyboard key. 235 void release(KeyboardKey key) { 236 237 keyboard[key] = State.released; 238 239 } 240 241 /// Press the given button, and hold it until `release`. 242 void press(MouseButton button = MouseButton.left) { 243 244 mouse[button] = State.pressed; 245 246 } 247 248 /// Release the given mouse button. 249 void release(MouseButton button = MouseButton.left) { 250 251 mouse[button] = State.released; 252 253 } 254 255 /// Press the given button, and hold it until `release`. 256 void press(GamepadButton button) { 257 258 gamepad[button] = State.pressed; 259 260 } 261 262 /// Release the given mouse button. 263 void release(GamepadButton button) { 264 265 gamepad[button] = State.released; 266 267 } 268 269 /// Check if the given mouse button has just been pressed/released or, if it's held down or not (up). 270 bool isPressed(MouseButton button) const 271 272 => mouse[button] == State.pressed; 273 274 bool isReleased(MouseButton button) const 275 276 => mouse[button] == State.released; 277 278 bool isDown(MouseButton button) const 279 280 => mouse[button] == State.pressed 281 || mouse[button] == State.repeated 282 || mouse[button] == State.down; 283 284 bool isUp(MouseButton button) const 285 286 => mouse[button] == State.released 287 || mouse[button] == State.up; 288 289 /// Check if the given keyboard key has just been pressed/released or, if it's held down or not (up). 290 bool isPressed(KeyboardKey key) const 291 292 => keyboard[key] == State.pressed; 293 294 bool isReleased(KeyboardKey key) const 295 296 => keyboard[key] == State.released; 297 298 bool isDown(KeyboardKey key) const 299 300 => keyboard[key] == State.pressed 301 || keyboard[key] == State.repeated 302 || keyboard[key] == State.down; 303 304 bool isUp(KeyboardKey key) const 305 306 => keyboard[key] == State.released 307 || keyboard[key] == State.up; 308 309 /// If true, the given keyboard key has been virtually pressed again, through a long-press. 310 bool isRepeated(KeyboardKey key) const 311 312 => keyboard[key] == State.repeated; 313 314 /// Get next queued character from user's input. The queue should be cleared every frame. Return null if no 315 /// character was pressed. 316 dchar inputCharacter() { 317 318 if (characterQueue.empty) return '\0'; 319 320 auto c = characterQueue.front; 321 characterQueue.popFront; 322 return c; 323 324 } 325 326 /// Insert a character into input queue. 327 void inputCharacter(dchar character) { 328 329 characterQueue ~= character; 330 331 } 332 333 /// ditto 334 void inputCharacter(dstring str) { 335 336 characterQueue ~= str; 337 338 } 339 340 /// Check if the given gamepad button has been pressed/released or, if it's held down or not (up). 341 int isPressed(GamepadButton button) const 342 343 => gamepad[button] == State.pressed; 344 345 int isReleased(GamepadButton button) const 346 347 => gamepad[button] == State.released; 348 349 int isDown(GamepadButton button) const 350 351 => gamepad[button] == State.pressed 352 || gamepad[button] == State.repeated 353 || gamepad[button] == State.down; 354 355 int isUp(GamepadButton button) const 356 357 => gamepad[button] == State.released 358 || gamepad[button] == State.up; 359 360 int isRepeated(GamepadButton button) const 361 362 => gamepad[button] == State.repeated; 363 364 /// Get/set mouse position 365 Vector2 mousePosition(Vector2 value) 366 367 => _mousePosition = value; 368 369 Vector2 mousePosition() const 370 371 => _mousePosition; 372 373 /// Get/set mouse scroll 374 Vector2 scroll(Vector2 value) 375 376 => _scroll = scroll; 377 378 Vector2 scroll() const 379 380 => _scroll; 381 382 /// Get time elapsed since last frame in seconds. 383 float deltaTime() const 384 385 => _deltaTime; 386 387 /// True if the user has just resized the window. 388 bool hasJustResized() const 389 390 => _justResized; 391 392 /// Get or set the size of the window. 393 Vector2 windowSize(Vector2 value) 394 395 => _windowSize = value; 396 397 Vector2 windowSize() const 398 399 => _windowSize; 400 401 float scale() const 402 403 => _scale; 404 405 float scale(float value) 406 407 => _scale = value; 408 409 /// Get HiDPI scale of the window. This is not currently supported by this backend. 410 Vector2 dpi() const 411 412 => _dpi * _scale; 413 414 /// Set area within the window items will be drawn to; any pixel drawn outside will be discarded. 415 Rectangle area(Rectangle rect) { 416 417 _scissorsOn = true; 418 return _area = rect; 419 420 } 421 422 Rectangle area() const 423 424 => _scissorsOn ? _area : Rectangle(0, 0, _windowSize.tupleof); 425 426 /// Restore the capability to draw anywhere in the window. 427 void restoreArea() { 428 429 _scissorsOn = false; 430 431 } 432 433 /// Get or set mouse cursor icon. 434 FluidMouseCursor mouseCursor(FluidMouseCursor cursor) 435 436 => _cursor = cursor; 437 438 FluidMouseCursor mouseCursor() const 439 440 => _cursor; 441 442 TextureReaper* reaper() return scope { 443 444 return &_reaper; 445 446 } 447 448 Texture loadTexture(Image image) @system { 449 450 // It's probably desirable to have this toggleable at class level 451 static if (svgTextures) { 452 453 import std.base64; 454 import arsd.png; 455 import arsd.image; 456 457 // Load the image 458 auto data = cast(ubyte[]) image.pixels; 459 auto arsdImage = new TrueColorImage(image.width, image.height, data); 460 461 // Encode as a PNG in a data URL 462 auto png = arsdImage.writePngToArray(); 463 auto base64 = Base64.encode(png); 464 auto url = format!"data:image/png;base64,%s"(base64); 465 466 // Convert to a Fluid image 467 return loadTexture(url, arsdImage.width, arsdImage.height); 468 469 } 470 471 // Can't load the texture, pretend to load a 16px texture 472 else return loadTexture(null, image.width, image.height); 473 474 } 475 476 Texture loadTexture(string filename) @system { 477 478 static if (svgTextures) { 479 480 import std.uri : encodeURI = encode; 481 import std.path; 482 import arsd.image; 483 484 // Load the image to check its size 485 auto image = loadImageFromFile(filename); 486 auto url = format!"file:///%s"(filename.absolutePath.encodeURI); 487 488 return loadTexture(url, image.width, image.height); 489 490 } 491 492 // Can't load the texture, pretend to load a 16px texture 493 else return loadTexture(null, 16, 16); 494 495 } 496 497 Texture loadTexture(string url, int width, int height) { 498 499 Texture texture; 500 texture.id = ++lastTextureID; 501 texture.tombstone = reaper.makeTombstone(this, texture.id); 502 texture.width = width; 503 texture.height = height; 504 505 // Allocate the texture 506 allocatedTextures[texture.id] = url; 507 508 return texture; 509 510 } 511 512 /// Destroy a texture created by this backend. `texture.destroy()` is the preferred way of calling this, since it 513 /// will ensure the correct backend is called. 514 void unloadTexture(uint id) @system { 515 516 const found = id in allocatedTextures; 517 518 assert(found, format!"headless: Attempted to free nonexistent texture ID %s (double free?)"(id)); 519 520 allocatedTextures.remove(id); 521 522 } 523 524 /// Check if the given texture has a valid ID 525 bool isTextureValid(Texture texture) { 526 527 return cast(bool) (texture.id in allocatedTextures); 528 529 } 530 531 bool isTextureValid(uint id) { 532 533 return cast(bool) (id in allocatedTextures); 534 535 } 536 537 /// Draw a line. 538 void drawLine(Vector2 start, Vector2 end, Color color) { 539 540 canvas ~= Drawing(DrawnLine(start, end, color)); 541 542 } 543 544 /// Draw a triangle, consisting of 3 vertices with counter-clockwise winding. 545 void drawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color) { 546 547 canvas ~= Drawing(DrawnTriangle(a, b, c, color)); 548 549 } 550 551 /// Draw a rectangle. 552 void drawRectangle(Rectangle rectangle, Color color) { 553 554 canvas ~= Drawing(DrawnRectangle(rectangle, color)); 555 556 } 557 558 /// Draw a texture. 559 void drawTexture(Texture texture, Rectangle rectangle, Color tint, string altText = "") 560 in (false) 561 do { 562 563 canvas ~= Drawing(DrawnTexture(texture, rectangle, tint)); 564 565 } 566 567 /// Draw a texture, but keep it aligned to pixel boundaries. 568 void drawTextureAlign(Texture texture, Rectangle rectangle, Color tint, string altText = "") 569 in (false) 570 do { 571 572 drawTexture(texture, rectangle, tint, altText); 573 574 } 575 576 /// Get items from the canvas that match the given type. 577 auto filterCanvas(T)() 578 579 => canvas[] 580 581 // Filter out items that don't match what was requested 582 .filter!(a => a.match!( 583 (T item) => true, 584 (_) => false 585 )) 586 587 // Return items that match 588 .map!(a => a.match!( 589 (T item) => item, 590 (_) => assert(false), 591 )); 592 593 alias lines = filterCanvas!DrawnLine; 594 alias triangles = filterCanvas!DrawnTriangle; 595 alias rectangles = filterCanvas!DrawnRectangle; 596 alias textures = filterCanvas!DrawnTexture; 597 598 /// Throw an `AssertError` if given triangle was never drawn. 599 void assertTriangle(Vector2 a, Vector2 b, Vector2 c, Color color) { 600 601 assert( 602 triangles.canFind!(trig => trig.isClose(a, b, c) && trig.color == color), 603 "No matching triangle" 604 ); 605 606 } 607 608 /// Throw an `AssertError` if given rectangle was never drawn. 609 void assertRectangle(Rectangle r, Color color) { 610 611 assert( 612 rectangles.canFind!(rect => rect.isClose(r) && rect.color == color), 613 "No matching rectangle" 614 ); 615 616 } 617 618 /// Throw an `AssertError` if the texture was never drawn with given parameters. 619 void assertTexture(const Texture texture, Vector2 position, Color tint) { 620 621 assert(texture.backend is this, "Given texture comes from a different backend"); 622 assert( 623 textures.canFind!(tex 624 => tex.id == texture.id 625 && tex.width == texture.width 626 && tex.height == texture.height 627 && tex.dpiX == texture.dpiX 628 && tex.dpiY == texture.dpiY 629 && tex.isPositionClose(position) 630 && tex.tint == tint), 631 "No matching texture" 632 ); 633 634 } 635 636 /// Throw an `AssertError` if given texture was never drawn. 637 void assertTexture(Rectangle r, Color color) { 638 639 assert( 640 textures.canFind!(rect => rect.isClose(r) && rect.color == color), 641 "No matching texture" 642 ); 643 644 } 645 646 version (Have_elemi) { 647 648 import std.conv; 649 import elemi.xml; 650 651 /// Convert the canvas to SVG. Intended for debugging only. 652 /// 653 /// `toSVG` provides the document as a string (including the XML prolog), `toSVGElement` provides a Fluid element 654 /// (without the prolog) and `saveSVG` saves it to a file. 655 /// 656 /// Note that rendering textures and text is only done if arsd.image is available. Otherwise, they will display 657 /// as rectangles filled with whatever tint color was set. Text, if rendered, is rasterized, because it occurs 658 /// earlier in the pipeline, and is not available to the backend. 659 void saveSVG(string filename) const { 660 661 import std.file : write; 662 663 write(filename, toSVG); 664 665 } 666 667 /// ditto 668 string toSVG() const 669 670 => Element.XMLDeclaration1_0 ~ this.toSVGElement; 671 672 /// ditto 673 Element toSVGElement() const { 674 675 /// Colors available as tint filters in the document. 676 bool[Color] tints; 677 678 /// Generate a tint filter for the given color 679 Element useTint(Color color) { 680 681 // Ignore if the given filter already exists 682 if (color in tints) return elems(); 683 684 tints[color] = true; 685 686 // <pain> 687 return elem!"filter"( 688 689 // Use the color as the filter ID, prefixed with "t" instead of "#" 690 attr("id") = color.toHex!"t", 691 692 // Create a layer full of that color 693 elem!"feFlood"( 694 attr("x") = "0", 695 attr("y") = "0", 696 attr("width") = "100%", 697 attr("height") = "100%", 698 attr("flood-color") = color.toHex, 699 ), 700 701 // Blend in with the original image 702 elem!"feBlend"( 703 attr("in2") = "SourceGraphic", 704 attr("mode") = "multiply", 705 ), 706 707 // Use the source image for opacity 708 elem!"feComposite"( 709 attr("in2") = "SourceGraphic", 710 attr("operator") = "in", 711 ), 712 713 ); 714 // </pain> 715 716 } 717 718 return elem!"svg"( 719 attr("xmlns") = "http://www.w3.org/2000/svg", 720 attr("version") = "1.1", 721 attr("width") = text(cast(int) windowSize.x), 722 attr("height") = text(cast(int) windowSize.y), 723 724 canvas[].map!(a => a.match!( 725 (DrawnLine line) => elem!"line"( 726 attr("x1") = line.start.x.text, 727 attr("y1") = line.start.y.text, 728 attr("x2") = line.end.x.text, 729 attr("y2") = line.end.y.text, 730 attr("stroke") = line.color.toHex, 731 ), 732 (DrawnTriangle trig) => elem!"polygon"( 733 attr("points") = [ 734 format!"%s,%s"(trig.a.tupleof), 735 format!"%s,%s"(trig.b.tupleof), 736 format!"%s,%s"(trig.c.tupleof), 737 ], 738 attr("fill") = trig.color.toHex, 739 ), 740 (DrawnTexture texture) { 741 742 auto url = texture.id in allocatedTextures; 743 744 // URL given, valid image 745 if (url && *url) 746 return elems( 747 useTint(texture.tint), 748 elem!"image"( 749 attr("x") = texture.rectangle.x.text, 750 attr("y") = texture.rectangle.y.text, 751 attr("width") = texture.rectangle.width.text, 752 attr("height") = texture.rectangle.height.text, 753 attr("href") = *url, 754 attr("style") = format!"filter:url(#%s)"(texture.tint.toHex!"t"), 755 ), 756 ); 757 758 // No image, draw a placeholder rect 759 else 760 return elem!"rect"( 761 attr("x") = texture.position.x.text, 762 attr("y") = texture.position.y.text, 763 attr("width") = texture.width.text, 764 attr("height") = texture.height.text, 765 attr("fill") = texture.tint.toHex, 766 ); 767 768 }, 769 (DrawnRectangle rect) => elem!"rect"( 770 attr("x") = rect.x.text, 771 attr("y") = rect.y.text, 772 attr("width") = rect.width.text, 773 attr("height") = rect.height.text, 774 attr("fill") = rect.color.toHex, 775 ), 776 )) 777 ); 778 779 } 780 781 } 782 783 } 784 785 unittest { 786 787 auto backend = new HeadlessBackend(Vector2(800, 600)); 788 789 with (backend) { 790 791 press(MouseButton.left); 792 793 assert(isPressed(MouseButton.left)); 794 assert(isDown(MouseButton.left)); 795 assert(!isUp(MouseButton.left)); 796 assert(!isReleased(MouseButton.left)); 797 798 press(KeyboardKey.enter); 799 800 assert(isPressed(KeyboardKey.enter)); 801 assert(isDown(KeyboardKey.enter)); 802 assert(!isUp(KeyboardKey.enter)); 803 assert(!isReleased(KeyboardKey.enter)); 804 assert(!isRepeated(KeyboardKey.enter)); 805 806 nextFrame; 807 808 assert(!isPressed(MouseButton.left)); 809 assert(isDown(MouseButton.left)); 810 assert(!isUp(MouseButton.left)); 811 assert(!isReleased(MouseButton.left)); 812 813 assert(!isPressed(KeyboardKey.enter)); 814 assert(isDown(KeyboardKey.enter)); 815 assert(!isUp(KeyboardKey.enter)); 816 assert(!isReleased(KeyboardKey.enter)); 817 assert(!isRepeated(KeyboardKey.enter)); 818 819 nextFrame; 820 821 press(KeyboardKey.enter); 822 823 assert(!isPressed(KeyboardKey.enter)); 824 assert(isDown(KeyboardKey.enter)); 825 assert(!isUp(KeyboardKey.enter)); 826 assert(!isReleased(KeyboardKey.enter)); 827 assert(isRepeated(KeyboardKey.enter)); 828 829 nextFrame; 830 831 release(MouseButton.left); 832 833 assert(!isPressed(MouseButton.left)); 834 assert(!isDown(MouseButton.left)); 835 assert(isUp(MouseButton.left)); 836 assert(isReleased(MouseButton.left)); 837 838 release(KeyboardKey.enter); 839 840 assert(!isPressed(KeyboardKey.enter)); 841 assert(!isDown(KeyboardKey.enter)); 842 assert(isUp(KeyboardKey.enter)); 843 assert(isReleased(KeyboardKey.enter)); 844 assert(!isRepeated(KeyboardKey.enter)); 845 846 nextFrame; 847 848 assert(!isPressed(MouseButton.left)); 849 assert(!isDown(MouseButton.left)); 850 assert(isUp(MouseButton.left)); 851 assert(!isReleased(MouseButton.left)); 852 853 assert(!isPressed(KeyboardKey.enter)); 854 assert(!isDown(KeyboardKey.enter)); 855 assert(isUp(KeyboardKey.enter)); 856 assert(!isReleased(KeyboardKey.enter)); 857 assert(!isRepeated(KeyboardKey.enter)); 858 859 } 860 861 } 862 863 /// std.math.isClose adjusted for the most common use-case. 864 private bool isClose(float a, float b) { 865 866 return std.math.isClose(a, b, 0.0, 0.01); 867 868 } 869 870 unittest { 871 872 assert(isClose(1, 1)); 873 assert(isClose(1.004, 1)); 874 assert(isClose(1.01, 1.008)); 875 876 assert(!isClose(1, 2)); 877 assert(!isClose(1.02, 1)); 878 assert(!isClose(1.01, 1.03)); 879 880 }