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