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