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 }