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.datetime;
11 import std.algorithm;
12 
13 public import fluid.backend.raylib5;
14 public import fluid.backend.headless;
15 
16 version (Have_raylib_d) public static import raylib;
17 
18 
19 @safe:
20 
21 
22 alias VoidDelegate = void delegate() @safe;
23 
24 FluidBackend defaultFluidBackend();
25 
26 /// `FluidBackend` is an interface making it possible to bind Fluid to a library other than Raylib.
27 ///
28 /// The default unit in graphical space is a **pixel** (`px`), here defined as **1/96 of an inch**. This is unless
29 /// stated otherwise, as in `Texture`.
30 ///
31 /// Warning: Backend API is unstable and functions may be added or removed with no prior warning.
32 interface FluidBackend {
33 
34     /// Get system's double click time.
35     final Duration doubleClickTime() const {
36 
37         // TODO This should be overridable
38 
39         return 500.msecs;
40 
41     }
42 
43     /// Check if the given mouse button has just been pressed/released or, if it's held down or not (up).
44     bool isPressed(MouseButton) const;
45     bool isReleased(MouseButton) const;
46     bool isDown(MouseButton) const;
47     bool isUp(MouseButton) const;
48 
49     /// Check if the given keyboard key has just been pressed/released or, if it's held down or not (up).
50     bool isPressed(KeyboardKey) const;
51     bool isReleased(KeyboardKey) const;
52     bool isDown(KeyboardKey) const;
53     bool isUp(KeyboardKey) const;
54 
55     /// If true, the given keyboard key has been virtually pressed again, through a long-press.
56     bool isRepeated(KeyboardKey) const;
57 
58     /// Get next queued character from user's input. The queue should be cleared every frame. Return null if no
59     /// character was pressed.
60     dchar inputCharacter();
61 
62     /// Check if the given gamepad button has been pressed/released or, if it's held down or not (up) on any of the
63     /// connected gamepads.
64     ///
65     /// Returns: 0 if the event isn't taking place on any controller, or number of the controller.
66     int isPressed(GamepadButton button) const;
67     int isReleased(GamepadButton button) const;
68     int isDown(GamepadButton button) const;
69     int isUp(GamepadButton button) const;
70 
71     /// If true, the given gamepad button has been virtually pressed again, through a long-press.
72     ///
73     /// Returns: 0 if no controller had a button repeat this frame, or number of the controller.
74     int isRepeated(GamepadButton button) const;
75 
76     /// Get/set mouse position
77     Vector2 mousePosition(Vector2);
78     Vector2 mousePosition() const;
79 
80     /// Get scroll value on both axes.
81     Vector2 scroll() const;
82 
83     /// Get or set system clipboard value.
84     string clipboard(string);
85     string clipboard() const;
86 
87     /// Get time elapsed since last frame in seconds.
88     float deltaTime() const;
89 
90     /// True if the user has just resized the window.
91     bool hasJustResized() const;
92 
93     /// Get or set the size of the window.
94     Vector2 windowSize(Vector2);
95     Vector2 windowSize() const;  /// ditto
96 
97     /// Set scale to apply to whatever is drawn next.
98     ///
99     /// Suggested implementation is to increase return value of `dpi`.
100     float scale() const;
101 
102     /// ditto
103     float scale(float);
104 
105     /// Get horizontal and vertical DPI of the window.
106     Vector2 dpi() const;
107 
108     /// Get the DPI value for the window as a scale relative to 96 DPI.
109     final Vector2 hidpiScale() const {
110 
111         const dpi = this.dpi;
112         return Vector2(dpi.x / 96f, dpi.y / 96f);
113 
114     }
115 
116     /// Set area within the window items will be drawn to; any pixel drawn outside will be discarded.
117     Rectangle area(Rectangle rect);
118     Rectangle area() const;
119 
120     /// Restore the capability to draw anywhere in the window.
121     void restoreArea();
122 
123     /// Get or set mouse cursor icon.
124     FluidMouseCursor mouseCursor(FluidMouseCursor);
125     FluidMouseCursor mouseCursor() const;
126 
127     /// Texture reaper used by this backend. May be null.
128     ///
129     /// Highly recommended for OpenGL-based backends.
130     TextureReaper* reaper() return scope;
131 
132     /// Load a texture from memory or file.
133     Texture loadTexture(Image image) @system;
134     Texture loadTexture(string filename) @system;
135 
136     /// Update a texture from an image. The texture must be valid and must be of the same size and format as the image.
137     void updateTexture(Texture texture, Image image) @system
138     in (texture.format == image.format)
139     in (texture.width == image.width)
140     in (texture.height == image.height);
141 
142     /// Destroy a texture created by this backend. Always use `texture.destroy()` to ensure thread safety and invoking
143     /// the correct backend.
144     protected void unloadTexture(uint id) @system;
145 
146     /// ditto
147     final void unloadTexture(Texture texture) @system {
148 
149         unloadTexture(texture.id);
150 
151     }
152 
153     /// Set tint for all newly drawn shapes. The input color for every shape should be multiplied by this color.
154     Color tint(Color);
155 
156     /// Get current tint color.
157     Color tint() const;
158 
159     /// Draw a line.
160     void drawLine(Vector2 start, Vector2 end, Color color);
161 
162     /// Draw a triangle, consisting of 3 vertices with counter-clockwise winding.
163     void drawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color);
164 
165     /// Draw a circle.
166     void drawCircle(Vector2 center, float radius, Color color);
167 
168     /// Draw a circle, but outline only.
169     void drawCircleOutline(Vector2 center, float radius, Color color);
170 
171     /// Draw a rectangle.
172     void drawRectangle(Rectangle rectangle, Color color);
173 
174     /// Draw a texture.
175     void drawTexture(Texture texture, Rectangle rectangle, Color tint)
176     in (texture.backend is this, "Given texture comes from a different backend");
177 
178     /// Draw a texture, but ensure it aligns with pixel boundaries, recommended for text.
179     void drawTextureAlign(Texture texture, Rectangle rectangle, Color tint)
180     in (texture.backend is this, "Given texture comes from a different backend");
181 
182 }
183 
184 /// Struct that maintains a registry of all allocated textures. It's used to finalize textures once they have been
185 /// marked for destruction. This makes it possible to mark them from any thread, while the reaper runs only on the main
186 /// thread, ensuring thread safety in OpenGL backends.
187 struct TextureReaper {
188 
189     /// Number of cycles between runs of the reaper.
190     int period = 60 * 5;
191 
192     int cycleAccumulator;
193 
194     @system shared(TextureTombstone)*[uint] textures;
195 
196     @disable this(ref TextureReaper);
197     @disable this(this);
198 
199     ~this() @trusted {
200 
201         destroyAll();
202 
203     }
204 
205     /// Create a tombstone.
206     shared(TextureTombstone)* makeTombstone(FluidBackend backend, uint textureID) @trusted {
207 
208         return textures[textureID] = TextureTombstone.make(backend);
209 
210     }
211 
212     /// Count number of cycles since last collection and collect if configured period has passed.
213     void check() {
214 
215         // Count cycles
216         if (++cycleAccumulator >= period) {
217 
218             // Run collection
219             collect();
220 
221         }
222 
223     }
224 
225     /// Collect all destroyed textures immediately.
226     void collect() @trusted {
227 
228         // Reset the cycle accumulator
229         cycleAccumulator = 0;
230 
231         // Find all destroyed textures
232         foreach (id, tombstone; textures) {
233 
234             if (!tombstone.isDestroyed) continue;
235 
236             auto backend = cast() tombstone.backend;
237 
238             // Unload the texture
239             backend.unloadTexture(id);
240 
241             // Disown the tombstone and remove it from the registry
242             tombstone.markDisowned();
243             textures.remove(id);
244 
245         }
246 
247     }
248 
249     /// Destroy all textures.
250     void destroyAll() @system {
251 
252         cycleAccumulator = 0;
253         scope (exit) textures.clear();
254 
255         // Find all textures
256         foreach (id, tombstone; textures) {
257 
258             auto backend = cast() tombstone.backend;
259 
260             // Unload the texture, even if it wasn't marked for deletion
261             backend.unloadTexture(id);
262             // TODO Should this be done? The destructor may be called from the GC. Maybe check if it was?
263             //      Test this!
264 
265             // Disown all textures
266             tombstone.markDisowned();
267 
268         }
269 
270     }
271 
272 }
273 
274 /// Tombstones are used to ensure textures are freed on the same thread they have been created on.
275 ///
276 /// Tombstones are kept alive until the texture is explicitly destroyed and then finalized (disowned) from the main
277 /// thread by a periodically-running `TextureReaper`. This is necessary to make Fluid safe in multithreaded
278 /// environments.
279 shared struct TextureTombstone {
280 
281     import core.memory;
282     import core.atomic;
283     import core.stdc.stdlib;
284 
285     /// Backend that created this texture.
286     private FluidBackend _backend;
287 
288     private int _references = 1;
289     private bool _disowned;
290 
291     @disable this(this);
292 
293     static TextureTombstone* make(FluidBackend backend) @system {
294 
295         import core.exception;
296 
297         // Allocate the tombstone
298         auto data = malloc(TextureTombstone.sizeof);
299         if (data is null) throw new OutOfMemoryError("Failed to allocate a tombstone");
300 
301         // Initialize the tombstone
302         shared tombstone = cast(shared TextureTombstone*) data;
303         *tombstone = TextureTombstone.init;
304         tombstone._backend = cast(shared) backend;
305 
306         assert(tombstone.references == 1);
307 
308         // Make sure the backend isn't freed while the tombstone is alive
309         GC.addRoot(cast(void*) backend);
310 
311         return tombstone;
312 
313     }
314 
315     /// Check if a request for destruction has been made for the texture.
316     bool isDestroyed() @system {
317         return _references.atomicLoad == 0;
318     }
319 
320     /// Check if the texture has been disowned by the backend. A disowned tombstone refers to a texture that has been
321     /// freed.
322     private bool isDisowned() @system {
323         return _disowned.atomicLoad;
324     }
325 
326     /// Get number of references to this tombstone.
327     private int references() @system {
328         return _references.atomicLoad;
329     }
330 
331     /// Get the backend owning this texture.
332     inout(shared FluidBackend) backend() inout {
333         return _backend;
334     }
335 
336     /// Mark the texture as destroyed.
337     void markDestroyed() @system {
338 
339         assert(!isDisowned || !isDestroyed, "Texture: Double destroy()");
340 
341         _references.atomicFetchSub(1);
342         tryDestroy();
343 
344     }
345 
346     /// Mark the texture as disowned.
347     private void markDisowned() @system {
348 
349         assert(!isDisowned || !isDestroyed);
350 
351         _disowned.atomicStore(true);
352         tryDestroy();
353 
354     }
355 
356     /// Mark the texture as copied.
357     private void markCopied() @system {
358 
359         _references.atomicFetchAdd(1);
360 
361     }
362 
363     /// As soon as the texture is both marked for destruction and disowned, the tombstone controlling its life is
364     /// destroyed.
365     ///
366     /// There are two relevant scenarios:
367     ///
368     /// * The texture is marked for destruction via a tombstone, then finalized from the main thread and disowned.
369     /// * The texture is finalized after the backend (for example, if they are both destroyed during the same GC
370     ///   collection). The backend disowns and frees the texture. The tombstone, however, remains alive to
371     ///   witness marking the texture as deleted.
372     ///
373     /// In both scenarios, this behavior ensures the tombstone will be freed.
374     private void tryDestroy() @system {
375 
376         // Destroyed and disowned
377         if (isDestroyed && isDisowned) {
378 
379             GC.removeRoot(cast(void*) _backend);
380             free(cast(void*) &this);
381 
382         }
383 
384     }
385 
386 }
387 
388 @system
389 unittest {
390 
391     // This unittest checks if textures will be correctly destroyed, even if the destruction call comes from another
392     // thread.
393 
394     import std.concurrency;
395     import fluid.space;
396     import fluid.image_view;
397 
398     auto io = new HeadlessBackend;
399     auto image = imageView("logo.png");
400     auto root = vspace(image);
401 
402     // Draw the frame once to let everything load
403     root.io = io;
404     root.draw();
405 
406     // Tune the reaper to run every frame
407     io.reaper.period = 1;
408 
409     // Get the texture
410     auto texture = image.release();
411     auto textureID = texture.id;
412     auto tombstone = texture.tombstone;
413 
414     // Texture should be allocated and assigned a tombstone
415     assert(texture.backend is io);
416     assert(!texture.tombstone.isDestroyed);
417     assert(io.isTextureValid(texture));
418 
419     // Destroy the texture on another thread
420     spawn((shared Texture sharedTexture) {
421 
422         auto texture = cast() sharedTexture;
423         texture.destroy();
424         ownerTid.send(true);
425 
426     }, cast(shared) texture);
427 
428     // Wait for confirmation
429     receiveOnly!bool;
430 
431     // The texture should be marked for deletion but remain alive
432     assert(texture.tombstone.isDestroyed);
433     assert(io.isTextureValid(texture));
434 
435     // Draw a frame, during which the reaper should destroy the texture
436     io.nextFrame;
437     root.children = [];
438     root.updateSize();
439     root.draw();
440 
441     assert(!io.isTextureValid(texture));
442     // There is no way to test if the tombstone has been freed
443 
444 }
445 
446 @system
447 unittest {
448 
449     // This unittest checks if tombstones work correctly even if the backend is destroyed before the texture.
450 
451     import std.concurrency;
452     import core.atomic;
453     import fluid.image_view;
454 
455     auto io = new HeadlessBackend;
456     auto root = imageView("logo.png");
457 
458     // Load the texture and draw
459     root.io = io;
460     root.draw();
461 
462     // Destroy the backend
463     destroy(io);
464 
465     auto texture = root.release();
466 
467     // The texture should have been automatically freed, but not marked for destruction
468     assert(!texture.tombstone.isDestroyed);
469     assert(texture.tombstone._disowned.atomicLoad);
470 
471     // Now, destroy the image
472     // If this operation succeeds, we're good
473     destroy(root);
474     // There is no way to test if the tombstone and texture have truly been freed
475 
476 }
477 
478 struct FluidMouseCursor {
479 
480     enum SystemCursors {
481 
482         systemDefault,     // Default system cursor.
483         none,              // No pointer.
484         pointer,           // Pointer indicating a link or button, typically a pointing hand. 👆
485         crosshair,         // Cross cursor, often indicating selection inside images.
486         text,              // Vertical beam indicating selectable text.
487         allScroll,         // Omnidirectional scroll, content can be scrolled in any direction (panned).
488         resizeEW,          // Cursor indicating the content underneath can be resized horizontally.
489         resizeNS,          // Cursor indicating the content underneath can be resized vertically.
490         resizeNESW,        // Diagonal resize cursor, top-right + bottom-left.
491         resizeNWSE,        // Diagonal resize cursor, top-left + bottom-right.
492         notAllowed,        // Indicates a forbidden action.
493 
494     }
495 
496     enum {
497 
498         systemDefault = FluidMouseCursor(SystemCursors.systemDefault),
499         none          = FluidMouseCursor(SystemCursors.none),
500         pointer       = FluidMouseCursor(SystemCursors.pointer),
501         crosshair     = FluidMouseCursor(SystemCursors.crosshair),
502         text          = FluidMouseCursor(SystemCursors.text),
503         allScroll     = FluidMouseCursor(SystemCursors.allScroll),
504         resizeEW      = FluidMouseCursor(SystemCursors.resizeEW),
505         resizeNS      = FluidMouseCursor(SystemCursors.resizeNS),
506         resizeNESW    = FluidMouseCursor(SystemCursors.resizeNESW),
507         resizeNWSE    = FluidMouseCursor(SystemCursors.resizeNWSE),
508         notAllowed    = FluidMouseCursor(SystemCursors.notAllowed),
509 
510     }
511 
512     /// Use a system-provided cursor.
513     SystemCursors system;
514     // TODO user-provided cursor image
515 
516 }
517 
518 enum MouseButton {
519     none,
520     left,         // Left (primary) mouse button.
521     right,        // Right (secondary) mouse button.
522     middle,       // Middle mouse button.
523     extra1,       // Additional mouse button.
524     extra2,       // ditto.
525     forward,      // Mouse button going forward in browser history.
526     back,         // Mouse button going back in browser history.
527 
528     primary = left,
529     secondary = right,
530 
531 }
532 
533 enum GamepadButton {
534 
535     none,                // No such button
536     dpadUp,              // Dpad up button.
537     dpadRight,           // Dpad right button
538     dpadDown,            // Dpad down button
539     dpadLeft,            // Dpad left button
540     triangle,            // Triangle (PS) or Y (Xbox)
541     circle,              // Circle (PS) or B (Xbox)
542     cross,               // Cross (PS) or A (Xbox)
543     square,              // Square (PS) or X (Xbox)
544     leftButton,          // Left button behind the controlller (LB).
545     leftTrigger,         // Left trigger (LT).
546     rightButton,         // Right button behind the controller (RB).
547     rightTrigger,        // Right trigger (RT)
548     select,              // "Select" button.
549     vendor,              // Button with the vendor logo.
550     start,               // "Start" button.
551     leftStick,           // Left joystick press.
552     rightStick,          // Right joystick press.
553 
554     y = triangle,
555     x = square,
556     a = cross,
557     b = circle,
558 
559 }
560 
561 enum GamepadAxis {
562 
563     leftX,         // Left joystick, X axis.
564     leftY,         // Left joystick, Y axis.
565     rightX,        // Right joystick, X axis.
566     rightY,        // Right joystick, Y axis.
567     leftTrigger,   // Analog input for the left trigger.
568     rightTrigger,  // Analog input for the right trigger.
569 
570 }
571 
572 enum KeyboardKey {
573     none               = 0,           // No key pressed
574     apostrophe         = 39,          // '
575     comma              = 44,          // ,
576     dash               = comma,
577     minus              = 45,          // -
578     period             = 46,          // .
579     slash              = 47,          // /
580     digit0             = 48,          // 0
581     digit1             = 49,          // 1
582     digit2             = 50,          // 2
583     digit3             = 51,          // 3
584     digit4             = 52,          // 4
585     digit5             = 53,          // 5
586     digit6             = 54,          // 6
587     digit7             = 55,          // 7
588     digit8             = 56,          // 8
589     digit9             = 57,          // 9
590     semicolon          = 59,          // ;
591     equal              = 61,          // =
592     a                  = 65,          // A | a
593     b                  = 66,          // B | b
594     c                  = 67,          // C | c
595     d                  = 68,          // D | d
596     e                  = 69,          // E | e
597     f                  = 70,          // F | f
598     g                  = 71,          // G | g
599     h                  = 72,          // H | h
600     i                  = 73,          // I | i
601     j                  = 74,          // J | j
602     k                  = 75,          // K | k
603     l                  = 76,          // L | l
604     m                  = 77,          // M | m
605     n                  = 78,          // N | n
606     o                  = 79,          // O | o
607     p                  = 80,          // P | p
608     q                  = 81,          // Q | q
609     r                  = 82,          // R | r
610     s                  = 83,          // S | s
611     t                  = 84,          // T | t
612     u                  = 85,          // U | u
613     v                  = 86,          // V | v
614     w                  = 87,          // W | w
615     x                  = 88,          // X | x
616     y                  = 89,          // Y | y
617     z                  = 90,          // Z | z
618     leftBracket        = 91,          // [
619     backslash          = 92,          // '\'
620     rightBracket       = 93,          // ]
621     backtick           = 96,          // `
622     grave              = backtick,
623     space              = 32,          // Space
624     escape             = 256,         // Esc
625     esc                = escape,
626     enter              = 257,         // Enter
627     tab                = 258,         // Tab
628     backspace          = 259,         // Backspace
629     insert             = 260,         // Ins
630     del                = 261,         // Del
631     delete_            = del,
632     right              = 262,         // Cursor right
633     left               = 263,         // Cursor left
634     down               = 264,         // Cursor down
635     up                 = 265,         // Cursor up
636     pageUp             = 266,         // Page up
637     pageDown           = 267,         // Page down
638     home               = 268,         // Home
639     end                = 269,         // End
640     capsLock           = 280,         // Caps lock
641     scrollLock         = 281,         // Scroll down
642     numLock            = 282,         // Num lock
643     printScreen        = 283,         // Print screen
644     pause              = 284,         // Pause
645     f1                 = 290,         // F1
646     f2                 = 291,         // F2
647     f3                 = 292,         // F3
648     f4                 = 293,         // F4
649     f5                 = 294,         // F5
650     f6                 = 295,         // F6
651     f7                 = 296,         // F7
652     f8                 = 297,         // F8
653     f9                 = 298,         // F9
654     f10                = 299,         // F10
655     f11                = 300,         // F11
656     f12                = 301,         // F12
657     leftShift          = 340,         // Shift left
658     leftControl        = 341,         // Control left
659     leftAlt            = 342,         // Alt left
660     leftSuper          = 343,         // Super left
661     leftCommand        = leftSuper,   // Command left
662     leftOption         = leftAlt,     // Option left
663     rightShift         = 344,         // Shift right
664     rightControl       = 345,         // Control right
665     rightAlt           = 346,         // Alt right
666     rightSuper         = 347,         // Super right
667     rightCommand       = rightSuper,  // Command right
668     rightOption        = rightAlt,    // Option right
669     contextMenu        = 348,         // Context menu
670     keypad0            = 320,         // Keypad 0
671     keypad1            = 321,         // Keypad 1
672     keypad2            = 322,         // Keypad 2
673     keypad3            = 323,         // Keypad 3
674     keypad4            = 324,         // Keypad 4
675     keypad5            = 325,         // Keypad 5
676     keypad6            = 326,         // Keypad 6
677     keypad7            = 327,         // Keypad 7
678     keypad8            = 328,         // Keypad 8
679     keypad9            = 329,         // Keypad 9
680     keypadDecimal      = 330,         // Keypad .
681     keypadDivide       = 331,         // Keypad /
682     keypadMultiply     = 332,         // Keypad *
683     keypadSubtract     = 333,         // Keypad -
684     keypadSum          = 334,         // Keypad +
685     keypadEnter        = 335,         // Keypad Enter
686     keypadEqual        = 336,         // Keypad =
687     androidBack        = 4,           // Android back button
688     androidMenu        = 82,          // Android menu button
689     volumeUp           = 24,          // Android volume up button
690     volumeDown         = 25           // Android volume down button
691     // Function keys for volume?
692 
693 }
694 
695 /// Generate an image filled with a given color.
696 ///
697 /// Note: Image data is GC-allocated. Make sure to keep a reference alive when passing to the backend. Do not use
698 /// `UnloadImage` if using Raylib.
699 static Image generateColorImage(int width, int height, Color color) {
700 
701     // Generate each pixel
702     auto data = new Color[width * height];
703     data[] = color;
704 
705     return Image(data, width, height);
706 
707 }
708 
709 /// Generate a paletted image filled with 0-index pixels of given alpha value.
710 static Image generatePalettedImage(int width, int height, ubyte alpha) {
711 
712     auto data = new PalettedColor[width * height];
713     data[] = PalettedColor(0, alpha);
714 
715     return Image(data, width, height);
716 
717 }
718 
719 /// Generate an alpha mask filled with given value.
720 static Image generateAlphaMask(int width, int height, ubyte value) {
721 
722     auto data = new ubyte[width * height];
723     data[] = value;
724 
725     return Image(data, width, height);
726 
727 }
728 
729 /// A paletted pixel, for use in `palettedAlpha` images; Stores images using an index into a palette, along with an
730 /// alpha value.
731 struct PalettedColor {
732 
733     ubyte index;
734     ubyte alpha;
735 
736 }
737 
738 /// Image available to the CPU.
739 struct Image {
740 
741     enum Format {
742 
743         /// RGBA, 8 bit per channel (32 bits per pixel).
744         rgba,
745 
746         /// Paletted image with alpha channel (16 bits per pixel)
747         palettedAlpha,
748 
749         /// Alpha-only image/mask (8 bits per pixel).
750         alpha,
751 
752     }
753 
754     Format format;
755 
756     /// Image data. Make sure to access data relevant to the current format.
757     ///
758     /// Each format has associated data storage. `rgba` has `rgbaPixels`, `palettedAlpha` has `palettedAlphaPixels` and
759     /// `alpha` has `alphaPixels`.
760     Color[] rgbaPixels;
761 
762     /// ditto
763     PalettedColor[] palettedAlphaPixels;
764 
765     /// ditto
766     ubyte[] alphaPixels;
767 
768     /// Palette data, if relevant. Access into an invalid palette index is equivalent to full white.
769     ///
770     /// For `palettedAlpha` images (and `PalettedColor` in general), the alpha value of each color in the palette is
771     /// ignored.
772     Color[] palette;
773 
774     int width, height;
775 
776     /// Create an RGBA image.
777     this(Color[] rgbaPixels, int width, int height) {
778 
779         this.format = Format.rgba;
780         this.rgbaPixels = rgbaPixels;
781         this.width = width;
782         this.height = height;
783 
784     }
785 
786     /// Create a paletted image.
787     this(PalettedColor[] palettedAlphaPixels, int width, int height) {
788 
789         this.format = Format.palettedAlpha;
790         this.palettedAlphaPixels = palettedAlphaPixels;
791         this.width = width;
792         this.height = height;
793 
794     }
795 
796     /// Create an alpha mask.
797     this(ubyte[] alphaPixels, int width, int height) {
798 
799         this.format = Format.alpha;
800         this.alphaPixels = alphaPixels;
801         this.width = width;
802         this.height = height;
803 
804     }
805 
806     Vector2 size() const {
807 
808         return Vector2(width, height);
809 
810     }
811 
812     int area() const {
813 
814         return width * height;
815 
816     }
817 
818     /// Get a palette entry at given index.
819     Color paletteColor(PalettedColor pixel) const {
820 
821         // Valid index, return the color; Set alpha to match the pixel
822         if (pixel.index < palette.length)
823             return palette[pixel.index].setAlpha(pixel.alpha);
824 
825         // Invalid index, return white
826         else
827             return Color(0xff, 0xff, 0xff, pixel.alpha);
828 
829     }
830 
831     /// Get data of the image in raw form.
832     inout(void)[] data() inout {
833 
834         final switch (format) {
835 
836             case Format.rgba:
837                 return rgbaPixels;
838             case Format.palettedAlpha:
839                 return palettedAlphaPixels;
840             case Format.alpha:
841                 return alphaPixels;
842 
843         }
844 
845     }
846 
847     /// Get color at given position. Position must be in image bounds.
848     Color get(int x, int y) const {
849 
850         const index = y * width + x;
851 
852         final switch (format) {
853 
854             case Format.rgba:
855                 return rgbaPixels[index];
856             case Format.palettedAlpha:
857                 return paletteColor(palettedAlphaPixels[index]);
858             case Format.alpha:
859                 return Color(0xff, 0xff, 0xff, alphaPixels[index]);
860 
861         }
862 
863     }
864 
865     unittest {
866 
867         auto colors = [
868             PalettedColor(0, ubyte(0)),
869             PalettedColor(1, ubyte(127)),
870             PalettedColor(2, ubyte(127)),
871             PalettedColor(3, ubyte(255)),
872         ];
873 
874         auto image = Image(colors, 2, 2);
875         image.palette = [
876             Color(0, 0, 0, 255),
877             Color(255, 0, 0, 255),
878             Color(0, 255, 0, 255),
879             Color(0, 0, 255, 255),
880         ];
881 
882         assert(image.get(0, 0) == Color(0, 0, 0, 0));
883         assert(image.get(1, 0) == Color(255, 0, 0, 127));
884         assert(image.get(0, 1) == Color(0, 255, 0, 127));
885         assert(image.get(1, 1) == Color(0, 0, 255, 255));
886 
887     }
888 
889     /// Set color at given position. Does nothing if position is out of bounds.
890     ///
891     /// The `set(int, int, Color)` overload only supports true color images. For paletted images, use
892     /// `set(int, int, PalettedColor)`. The latter can also be used for building true color images using a palette, if
893     /// one is supplied in the image at the time.
894     void set(int x, int y, Color color) {
895 
896         if (x < 0 || y < 0) return;
897         if (x >= width || y >= height) return;
898 
899         const index = y * width + x;
900 
901         final switch (format) {
902 
903             case Format.rgba:
904                 rgbaPixels[index] = color;
905                 return;
906             case Format.palettedAlpha:
907                 assert(false, "Unsupported image format: Cannot `set` pixels by color in a paletted image.");
908             case Format.alpha:
909                 alphaPixels[index] = color.a;
910                 return;
911 
912         }
913 
914     }
915 
916     /// ditto
917     void set(int x, int y, PalettedColor entry) {
918 
919         if (x < 0 || y < 0) return;
920         if (x >= width || y >= height) return;
921 
922         const index = y * width + x;
923         const color = paletteColor(entry);
924 
925         final switch (format) {
926 
927             case Format.rgba:
928                 rgbaPixels[index] = color;
929                 return;
930             case Format.palettedAlpha:
931                 palettedAlphaPixels[index] = entry;
932                 return;
933             case Format.alpha:
934                 alphaPixels[index] = color.a;
935                 return;
936 
937         }
938 
939     }
940 
941     /// Clear the image, replacing every pixel with given color.
942     ///
943     /// The `clear(Color)` overload only supports true color images. For paletted images, use `clear(PalettedColor)`.
944     /// The latter can also be used for building true color images using a palette, if one is supplied in the image at
945     /// the time.
946     void clear(Color color) {
947 
948         final switch (format) {
949 
950             case Format.rgba:
951                 rgbaPixels[] = color;
952                 return;
953             case Format.palettedAlpha:
954                 assert(false, "Unsupported image format: Cannot `clear` by color in a paletted image.");
955             case Format.alpha:
956                 alphaPixels[] = color.a;
957                 return;
958 
959         }
960 
961     }
962 
963     /// ditto
964     void clear(PalettedColor entry) {
965 
966         const color = paletteColor(entry);
967 
968         final switch (format) {
969 
970             case Format.rgba:
971                 rgbaPixels[] = color;
972                 return;
973             case Format.palettedAlpha:
974                 palettedAlphaPixels[] = entry;
975                 return;
976             case Format.alpha:
977                 alphaPixels[] = color.a;
978                 return;
979 
980         }
981 
982     }
983 
984 }
985 
986 
987 /// Image or texture can be rendered by Fluid, for example, a texture stored in VRAM.
988 ///
989 /// Textures make use of manual memory management. See `TextureGC` for a GC-managed texture.
990 struct Texture {
991 
992     /// Tombstone for this texture
993     shared(TextureTombstone)* tombstone;
994 
995     /// Format of the texture.
996     Image.Format format;
997 
998     /// GPU/backend ID of the texture.
999     uint id;
1000 
1001     /// Width and height of the texture, **in dots**. The meaning of a dot is defined by `dpiX` and `dpiY`
1002     int width, height;
1003 
1004     /// Dots per inch for the X and Y axis. Defaults to 96, thus making a dot in the texture equivalent to a pixel.
1005     int dpiX = 96, dpiY = 96;
1006 
1007     /// If relevant, the texture is to use this palette.
1008     Color[] palette;
1009 
1010     bool opEquals(const Texture other) const {
1011         return id == other.id
1012             && width == other.width
1013             && height == other.height
1014             && dpiX == other.dpiX
1015             && dpiY == other.dpiY;
1016 
1017     }
1018 
1019     version (Have_raylib_d)
1020     void opAssign(raylib.Texture rayTexture) @system {
1021         this = rayTexture.toFluid();
1022     }
1023 
1024     /// Get the backend for this texture. Doesn't work after freeing the tombstone.
1025     inout(FluidBackend) backend() inout @trusted {
1026         return cast(inout FluidBackend) tombstone.backend;
1027     }
1028 
1029     /// DPI value of the texture.
1030     Vector2 dpi() const {
1031         return Vector2(dpiX, dpiY);
1032     }
1033 
1034     /// Get texture size as a vector.
1035     Vector2 canvasSize() const {
1036         return Vector2(width, height);
1037     }
1038 
1039     /// Get the size the texture will occupy within the viewport.
1040     Vector2 viewportSize() const {
1041         return Vector2(
1042             width * 96 / dpiX,
1043             height * 96 / dpiY
1044         );
1045     }
1046 
1047     /// Update the texture to match the given image.
1048     void update(Image image) @system {
1049 
1050         backend.updateTexture(this, image);
1051 
1052     }
1053 
1054     /// Draw this texture.
1055     void draw(Vector2 position, Color tint = Color(0xff, 0xff, 0xff, 0xff)) {
1056 
1057         auto rectangle = Rectangle(position.tupleof, viewportSize.tupleof);
1058 
1059         backend.drawTexture(this, rectangle, tint);
1060 
1061     }
1062 
1063     void draw(Rectangle rectangle, Color tint = Color(0xff, 0xff, 0xff, 0xff)) {
1064 
1065         backend.drawTexture(this, rectangle, tint);
1066 
1067     }
1068 
1069     /// Destroy this texture. This function is thread-safe.
1070     void destroy() @system {
1071 
1072         if (tombstone is null) return;
1073 
1074         tombstone.markDestroyed();
1075         tombstone = null;
1076         id = 0;
1077 
1078     }
1079 
1080 }
1081 
1082 /// Wrapper over `Texture` that automates destruction via GC or RAII.
1083 struct TextureGC {
1084 
1085     /// Underlying texture. Lifetime is bound to this struct.
1086     Texture texture;
1087 
1088     alias texture this;
1089 
1090     /// Load a texture from filename.
1091     this(FluidBackend backend, string filename) @trusted {
1092 
1093         this.texture = backend.loadTexture(filename);
1094 
1095     }
1096 
1097     /// Load a texture from image data.
1098     this(FluidBackend backend, Image data) @trusted {
1099 
1100         this.texture = backend.loadTexture(data);
1101 
1102     }
1103 
1104     /// Move constructor for TextureGC; increment the reference counter for the texture.
1105     ///
1106     /// While I originally did not intend to implement reference counting, it is necessary to make TextureGC work in
1107     /// dynamic arrays. Changing the size of the array will copy the contents without performing a proper move of the
1108     /// old items. The postblit is the only kind of move constructor that will be called in this case, and a copy
1109     /// constructor does not do its job.
1110     this(this) @trusted {
1111 
1112         if (tombstone)
1113         tombstone.markCopied();
1114 
1115     }
1116 
1117     @system
1118     unittest {
1119 
1120         import std.string;
1121 
1122         // This tests using TextureGC inside of a dynamic array, especially after resizing. See documentation for
1123         // the postblit above.
1124 
1125         // Test two variants:
1126         // * One, where we rely on the language to finalize the copied value
1127         // * And one, where we manually destroy the value
1128         foreach (explicitDestruction; [false, true]) {
1129 
1130             void makeCopy(TextureGC[] arr) {
1131 
1132                 // Create the copy
1133                 auto copy = arr;
1134 
1135                 assert(sameHead(arr, copy));
1136 
1137                 // Expand the array, creating another
1138                 copy.length = 1024;
1139 
1140                 assert(!sameHead(arr, copy));
1141 
1142                 // References to tombstones exist in both arrays now
1143                 assert(!copy[0].tombstone.isDestroyed);
1144                 assert(!arr[0].tombstone.isDestroyed);
1145 
1146                 // The copy should be marked as moved
1147                 assert(copy[0].tombstone.references == 2);
1148                 assert(arr[0].tombstone.references == 2);
1149 
1150                 // Destroy the tombstone
1151                 if (explicitDestruction) {
1152 
1153                     auto tombstone = copy[0].tombstone;
1154 
1155                     copy[0].destroy();
1156                     assert(tombstone.references == 1);
1157                     assert(!tombstone.isDestroyed);
1158 
1159                 }
1160 
1161                 // Forget about the copy
1162                 copy = null;
1163 
1164             }
1165 
1166             static void trashStack() {
1167 
1168                 import core.memory;
1169 
1170                 // Destroy the stack to get rid of any references to `copy`
1171                 ubyte[2048] garbage;
1172 
1173                 // Collect it, make sure the tombstone gets eaten
1174                 GC.collect();
1175 
1176             }
1177 
1178             auto io = new HeadlessBackend;
1179             auto image = generateColorImage(10, 10, color("#fff"));
1180             auto arr = [
1181                 TextureGC(io, image),
1182                 TextureGC.init,
1183             ];
1184 
1185             makeCopy(arr);
1186             trashStack();
1187 
1188             assert(!arr[0].tombstone.isDestroyed, "Tombstone of a live texture was destroyed after copying an array"
1189                 ~ format!" (explicitDestruction %s)"(explicitDestruction));
1190 
1191             io.reaper.collect();
1192 
1193             assert(io.isTextureValid(arr[0]));
1194             assert(!arr[0].tombstone.isDestroyed);
1195             assert(!arr[0].tombstone.isDisowned);
1196             assert(arr[0].tombstone.references == 1);
1197 
1198         }
1199 
1200     }
1201 
1202     @system
1203     unittest {
1204 
1205         auto io = new HeadlessBackend;
1206         auto image = generateColorImage(10, 10, color("#fff"));
1207         auto arr = [
1208             TextureGC(io, image),
1209             TextureGC.init,
1210         ];
1211         auto copy = arr.dup;
1212 
1213         assert(arr[0].tombstone.references == 2);
1214 
1215         io.reaper.collect();
1216 
1217         assert(io.isTextureValid(arr[0]));
1218 
1219     }
1220 
1221     ~this() @trusted {
1222 
1223         texture.destroy();
1224 
1225     }
1226 
1227     /// Release the texture, moving it to manual management.
1228     Texture release() @system {
1229 
1230         auto result = texture;
1231         texture = texture.init;
1232         return result;
1233 
1234     }
1235 
1236 }
1237 
1238 /// Get a hex code from color.
1239 string toHex(string prefix = "#")(Color color) {
1240 
1241     import std.format;
1242 
1243     // Full alpha, use a six digit code
1244     if (color.a == 0xff) {
1245 
1246         return format!(prefix ~ "%02x%02x%02x")(color.r, color.g, color.b);
1247 
1248     }
1249 
1250     // Include alpha otherwise
1251     else return format!(prefix ~ "%02x%02x%02x%02x")(color.tupleof);
1252 
1253 }
1254 
1255 unittest {
1256 
1257     // No relevant alpha
1258     assert(color("fff").toHex == "#ffffff");
1259     assert(color("ffff").toHex == "#ffffff");
1260     assert(color("ffffff").toHex == "#ffffff");
1261     assert(color("ffffffff").toHex == "#ffffff");
1262     assert(color("fafbfc").toHex == "#fafbfc");
1263     assert(color("123").toHex == "#112233");
1264 
1265     // Alpha set
1266     assert(color("c0fe").toHex == "#cc00ffee");
1267     assert(color("1234").toHex == "#11223344");
1268     assert(color("0000").toHex == "#00000000");
1269     assert(color("12345678").toHex == "#12345678");
1270 
1271 }
1272 
1273 /// Create a color from hex code.
1274 Color color(string hexCode)() {
1275 
1276     return color(hexCode);
1277 
1278 }
1279 
1280 /// ditto
1281 Color color(string hexCode) pure {
1282 
1283     import std.conv: to;
1284     import std.string : chompPrefix;
1285 
1286     // Remove the # if there is any
1287     const hex = hexCode.chompPrefix("#");
1288 
1289     Color result;
1290     result.a = 0xff;
1291 
1292     switch (hex.length) {
1293 
1294         // 4 digit RGBA
1295         case 4:
1296             result.a = hex[3..4].to!ubyte(16);
1297             result.a *= 17;
1298 
1299             // Parse the rest like RGB
1300             goto case;
1301 
1302         // 3 digit RGB
1303         case 3:
1304             result.r = hex[0..1].to!ubyte(16);
1305             result.g = hex[1..2].to!ubyte(16);
1306             result.b = hex[2..3].to!ubyte(16);
1307             result.r *= 17;
1308             result.g *= 17;
1309             result.b *= 17;
1310             break;
1311 
1312         // 8 digit RGBA
1313         case 8:
1314             result.a = hex[6..8].to!ubyte(16);
1315             goto case;
1316 
1317         // 6 digit RGB
1318         case 6:
1319             result.r = hex[0..2].to!ubyte(16);
1320             result.g = hex[2..4].to!ubyte(16);
1321             result.b = hex[4..6].to!ubyte(16);
1322             break;
1323 
1324         default:
1325             assert(false, "Invalid hex code length");
1326 
1327     }
1328 
1329     return result;
1330 
1331 }
1332 
1333 unittest {
1334 
1335     import std.exception;
1336 
1337     assert(color!"#123" == Color(0x11, 0x22, 0x33, 0xff));
1338     assert(color!"#1234" == Color(0x11, 0x22, 0x33, 0x44));
1339     assert(color!"1234" == Color(0x11, 0x22, 0x33, 0x44));
1340     assert(color!"123456" == Color(0x12, 0x34, 0x56, 0xff));
1341     assert(color!"2a5592f0" == Color(0x2a, 0x55, 0x92, 0xf0));
1342 
1343     assertThrown(color!"ag5");
1344 
1345 }
1346 
1347 /// Set the alpha channel for the given color, as a float.
1348 Color setAlpha(Color color, float alpha) {
1349 
1350     import std.algorithm : clamp;
1351 
1352     color.a = cast(ubyte) clamp(ubyte.max * alpha, 0, ubyte.max);
1353     return color;
1354 
1355 }
1356 
1357 Color setAlpha()(Color color, int alpha) {
1358 
1359     static assert(false, "Overload setAlpha(Color, int). Explicitly choose setAlpha(Color, float) (0...1 range) or "
1360         ~ "setAlpha(Color, ubyte) (0...255 range)");
1361 
1362 }
1363 
1364 /// Set the alpha channel for the given color, as a float.
1365 Color setAlpha(Color color, ubyte alpha) {
1366 
1367     color.a = alpha;
1368     return color;
1369 
1370 }
1371 
1372 /// Blend two colors together; apply `top` on top of the `bottom` color. If `top` has maximum alpha, returns `top`. If
1373 /// alpha is zero, returns `bottom`.
1374 ///
1375 /// BUG: This function is currently broken and returns incorrect results.
1376 Color alphaBlend(Color bottom, Color top) {
1377 
1378     auto topA = cast(float) top.a / ubyte.max;
1379     auto bottomA = (1 - topA) * cast(float) bottom.a / ubyte.max;
1380 
1381     return Color(
1382         cast(ubyte) (bottom.r * bottomA + top.r * topA),
1383         cast(ubyte) (bottom.g * bottomA + top.g * topA),
1384         cast(ubyte) (bottom.b * bottomA + top.b * topA),
1385         cast(ubyte) (bottom.a * bottomA + top.a * topA),
1386     );
1387 
1388 }
1389 
1390 /// Multiple color values.
1391 Color multiply(Color a, Color b) {
1392 
1393     return Color(
1394         cast(ubyte) (a.r * b.r / 255.0),
1395         cast(ubyte) (a.g * b.g / 255.0),
1396         cast(ubyte) (a.b * b.b / 255.0),
1397         cast(ubyte) (a.a * b.a / 255.0),
1398     );
1399 
1400 }
1401 
1402 unittest {
1403 
1404     assert(multiply(color!"#fff", color!"#a00") == color!"#a00");
1405     assert(multiply(color!"#1eff00", color!"#009bdd") == color!"#009b00");
1406     assert(multiply(color!"#aaaa", color!"#1234") == color!"#0b16222d");
1407 
1408 }
1409 
1410 version (unittest) {
1411 
1412     debug (Fluid_BuildMessages) {
1413         pragma(msg, "Fluid: Using headless as the default backend (unittest)");
1414     }
1415 
1416     FluidBackend defaultFluidBackend() {
1417 
1418         return new HeadlessBackend;
1419 
1420     }
1421 
1422 }
1423 
1424 else version (Have_raylib_d) {
1425 
1426     debug (Fluid_BuildMessages) {
1427         pragma(msg, "Fluid: Using Raylib 5 as the default backend");
1428     }
1429 
1430     FluidBackend defaultFluidBackend() {
1431 
1432         return new Raylib5Backend;
1433 
1434     }
1435 
1436 }
1437 
1438 else {
1439 
1440     debug (Fluid_BuildMessages) {
1441         pragma(msg, "Fluid: No built-in backend in use");
1442     }
1443 
1444     FluidBackend defaultFluidBackend() {
1445 
1446         return null;
1447 
1448     }
1449 
1450 }
1451 
1452 // Structures
1453 version (Have_raylib_d) {
1454 
1455     debug (Fluid_BuildMessages) {
1456         pragma(msg, "Fluid: Using Raylib core structures");
1457     }
1458 
1459     import raylib;
1460 
1461     alias Rectangle = raylib.Rectangle;
1462     alias Vector2 = raylib.Vector2;
1463     alias Color = raylib.Color;
1464 
1465 }
1466 
1467 else {
1468 
1469     struct Vector2 {
1470 
1471         float x = 0;
1472         float y = 0;
1473 
1474         mixin Linear;
1475 
1476     }
1477 
1478     struct Rectangle {
1479 
1480         float x, y;
1481         float width, height;
1482 
1483         alias w = width;
1484         alias h = height;
1485 
1486     }
1487 
1488     struct Color {
1489 
1490         ubyte r, g, b, a;
1491 
1492     }
1493 
1494     /// `mixin Linear` taken from [raylib-d](https://github.com/schveiguy/raylib-d), reformatted and without Rotor3
1495     /// support.
1496     ///
1497     /// Licensed under the [z-lib license](https://github.com/schveiguy/raylib-d/blob/master/LICENSE).
1498     private mixin template Linear() {
1499 
1500         private static alias T = typeof(this);
1501         private import std.traits : FieldNameTuple;
1502 
1503         static T zero() {
1504 
1505             enum fragment = {
1506                 string result;
1507                 static foreach(i; 0 .. T.tupleof.length)
1508                     result ~= "0,";
1509                 return result;
1510             }();
1511 
1512             return mixin("T(", fragment, ")");
1513         }
1514 
1515         static T one() {
1516 
1517             enum fragment = {
1518                 string result;
1519                 static foreach(i; 0 .. T.tupleof.length)
1520                     result ~= "1,";
1521                 return result;
1522             }();
1523             return mixin("T(", fragment, ")");
1524 
1525         }
1526 
1527         inout T opUnary(string op)() if (op == "+" || op == "-") {
1528 
1529             enum fragment = {
1530                 string result;
1531                 static foreach(fn; FieldNameTuple!T)
1532                     result ~= op ~ fn ~ ",";
1533                 return result;
1534             }();
1535             return mixin("T(", fragment, ")");
1536 
1537         }
1538 
1539         inout T opBinary(string op)(inout T rhs) if (op == "+" || op == "-") {
1540 
1541             enum fragment = {
1542                 string result;
1543                 foreach(fn; FieldNameTuple!T)
1544                     result ~= fn ~ op ~ "rhs." ~ fn ~ ",";
1545                 return result;
1546             }();
1547             return mixin("T(", fragment, ")");
1548 
1549         }
1550 
1551         ref T opOpAssign(string op)(inout T rhs) if (op == "+" || op == "-") {
1552 
1553             foreach (field; FieldNameTuple!T)
1554                 mixin(field, op,  "= rhs.", field, ";");
1555 
1556             return this;
1557 
1558         }
1559 
1560         inout T opBinary(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op ==  "/") {
1561 
1562             enum fragment = {
1563                 string result;
1564                 foreach(fn; FieldNameTuple!T)
1565                     result ~= fn ~ op ~ "rhs,";
1566                 return result;
1567             }();
1568             return mixin("T(", fragment, ")");
1569 
1570         }
1571 
1572         inout T opBinaryRight(string op)(inout float lhs) if (op == "+" || op == "-" || op == "*" || op ==  "/") {
1573 
1574             enum fragment = {
1575                 string result;
1576                 foreach(fn; FieldNameTuple!T)
1577                     result ~= "lhs" ~ op ~ fn ~ ",";
1578                 return result;
1579             }();
1580             return mixin("T(", fragment, ")");
1581 
1582         }
1583 
1584         ref T opOpAssign(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op ==  "/") {
1585 
1586             foreach (field; FieldNameTuple!T)
1587                 mixin(field, op, "= rhs;");
1588             return this;
1589 
1590         }
1591     }
1592 
1593 }