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 }