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