1 /// This module handles input/output facilities Fluid requires to operate. It connects backends like Raylib, exposing
2 /// them under a common interface so they can be changed at will.
3 ///
4 /// Fluid comes with a built-in interface for Raylib.
5 module fluid.backend;
6 
7 import std.meta;
8 import std.range;
9 import std.traits;
10 import std.algorithm;
11 
12 public import fluid.backend.raylib5;
13 public import fluid.backend.headless;
14 public import fluid.backend.simpledisplay;
15 
16 
17 @safe:
18 
19 
20 alias VoidDelegate = void delegate() @safe;
21 
22 static FluidBackend defaultFluidBackend;
23 
24 /// `FluidBackend` is an interface making it possible to bind Fluid to a library other than Raylib. Another built-in
25 /// backend is `fluid.simpledisplay.SimpledisplayBackend` for `arsd.simpledisplay`.
26 ///
27 /// The default unit in graphical space is a **pixel** (`px`), here defined as **1/96 of an inch**. This is unless
28 /// stated otherwise, as in `Texture`.
29 interface FluidBackend {
30 
31     /// Check if the given mouse button has just been pressed/released or, if it's held down or not (up).
32     bool isPressed(MouseButton) const;
33     bool isReleased(MouseButton) const;
34     bool isDown(MouseButton) const;
35     bool isUp(MouseButton) const;
36 
37     /// Check if the given keyboard key has just been pressed/released or, if it's held down or not (up).
38     bool isPressed(KeyboardKey) const;
39     bool isReleased(KeyboardKey) const;
40     bool isDown(KeyboardKey) const;
41     bool isUp(KeyboardKey) const;
42 
43     /// If true, the given keyboard key has been virtually pressed again, through a long-press.
44     bool isRepeated(KeyboardKey) const;
45 
46     /// Get next queued character from user's input. The queue should be cleared every frame. Return null if no
47     /// character was pressed.
48     dchar inputCharacter();
49 
50     /// Check if the given gamepad button has been pressed/released or, if it's held down or not (up) on any of the
51     /// connected gamepads.
52     ///
53     /// Returns: 0 if the event isn't taking place on any controller, or number of the controller.
54     int isPressed(GamepadButton button) const;
55     int isReleased(GamepadButton button) const;
56     int isDown(GamepadButton button) const;
57     int isUp(GamepadButton button) const;
58 
59     /// If true, the given gamepad button has been virtually pressed again, through a long-press.
60     ///
61     /// Returns: 0 if no controller had a button repeat this frame, or number of the controller.
62     int isRepeated(GamepadButton button) const;
63 
64     /// Get/set mouse position
65     Vector2 mousePosition(Vector2);
66     Vector2 mousePosition() const;
67 
68     /// Get scroll value on both axes.
69     Vector2 scroll() const;
70 
71     /// Get time elapsed since last frame in seconds.
72     float deltaTime() const;
73 
74     /// True if the user has just resized the window.
75     bool hasJustResized() const;
76 
77     /// Get or set the size of the window.
78     Vector2 windowSize(Vector2);
79     Vector2 windowSize() const;  /// ditto
80 
81     /// Set scale to apply to whatever is drawn next.
82     ///
83     /// Suggested implementation is to increase return value of `dpi`.
84     float scale() const;
85 
86     /// ditto
87     float scale(float);
88 
89     /// Get horizontal and vertical DPI of the window.
90     Vector2 dpi() const;
91 
92     /// Get the DPI value for the window as a scale relative to 96 DPI.
93     final Vector2 hidpiScale() const {
94 
95         const dpi = this.dpi;
96         return Vector2(dpi.x / 96f, dpi.y / 96f);
97 
98     }
99 
100     /// Set area within the window items will be drawn to; any pixel drawn outside will be discarded.
101     Rectangle area(Rectangle rect);
102     Rectangle area() const;
103 
104     /// Restore the capability to draw anywhere in the window.
105     void restoreArea();
106 
107     /// Get or set mouse cursor icon.
108     FluidMouseCursor mouseCursor(FluidMouseCursor);
109     FluidMouseCursor mouseCursor() const;
110 
111     /// Texture reaper used by this backend. May be null.
112     ///
113     /// Highly recommended for OpenGL-based backends.
114     TextureReaper* reaper() return scope;
115 
116     /// Load a texture from memory or file.
117     Texture loadTexture(Image image) @system;
118     Texture loadTexture(string filename) @system;
119 
120     /// Destroy a texture created by this backend. Always use `texture.destroy()` to ensure thread safety and invoking
121     /// the correct backend.
122     protected void unloadTexture(uint id) @system;
123 
124     /// ditto
125     final void unloadTexture(Texture texture) @system {
126 
127         unloadTexture(texture.id);
128 
129     }
130 
131     /// Draw a line.
132     void drawLine(Vector2 start, Vector2 end, Color color);
133 
134     /// Draw a triangle, consisting of 3 vertices with counter-clockwise winding.
135     void drawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color);
136 
137     /// Draw a rectangle.
138     void drawRectangle(Rectangle rectangle, Color color);
139 
140     /// Draw a texture.
141     void drawTexture(Texture texture, Rectangle rectangle, Color tint, string altText = "")
142     in (texture.backend is this, "Given texture comes from a different backend");
143 
144     /// Draw a texture, but ensure it aligns with pixel boundaries, recommended for text.
145     void drawTextureAlign(Texture texture, Rectangle rectangle, Color tint, string altText = "")
146     in (texture.backend is this, "Given texture comes from a different backend");
147 
148 }
149 
150 /// Struct that maintains a registry of all allocated textures. It's used to finalize textures once they have been
151 /// marked for destruction. This makes it possible to mark them from any thread, while the reaper runs only on the main
152 /// thread, ensuring thread safety in OpenGL backends.
153 struct TextureReaper {
154 
155     /// Number of cycles between runs of the reaper.
156     int period = 60 * 5;
157 
158     int cycleAccumulator;
159 
160     @system shared(TextureTombstone)*[uint] textures;
161 
162     @disable this(ref TextureReaper);
163     @disable this(this);
164 
165     ~this() @trusted {
166 
167         destroyAll();
168 
169     }
170 
171     /// Create a tombstone.
172     shared(TextureTombstone)* makeTombstone(FluidBackend backend, uint textureID) @trusted {
173 
174         return textures[textureID] = TextureTombstone.make(backend);
175 
176     }
177 
178     /// Count number of cycles since last collection and collect if configured period has passed.
179     void check() {
180 
181         // Count cycles
182         if (++cycleAccumulator >= period) {
183 
184             // Run collection
185             collect();
186 
187         }
188 
189     }
190 
191     /// Collect all destroyed textures immediately.
192     void collect() @trusted {
193 
194         // Reset the cycle accumulator
195         cycleAccumulator = 0;
196 
197         // Find all destroyed textures
198         foreach (id, tombstone; textures) {
199 
200             // Texture marked for deletion
201             if (tombstone.isDestroyed) {
202 
203                 auto backend = cast() tombstone.backend;
204 
205                 // Unload it
206                 backend.unloadTexture(id);
207                 tombstone.markDisowned();
208 
209                 // Remove the texture from registry
210                 textures.remove(id);
211 
212             }
213 
214         }
215 
216     }
217 
218     /// Destroy all textures.
219     void destroyAll() @system {
220 
221         cycleAccumulator = 0;
222         scope (exit) textures.clear();
223 
224         // Find all textures
225         foreach (id, tombstone; textures) {
226 
227             auto backend = cast() tombstone.backend;
228 
229             // Unload the texture, even if it wasn't marked for deletion
230             backend.unloadTexture(id);
231 
232             // Disown all textures
233             tombstone.markDisowned();
234 
235         }
236 
237     }
238 
239 }
240 
241 /// Tombstones are used to ensure textures are freed on the same thread they have been created on.
242 ///
243 /// Tombstones are kept alive until the texture is explicitly destroyed and then finalized (disowned) from the main
244 /// thread by a periodically-running `TextureReaper`. This is necessary to make Fluid safe in multithreaded
245 /// environments.
246 shared struct TextureTombstone {
247 
248     import core.memory;
249     import core.atomic;
250     import core.stdc.stdlib;
251 
252     /// Backend that created this texture.
253     private FluidBackend _backend;
254 
255     private bool _destroyed, _disowned;
256 
257     static TextureTombstone* make(FluidBackend backend) @system {
258 
259         import core.exception;
260 
261         // Allocate the tombstone
262         auto data = malloc(TextureTombstone.sizeof);
263         if (data is null) throw new OutOfMemoryError("Failed to allocate a tombstone");
264 
265         // Initialize the tombstone
266         shared tombstone = cast(shared TextureTombstone*) data;
267         *tombstone = TextureTombstone.init;
268         tombstone._backend = cast(shared) backend;
269 
270         // Make sure the backend isn't freed while the tombstone is alive
271         GC.addRoot(cast(void*) backend);
272 
273         return tombstone;
274 
275     }
276 
277     /// Check if the texture has been destroyed.
278     bool isDestroyed() @system => _destroyed.atomicLoad;
279 
280     /// Get the backend owning this texture.
281     inout(shared FluidBackend) backend() inout => _backend;
282 
283     /// Mark the texture as destroyed.
284     void markDestroyed() @system {
285 
286         _destroyed.atomicStore(true);
287         tryDestroy();
288 
289     }
290 
291     /// Mark the texture as disowned.
292     void markDisowned() @system {
293 
294         _disowned.atomicStore(true);
295         tryDestroy();
296 
297     }
298 
299     /// As soon as the texture is both marked for destruction and disowned, the tombstone controlling its life is
300     /// destroyed.
301     ///
302     /// There are two relevant scenarios:
303     ///
304     /// * The texture is marked for destruction via a tombstone, then finalized from the main thread and disowned.
305     /// * The texture is finalized after the backend (for example, if they are both destroyed during the same GC
306     ///   collection). The backend disowns and frees the texture. The tombstone, however, remains alive to
307     ///   witness marking the texture as deleted.
308     ///
309     /// In both scenarios, this behavior ensures the tombstone will be freed.
310     private void tryDestroy() @system {
311 
312         // Destroyed and disowned
313         if (_destroyed.atomicLoad && _disowned.atomicLoad) {
314 
315             GC.removeRoot(cast(void*) _backend);
316             free(cast(void*) &this);
317 
318         }
319 
320     }
321 
322 }
323 
324 @system
325 unittest {
326 
327     // This unittest checks if textures will be correctly destroyed, even if the destruction call comes from another
328     // thread.
329 
330     import std.concurrency;
331     import fluid.space;
332     import fluid.image_view;
333 
334     auto io = new HeadlessBackend;
335     auto image = imageView("logo.png");
336     auto root = vspace(image);
337 
338     // Draw the frame once to let everything load
339     root.io = io;
340     root.draw();
341 
342     // Tune the reaper to run every frame
343     io.reaper.period = 1;
344 
345     // Get the texture
346     auto texture = image.release();
347     auto textureID = texture.id;
348     auto tombstone = texture.tombstone;
349 
350     // Texture should be allocated and assigned a tombstone
351     assert(texture.backend is io);
352     assert(!texture.tombstone.isDestroyed);
353     assert(io.isTextureValid(texture));
354 
355     // Destroy the texture on another thread
356     spawn((Texture texture) {
357 
358         texture.destroy();
359         ownerTid.send(true);
360 
361     }, texture);
362 
363     // Wait for confirmation
364     receiveOnly!bool;
365 
366     // The texture should be marked for deletion but remain alive
367     assert(texture.tombstone.isDestroyed);
368     assert(io.isTextureValid(texture));
369 
370     // Draw a frame, during which the reaper should destroy the texture
371     io.nextFrame;
372     root.children = [];
373     root.updateSize();
374     root.draw();
375 
376     assert(!io.isTextureValid(texture));
377     // There is no way to test if the tombstone has been freed
378 
379 }
380 
381 @system
382 unittest {
383 
384     // This unittest checks if tombstones work correctly even if the backend is destroyed before the texture.
385 
386     import std.concurrency;
387     import core.atomic;
388     import fluid.image_view;
389 
390     auto io = new HeadlessBackend;
391     auto root = imageView("logo.png");
392 
393     // Load the texture and draw
394     root.io = io;
395     root.draw();
396 
397     // Destroy the backend
398     destroy(io);
399 
400     auto texture = root.release();
401 
402     // The texture should have been automatically freed, but not marked for destruction
403     assert(!texture.tombstone.isDestroyed);
404     assert(texture.tombstone._disowned.atomicLoad);
405 
406     // Now, destroy the image
407     // If this operation succeeds, we're good
408     destroy(root);
409     // There is no way to test if the tombstone and texture have truly been freed
410 
411 }
412 
413 struct FluidMouseCursor {
414 
415     enum SystemCursors {
416 
417         systemDefault,     // Default system cursor.
418         none,              // No pointer.
419         pointer,           // Pointer indicating a link or button, typically a pointing hand. 👆
420         crosshair,         // Cross cursor, often indicating selection inside images.
421         text,              // Vertical beam indicating selectable text.
422         allScroll,         // Omnidirectional scroll, content can be scrolled in any direction (panned).
423         resizeEW,          // Cursor indicating the content underneath can be resized horizontally.
424         resizeNS,          // Cursor indicating the content underneath can be resized vertically.
425         resizeNESW,        // Diagonal resize cursor, top-right + bottom-left.
426         resizeNWSE,        // Diagonal resize cursor, top-left + bottom-right.
427         notAllowed,        // Indicates a forbidden action.
428 
429     }
430 
431     enum {
432 
433         systemDefault = FluidMouseCursor(SystemCursors.systemDefault),
434         none          = FluidMouseCursor(SystemCursors.none),
435         pointer       = FluidMouseCursor(SystemCursors.pointer),
436         crosshair     = FluidMouseCursor(SystemCursors.crosshair),
437         text          = FluidMouseCursor(SystemCursors.text),
438         allScroll     = FluidMouseCursor(SystemCursors.allScroll),
439         resizeEW      = FluidMouseCursor(SystemCursors.resizeEW),
440         resizeNS      = FluidMouseCursor(SystemCursors.resizeNS),
441         resizeNESW    = FluidMouseCursor(SystemCursors.resizeNESW),
442         resizeNWSE    = FluidMouseCursor(SystemCursors.resizeNWSE),
443         notAllowed    = FluidMouseCursor(SystemCursors.notAllowed),
444 
445     }
446 
447     /// Use a system-provided cursor.
448     SystemCursors system;
449     // TODO user-provided cursor image
450 
451 }
452 
453 enum MouseButton {
454     none,
455     left,         // Left (primary) mouse button.
456     right,        // Right (secondary) mouse button.
457     middle,       // Middle mouse button.
458     extra1,       // Additional mouse button.
459     extra2,       // ditto.
460     forward,      // Mouse button going forward in browser history.
461     back,         // Mouse button going back in browser history.
462 
463     primary = left,
464     secondary = right,
465 
466 }
467 
468 enum GamepadButton {
469 
470     none,                // No such button
471     dpadUp,              // Dpad up button.
472     dpadRight,           // Dpad right button
473     dpadDown,            // Dpad down button
474     dpadLeft,            // Dpad left button
475     triangle,            // Triangle (PS) or Y (Xbox)
476     circle,              // Circle (PS) or B (Xbox)
477     cross,               // Cross (PS) or A (Xbox)
478     square,              // Square (PS) or X (Xbox)
479     leftButton,          // Left button behind the controlller (LB).
480     leftTrigger,         // Left trigger (LT).
481     rightButton,         // Right button behind the controller (RB).
482     rightTrigger,        // Right trigger (RT)
483     select,              // "Select" button.
484     vendor,              // Button with the vendor logo.
485     start,               // "Start" button.
486     leftStick,           // Left joystick press.
487     rightStick,          // Right joystick press.
488 
489     y = triangle,
490     x = square,
491     a = cross,
492     b = circle,
493 
494 }
495 
496 enum GamepadAxis {
497 
498     leftX,         // Left joystick, X axis.
499     leftY,         // Left joystick, Y axis.
500     rightX,        // Right joystick, X axis.
501     rightY,        // Right joystick, Y axis.
502     leftTrigger,   // Analog input for the left trigger.
503     rightTrigger,  // Analog input for the right trigger.
504 
505 }
506 
507 enum KeyboardKey {
508     none               = 0,        // No key pressed
509     apostrophe         = 39,       // '
510     comma              = 44,       // ,
511     dash               = comma,
512     minus              = 45,       // -
513     period             = 46,       // .
514     slash              = 47,       // /
515     digit0             = 48,       // 0
516     digit1             = 49,       // 1
517     digit2             = 50,       // 2
518     digit3             = 51,       // 3
519     digit4             = 52,       // 4
520     digit5             = 53,       // 5
521     digit6             = 54,       // 6
522     digit7             = 55,       // 7
523     digit8             = 56,       // 8
524     digit9             = 57,       // 9
525     semicolon          = 59,       // ;
526     equal              = 61,       // =
527     a                  = 65,       // A | a
528     b                  = 66,       // B | b
529     c                  = 67,       // C | c
530     d                  = 68,       // D | d
531     e                  = 69,       // E | e
532     f                  = 70,       // F | f
533     g                  = 71,       // G | g
534     h                  = 72,       // H | h
535     i                  = 73,       // I | i
536     j                  = 74,       // J | j
537     k                  = 75,       // K | k
538     l                  = 76,       // L | l
539     m                  = 77,       // M | m
540     n                  = 78,       // N | n
541     o                  = 79,       // O | o
542     p                  = 80,       // P | p
543     q                  = 81,       // Q | q
544     r                  = 82,       // R | r
545     s                  = 83,       // S | s
546     t                  = 84,       // T | t
547     u                  = 85,       // U | u
548     v                  = 86,       // V | v
549     w                  = 87,       // W | w
550     x                  = 88,       // X | x
551     y                  = 89,       // Y | y
552     z                  = 90,       // Z | z
553     leftBracket        = 91,       // [
554     backslash          = 92,       // '\'
555     rightBracket       = 93,       // ]
556     backtick           = 96,       // `
557     grave              = backtick,
558     space              = 32,       // Space
559     escape             = 256,      // Esc
560     esc                = escape,
561     enter              = 257,      // Enter
562     tab                = 258,      // Tab
563     backspace          = 259,      // Backspace
564     insert             = 260,      // Ins
565     del                = 261,      // Del
566     delete_            = del,
567     right              = 262,      // Cursor right
568     left               = 263,      // Cursor left
569     down               = 264,      // Cursor down
570     up                 = 265,      // Cursor up
571     pageUp             = 266,      // Page up
572     pageDown           = 267,      // Page down
573     home               = 268,      // Home
574     end                = 269,      // End
575     capsLock           = 280,      // Caps lock
576     scrollLock         = 281,      // Scroll down
577     numLock            = 282,      // Num lock
578     printScreen        = 283,      // Print screen
579     pause              = 284,      // Pause
580     f1                 = 290,      // F1
581     f2                 = 291,      // F2
582     f3                 = 292,      // F3
583     f4                 = 293,      // F4
584     f5                 = 294,      // F5
585     f6                 = 295,      // F6
586     f7                 = 296,      // F7
587     f8                 = 297,      // F8
588     f9                 = 298,      // F9
589     f10                = 299,      // F10
590     f11                = 300,      // F11
591     f12                = 301,      // F12
592     leftShift          = 340,      // Shift left
593     leftControl        = 341,      // Control left
594     leftAlt            = 342,      // Alt left
595     leftSuper          = 343,      // Super left
596     rightShift         = 344,      // Shift right
597     rightControl       = 345,      // Control right
598     rightAlt           = 346,      // Alt right
599     rightSuper         = 347,      // Super right
600     contextMenu        = 348,      // Context menu
601     keypad0            = 320,      // Keypad 0
602     keypad1            = 321,      // Keypad 1
603     keypad2            = 322,      // Keypad 2
604     keypad3            = 323,      // Keypad 3
605     keypad4            = 324,      // Keypad 4
606     keypad5            = 325,      // Keypad 5
607     keypad6            = 326,      // Keypad 6
608     keypad7            = 327,      // Keypad 7
609     keypad8            = 328,      // Keypad 8
610     keypad9            = 329,      // Keypad 9
611     keypadDecimal      = 330,      // Keypad .
612     keypadDivide       = 331,      // Keypad /
613     keypadMultiply     = 332,      // Keypad *
614     keypadSubtract     = 333,      // Keypad -
615     keypadSum          = 334,      // Keypad +
616     keypadEnter        = 335,      // Keypad Enter
617     keypadEqual        = 336,      // Keypad =
618     androidBack        = 4,        // Android back button
619     androidMenu        = 82,       // Android menu button
620     volumeUp           = 24,       // Android volume up button
621     volumeDown         = 25        // Android volume down button
622     // Function keys for volume?
623 
624 }
625 
626 /// Generate an image filled with a given color.
627 ///
628 /// Note: Image data is GC-allocated. Make sure to keep a reference alive when passing to the backend. Do not use
629 /// `UnloadImage` if using Raylib.
630 static Image generateColorImage(int width, int height, Color color) {
631 
632     // Generate each pixel
633     auto data = new Color[width * height];
634     data[] = color;
635 
636     // Prepare the result
637     Image image;
638     image.pixels = data;
639     image.width = width;
640     image.height = height;
641 
642     return image;
643 
644 }
645 
646 /// Image available to the CPU.
647 struct Image {
648 
649     /// Image data.
650     Color[] pixels;
651     int width, height;
652 
653     Vector2 size() const => Vector2(width, height);
654 
655     ref inout(Color) get(int x, int y) inout {
656 
657         return pixels[y * width + x];
658 
659     }
660 
661     /// Safer alternative to `get`, doesn't draw out of bounds.
662     void set(int x, int y, Color color) {
663 
664         if (x < 0 || y < 0) return;
665         if (x >= width || y >= height) return;
666 
667         get(x, y) = color;
668 
669     }
670 
671 }
672 
673 /// Represents a GPU texture.
674 ///
675 /// Textures make use of manual memory management.
676 struct Texture {
677 
678     /// Tombstone for this texture
679     shared(TextureTombstone)* tombstone;
680 
681     /// GPU/backend ID of the texture.
682     uint id;
683 
684     /// Width and height of the texture, **in dots**. The meaning of a dot is defined by `dpiX` and `dpiY`
685     int width, height;
686 
687     /// Dots per inch for the X and Y axis. Defaults to 96, thus making a dot in the texture equivalent to a pixel.
688     int dpiX = 96, dpiY = 96;
689 
690     bool opEquals(const Texture other) const
691 
692         => id == other.id
693         && width == other.width
694         && height == other.height
695         && dpiX == other.dpiX
696         && dpiY == other.dpiY;
697 
698     /// Get the backend for this texture. Doesn't work after freeing the tombstone.
699     inout(FluidBackend) backend() inout @trusted
700 
701         => cast(inout FluidBackend) tombstone.backend;
702 
703     /// DPI value of the texture.
704     Vector2 dpi() const
705 
706         => Vector2(dpiX, dpiY);
707 
708     /// Get texture size as a vector.
709     Vector2 canvasSize() const
710 
711         => Vector2(width, height);
712 
713     /// Get the size the texture will occupy within the viewport.
714     Vector2 viewportSize() const
715 
716         => Vector2(
717             width * 96 / dpiX,
718             height * 96 / dpiY
719         );
720 
721     /// Draw this texture.
722     void draw(Vector2 position, Color tint = color!"fff") {
723 
724         auto rectangle = Rectangle(position.tupleof, viewportSize.tupleof);
725 
726         backend.drawTexture(this, rectangle, tint);
727 
728     }
729 
730     void draw(Rectangle rectangle, Color tint = color!"fff") {
731 
732         backend.drawTexture(this, rectangle, tint);
733 
734     }
735 
736     /// Destroy this texture. This function is thread-safe.
737     void destroy() @system {
738 
739         if (tombstone is null) return;
740 
741         tombstone.markDestroyed();
742         tombstone = null;
743         id = 0;
744 
745     }
746 
747 }
748 
749 /// Get a hex code from color.
750 string toHex(string prefix = "#")(Color color) {
751 
752     import std.format;
753 
754     // Full alpha, use a six digit code
755     if (color.a == 0xff) {
756 
757         return format!(prefix ~ "%02x%02x%02x")(color.r, color.g, color.b);
758 
759     }
760 
761     // Include alpha otherwise
762     else return format!(prefix ~ "%02x%02x%02x%02x")(color.tupleof);
763 
764 }
765 
766 unittest {
767 
768     // No relevant alpha
769     assert(color("fff").toHex == "#ffffff");
770     assert(color("ffff").toHex == "#ffffff");
771     assert(color("ffffff").toHex == "#ffffff");
772     assert(color("ffffffff").toHex == "#ffffff");
773     assert(color("fafbfc").toHex == "#fafbfc");
774     assert(color("123").toHex == "#112233");
775 
776     // Alpha set
777     assert(color("c0fe").toHex == "#cc00ffee");
778     assert(color("1234").toHex == "#11223344");
779     assert(color("0000").toHex == "#00000000");
780     assert(color("12345678").toHex == "#12345678");
781 
782 }
783 
784 /// Create a color from hex code.
785 Color color(string hexCode)() {
786 
787     return color(hexCode);
788 
789 }
790 
791 /// ditto
792 Color color(string hexCode) pure {
793 
794     import std.string : chompPrefix;
795     import std.format : format, formattedRead;
796 
797     // Remove the # if there is any
798     const hex = hexCode.chompPrefix("#");
799 
800     Color result;
801     result.a = 0xff;
802 
803     switch (hex.length) {
804 
805         // 4 digit RGBA
806         case 4:
807             formattedRead!"%x"(hex[3..4], result.a);
808             result.a *= 17;
809 
810             // Parse the rest like RGB
811             goto case;
812 
813         // 3 digit RGB
814         case 3:
815             formattedRead!"%x"(hex[0..1], result.r);
816             formattedRead!"%x"(hex[1..2], result.g);
817             formattedRead!"%x"(hex[2..3], result.b);
818             result.r *= 17;
819             result.g *= 17;
820             result.b *= 17;
821             break;
822 
823         // 8 digit RGBA
824         case 8:
825             formattedRead!"%x"(hex[6..8], result.a);
826             goto case;
827 
828         // 6 digit RGB
829         case 6:
830             formattedRead!"%x"(hex[0..2], result.r);
831             formattedRead!"%x"(hex[2..4], result.g);
832             formattedRead!"%x"(hex[4..6], result.b);
833             break;
834 
835         default:
836             assert(false, "Invalid hex code length");
837 
838     }
839 
840     return result;
841 
842 }
843 
844 unittest {
845 
846     import std.exception;
847 
848     assert(color!"#123" == Color(0x11, 0x22, 0x33, 0xff));
849     assert(color!"#1234" == Color(0x11, 0x22, 0x33, 0x44));
850     assert(color!"1234" == Color(0x11, 0x22, 0x33, 0x44));
851     assert(color!"123456" == Color(0x12, 0x34, 0x56, 0xff));
852     assert(color!"2a5592f0" == Color(0x2a, 0x55, 0x92, 0xf0));
853 
854     assertThrown(color!"ag5");
855 
856 }
857 
858 /// Set the alpha channel for the given color, as a float.
859 Color setAlpha(Color color, float alpha) {
860 
861     import std.algorithm : clamp;
862 
863     color.a = cast(ubyte) clamp(ubyte.max * alpha, 0, ubyte.max);
864     return color;
865 
866 }
867 
868 /// Blend two colors together; apply `top` on top of the `bottom` color. If `top` has maximum alpha, returns `top`. If
869 /// alpha is zero, returns `bottom`.
870 Color alphaBlend(Color bottom, Color top) {
871 
872     auto topA = cast(float) top.a / ubyte.max;
873     auto bottomA = (1 - topA) * cast(float) bottom.a / ubyte.max;
874 
875     return Color(
876         cast(ubyte) (bottom.r * bottomA + top.r * topA),
877         cast(ubyte) (bottom.g * bottomA + top.g * topA),
878         cast(ubyte) (bottom.b * bottomA + top.b * topA),
879         cast(ubyte) (bottom.a * bottomA + top.a * topA),
880     );
881 
882 }
883 
884 version (Have_raylib_d) {
885 
886     import raylib;
887 
888     debug (Fluid_BuildMessages) {
889         pragma(msg, "Fluid: Using Raylib 5 as the default backend");
890     }
891 
892     static this() {
893 
894         defaultFluidBackend = new Raylib5Backend;
895 
896     }
897 
898     alias Rectangle = raylib.Rectangle;
899     alias Vector2 = raylib.Vector2;
900     alias Color = raylib.Color;
901 
902 }
903 
904 else {
905 
906     debug (Fluid_BuildMessages) {
907         pragma(msg, "Fluid: No built-in backend in use");
908     }
909 
910     struct Vector2 {
911 
912         float x = 0;
913         float y = 0;
914 
915         mixin Linear;
916 
917     }
918 
919     struct Rectangle {
920 
921         float x, y;
922         float width, height;
923 
924         alias w = width;
925         alias h = height;
926 
927     }
928 
929     struct Color {
930 
931         ubyte r, g, b, a;
932 
933     }
934 
935     /// `mixin Linear` taken from [raylib-d](https://github.com/schveiguy/raylib-d), reformatted and without Rotor3
936     /// support.
937     ///
938     /// Licensed under the [z-lib license](https://github.com/schveiguy/raylib-d/blob/master/LICENSE).
939     private mixin template Linear() {
940 
941         private static alias T = typeof(this);
942         private import std.traits : FieldNameTuple;
943 
944         static T zero() {
945 
946             enum fragment = {
947                 string result;
948                 static foreach(i; 0 .. T.tupleof.length)
949                     result ~= "0,";
950                 return result;
951             }();
952 
953             return mixin("T(", fragment, ")");
954         }
955 
956         static T one() {
957 
958             enum fragment = {
959                 string result;
960                 static foreach(i; 0 .. T.tupleof.length)
961                     result ~= "1,";
962                 return result;
963             }();
964             return mixin("T(", fragment, ")");
965 
966         }
967 
968         inout T opUnary(string op)() if (op == "+" || op == "-") {
969 
970             enum fragment = {
971                 string result;
972                 static foreach(fn; FieldNameTuple!T)
973                     result ~= op ~ fn ~ ",";
974                 return result;
975             }();
976             return mixin("T(", fragment, ")");
977 
978         }
979 
980         inout T opBinary(string op)(inout T rhs) if (op == "+" || op == "-") {
981 
982             enum fragment = {
983                 string result;
984                 foreach(fn; FieldNameTuple!T)
985                     result ~= fn ~ op ~ "rhs." ~ fn ~ ",";
986                 return result;
987             }();
988             return mixin("T(", fragment, ")");
989 
990         }
991 
992         ref T opOpAssign(string op)(inout T rhs) if (op == "+" || op == "-") {
993 
994             foreach (field; FieldNameTuple!T)
995                 mixin(field, op,  "= rhs.", field, ";");
996 
997             return this;
998 
999         }
1000 
1001         inout T opBinary(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op ==  "/") {
1002 
1003             enum fragment = {
1004                 string result;
1005                 foreach(fn; FieldNameTuple!T)
1006                     result ~= fn ~ op ~ "rhs,";
1007                 return result;
1008             }();
1009             return mixin("T(", fragment, ")");
1010 
1011         }
1012 
1013         inout T opBinaryRight(string op)(inout float lhs) if (op == "+" || op == "-" || op == "*" || op ==  "/") {
1014 
1015             enum fragment = {
1016                 string result;
1017                 foreach(fn; FieldNameTuple!T)
1018                     result ~= "lhs" ~ op ~ fn ~ ",";
1019                 return result;
1020             }();
1021             return mixin("T(", fragment, ")");
1022 
1023         }
1024 
1025         ref T opOpAssign(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op ==  "/") {
1026 
1027             foreach (field; FieldNameTuple!T)
1028                 mixin(field, op, "= rhs;");
1029             return this;
1030 
1031         }
1032     }
1033 
1034 }