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