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(alpha, 0);
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 alpha;
722     ubyte index;
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     /// Set color at given position. Does nothing if position is out of bounds.
854     ///
855     /// The `set(int, int, Color)` overload only supports true color images. For paletted images, use
856     /// `set(int, int, PalettedColor)`. The latter can also be used for building true color images using a palette, if
857     /// one is supplied in the image at the time.
858     void set(int x, int y, Color color) {
859 
860         if (x < 0 || y < 0) return;
861         if (x >= width || y >= height) return;
862 
863         const index = y * width + x;
864 
865         final switch (format) {
866 
867             case Format.rgba:
868                 rgbaPixels[index] = color;
869                 return;
870             case Format.palettedAlpha:
871                 assert(false, "Unsupported image format: Cannot `set` pixels by color in a paletted image.");
872             case Format.alpha:
873                 alphaPixels[index] = color.a;
874                 return;
875 
876         }
877 
878     }
879 
880     /// ditto
881     void set(int x, int y, PalettedColor entry) {
882 
883         if (x < 0 || y < 0) return;
884         if (x >= width || y >= height) return;
885 
886         const index = y * width + x;
887         const color = paletteColor(entry);
888 
889         final switch (format) {
890 
891             case Format.rgba:
892                 rgbaPixels[index] = color;
893                 return;
894             case Format.palettedAlpha:
895                 palettedAlphaPixels[index] = entry;
896                 return;
897             case Format.alpha:
898                 alphaPixels[index] = color.a;
899                 return;
900 
901         }
902 
903     }
904 
905     /// Clear the image, replacing every pixel with given color.
906     ///
907     /// The `clear(Color)` overload only supports true color images. For paletted images, use `clear(PalettedColor)`.
908     /// The latter can also be used for building true color images using a palette, if one is supplied in the image at
909     /// the time.
910     void clear(Color color) {
911 
912         final switch (format) {
913 
914             case Format.rgba:
915                 rgbaPixels[] = color;
916                 return;
917             case Format.palettedAlpha:
918                 assert(false, "Unsupported image format: Cannot `clear` by color in a paletted image.");
919             case Format.alpha:
920                 alphaPixels[] = color.a;
921                 return;
922 
923         }
924 
925     }
926 
927     /// ditto
928     void clear(PalettedColor entry) {
929 
930         const color = paletteColor(entry);
931 
932         final switch (format) {
933 
934             case Format.rgba:
935                 rgbaPixels[] = color;
936                 return;
937             case Format.palettedAlpha:
938                 palettedAlphaPixels[] = entry;
939                 return;
940             case Format.alpha:
941                 alphaPixels[] = color.a;
942                 return;
943 
944         }
945 
946     }
947 
948 }
949 
950 
951 /// Image or texture can be rendered by Fluid, for example, a texture stored in VRAM.
952 ///
953 /// Textures make use of manual memory management. See `TextureGC` for a GC-managed texture.
954 struct Texture {
955 
956     /// Tombstone for this texture
957     shared(TextureTombstone)* tombstone;
958 
959     /// Format of the texture.
960     Image.Format format;
961 
962     /// GPU/backend ID of the texture.
963     uint id;
964 
965     /// Width and height of the texture, **in dots**. The meaning of a dot is defined by `dpiX` and `dpiY`
966     int width, height;
967 
968     /// Dots per inch for the X and Y axis. Defaults to 96, thus making a dot in the texture equivalent to a pixel.
969     int dpiX = 96, dpiY = 96;
970 
971     /// If relevant, the texture is to use this palette.
972     Color[] palette;
973 
974     bool opEquals(const Texture other) const
975 
976         => id == other.id
977         && width == other.width
978         && height == other.height
979         && dpiX == other.dpiX
980         && dpiY == other.dpiY;
981 
982     version (Have_raylib_d)void opAssign(raylib.Texture rayTexture) @system {
983         this = rayTexture.toFluid();
984     }
985 
986     /// Get the backend for this texture. Doesn't work after freeing the tombstone.
987     inout(FluidBackend) backend() inout @trusted
988 
989         => cast(inout FluidBackend) tombstone.backend;
990 
991     /// DPI value of the texture.
992     Vector2 dpi() const
993 
994         => Vector2(dpiX, dpiY);
995 
996     /// Get texture size as a vector.
997     Vector2 canvasSize() const
998 
999         => Vector2(width, height);
1000 
1001     /// Get the size the texture will occupy within the viewport.
1002     Vector2 viewportSize() const
1003 
1004         => Vector2(
1005             width * 96 / dpiX,
1006             height * 96 / dpiY
1007         );
1008 
1009     /// Update the texture to match the given image.
1010     void update(Image image) @system {
1011 
1012         backend.updateTexture(this, image);
1013 
1014     }
1015 
1016     /// Draw this texture.
1017     void draw(Vector2 position, Color tint = color!"fff") {
1018 
1019         auto rectangle = Rectangle(position.tupleof, viewportSize.tupleof);
1020 
1021         backend.drawTexture(this, rectangle, tint);
1022 
1023     }
1024 
1025     void draw(Rectangle rectangle, Color tint = color!"fff") {
1026 
1027         backend.drawTexture(this, rectangle, tint);
1028 
1029     }
1030 
1031     /// Destroy this texture. This function is thread-safe.
1032     void destroy() @system {
1033 
1034         if (tombstone is null) return;
1035 
1036         tombstone.markDestroyed();
1037         tombstone = null;
1038         id = 0;
1039 
1040     }
1041 
1042 }
1043 
1044 /// Wrapper over `Texture` that automates destruction via GC or RAII.
1045 struct TextureGC {
1046 
1047     /// Underlying texture. Lifetime is bound to this struct.
1048     Texture texture;
1049 
1050     alias texture this;
1051 
1052     /// Load a texture from filename.
1053     this(FluidBackend backend, string filename) @trusted {
1054 
1055         this.texture = backend.loadTexture(filename);
1056 
1057     }
1058 
1059     /// Load a texture from image data.
1060     this(FluidBackend backend, Image data) @trusted {
1061 
1062         this.texture = backend.loadTexture(data);
1063 
1064     }
1065 
1066     /// Move constructor for TextureGC; increment the reference counter for the texture.
1067     ///
1068     /// While I originally did not intend to implement reference counting, it is necessary to make TextureGC work in
1069     /// dynamic arrays. Changing the size of the array will copy the contents without performing a proper move of the
1070     /// old items. The postblit is the only kind of move constructor that will be called in this case, and a copy
1071     /// constructor does not do its job.
1072     this(this) @system {
1073 
1074         if (tombstone)
1075         tombstone.markCopied();
1076 
1077     }
1078 
1079     @system
1080     unittest {
1081 
1082         import std.string;
1083 
1084         // This tests using TextureGC inside of a dynamic array, especially after resizing. See documentation for
1085         // the postblit above.
1086 
1087         // Test two variants:
1088         // * One, where we rely on the language to finalize the copied value
1089         // * And one, where we manually destroy the value
1090         foreach (explicitDestruction; [false, true]) {
1091 
1092             void makeCopy(TextureGC[] arr) {
1093 
1094                 // Create the copy
1095                 auto copy = arr;
1096 
1097                 assert(sameHead(arr, copy));
1098 
1099                 // Expand the array, creating another
1100                 copy.length = 1024;
1101 
1102                 assert(!sameHead(arr, copy));
1103 
1104                 // References to tombstones exist in both arrays now
1105                 assert(!copy[0].tombstone.isDestroyed);
1106                 assert(!arr[0].tombstone.isDestroyed);
1107 
1108                 // The copy should be marked as moved
1109                 assert(copy[0].tombstone.references == 2);
1110                 assert(arr[0].tombstone.references == 2);
1111 
1112                 // Destroy the tombstone
1113                 if (explicitDestruction) {
1114 
1115                     auto tombstone = copy[0].tombstone;
1116 
1117                     copy[0].destroy();
1118                     assert(tombstone.references == 1);
1119                     assert(!tombstone.isDestroyed);
1120 
1121                 }
1122 
1123                 // Forget about the copy
1124                 copy = null;
1125 
1126             }
1127 
1128             static void trashStack() {
1129 
1130                 import core.memory;
1131 
1132                 // Destroy the stack to get rid of any references to `copy`
1133                 ubyte[2048] garbage;
1134 
1135                 // Collect it, make sure the tombstone gets eaten
1136                 GC.collect();
1137 
1138             }
1139 
1140             auto io = new HeadlessBackend;
1141             auto image = generateColorImage(10, 10, color("#fff"));
1142             auto arr = [
1143                 TextureGC(io, image),
1144                 TextureGC.init,
1145             ];
1146 
1147             makeCopy(arr);
1148             trashStack();
1149 
1150             assert(!arr[0].tombstone.isDestroyed, "Tombstone of a live texture was destroyed after copying an array"
1151                 ~ format!" (explicitDestruction %s)"(explicitDestruction));
1152 
1153             io.reaper.collect();
1154 
1155             assert(io.isTextureValid(arr[0]));
1156             assert(!arr[0].tombstone.isDestroyed);
1157             assert(!arr[0].tombstone.isDisowned);
1158             assert(arr[0].tombstone.references == 1);
1159 
1160         }
1161 
1162     }
1163 
1164     @system
1165     unittest {
1166 
1167         auto io = new HeadlessBackend;
1168         auto image = generateColorImage(10, 10, color("#fff"));
1169         auto arr = [
1170             TextureGC(io, image),
1171             TextureGC.init,
1172         ];
1173         auto copy = arr.dup;
1174 
1175         assert(arr[0].tombstone.references == 2);
1176 
1177         io.reaper.collect();
1178 
1179         assert(io.isTextureValid(arr[0]));
1180 
1181     }
1182 
1183     ~this() @trusted {
1184 
1185         texture.destroy();
1186 
1187     }
1188 
1189     /// Release the texture, moving it to manual management.
1190     Texture release() @system {
1191 
1192         auto result = texture;
1193         texture = texture.init;
1194         return result;
1195 
1196     }
1197 
1198 }
1199 
1200 /// Get a hex code from color.
1201 string toHex(string prefix = "#")(Color color) {
1202 
1203     import std.format;
1204 
1205     // Full alpha, use a six digit code
1206     if (color.a == 0xff) {
1207 
1208         return format!(prefix ~ "%02x%02x%02x")(color.r, color.g, color.b);
1209 
1210     }
1211 
1212     // Include alpha otherwise
1213     else return format!(prefix ~ "%02x%02x%02x%02x")(color.tupleof);
1214 
1215 }
1216 
1217 unittest {
1218 
1219     // No relevant alpha
1220     assert(color("fff").toHex == "#ffffff");
1221     assert(color("ffff").toHex == "#ffffff");
1222     assert(color("ffffff").toHex == "#ffffff");
1223     assert(color("ffffffff").toHex == "#ffffff");
1224     assert(color("fafbfc").toHex == "#fafbfc");
1225     assert(color("123").toHex == "#112233");
1226 
1227     // Alpha set
1228     assert(color("c0fe").toHex == "#cc00ffee");
1229     assert(color("1234").toHex == "#11223344");
1230     assert(color("0000").toHex == "#00000000");
1231     assert(color("12345678").toHex == "#12345678");
1232 
1233 }
1234 
1235 /// Create a color from hex code.
1236 Color color(string hexCode)() {
1237 
1238     return color(hexCode);
1239 
1240 }
1241 
1242 /// ditto
1243 Color color(string hexCode) pure {
1244 
1245     import std.string : chompPrefix;
1246     import std.format : format, formattedRead;
1247 
1248     // Remove the # if there is any
1249     const hex = hexCode.chompPrefix("#");
1250 
1251     Color result;
1252     result.a = 0xff;
1253 
1254     switch (hex.length) {
1255 
1256         // 4 digit RGBA
1257         case 4:
1258             formattedRead!"%x"(hex[3..4], result.a);
1259             result.a *= 17;
1260 
1261             // Parse the rest like RGB
1262             goto case;
1263 
1264         // 3 digit RGB
1265         case 3:
1266             formattedRead!"%x"(hex[0..1], result.r);
1267             formattedRead!"%x"(hex[1..2], result.g);
1268             formattedRead!"%x"(hex[2..3], result.b);
1269             result.r *= 17;
1270             result.g *= 17;
1271             result.b *= 17;
1272             break;
1273 
1274         // 8 digit RGBA
1275         case 8:
1276             formattedRead!"%x"(hex[6..8], result.a);
1277             goto case;
1278 
1279         // 6 digit RGB
1280         case 6:
1281             formattedRead!"%x"(hex[0..2], result.r);
1282             formattedRead!"%x"(hex[2..4], result.g);
1283             formattedRead!"%x"(hex[4..6], result.b);
1284             break;
1285 
1286         default:
1287             assert(false, "Invalid hex code length");
1288 
1289     }
1290 
1291     return result;
1292 
1293 }
1294 
1295 unittest {
1296 
1297     import std.exception;
1298 
1299     assert(color!"#123" == Color(0x11, 0x22, 0x33, 0xff));
1300     assert(color!"#1234" == Color(0x11, 0x22, 0x33, 0x44));
1301     assert(color!"1234" == Color(0x11, 0x22, 0x33, 0x44));
1302     assert(color!"123456" == Color(0x12, 0x34, 0x56, 0xff));
1303     assert(color!"2a5592f0" == Color(0x2a, 0x55, 0x92, 0xf0));
1304 
1305     assertThrown(color!"ag5");
1306 
1307 }
1308 
1309 /// Set the alpha channel for the given color, as a float.
1310 Color setAlpha(Color color, float alpha) {
1311 
1312     import std.algorithm : clamp;
1313 
1314     color.a = cast(ubyte) clamp(ubyte.max * alpha, 0, ubyte.max);
1315     return color;
1316 
1317 }
1318 
1319 /// Blend two colors together; apply `top` on top of the `bottom` color. If `top` has maximum alpha, returns `top`. If
1320 /// alpha is zero, returns `bottom`.
1321 ///
1322 /// BUG: This function is currently broken and returns incorrect results.
1323 Color alphaBlend(Color bottom, Color top) {
1324 
1325     auto topA = cast(float) top.a / ubyte.max;
1326     auto bottomA = (1 - topA) * cast(float) bottom.a / ubyte.max;
1327 
1328     return Color(
1329         cast(ubyte) (bottom.r * bottomA + top.r * topA),
1330         cast(ubyte) (bottom.g * bottomA + top.g * topA),
1331         cast(ubyte) (bottom.b * bottomA + top.b * topA),
1332         cast(ubyte) (bottom.a * bottomA + top.a * topA),
1333     );
1334 
1335 }
1336 
1337 /// Multiple color values.
1338 Color multiply(Color a, Color b) {
1339 
1340     return Color(
1341         cast(ubyte) (a.r * b.r / 255.0),
1342         cast(ubyte) (a.g * b.g / 255.0),
1343         cast(ubyte) (a.b * b.b / 255.0),
1344         cast(ubyte) (a.a * b.a / 255.0),
1345     );
1346 
1347 }
1348 
1349 unittest {
1350 
1351     assert(multiply(color!"#fff", color!"#a00") == color!"#a00");
1352     assert(multiply(color!"#1eff00", color!"#009bdd") == color!"#009b00");
1353     assert(multiply(color!"#aaaa", color!"#1234") == color!"#0b16222d");
1354 
1355 }
1356 
1357 version (unittest) {
1358 
1359     debug (Fluid_BuildMessages) {
1360         pragma(msg, "Fluid: Using headless as the default backend (unittest)");
1361     }
1362 
1363     FluidBackend defaultFluidBackend() {
1364 
1365         return new HeadlessBackend;
1366 
1367     }
1368 
1369 }
1370 
1371 else version (Have_raylib_d) {
1372 
1373     debug (Fluid_BuildMessages) {
1374         pragma(msg, "Fluid: Using Raylib 5 as the default backend");
1375     }
1376 
1377     FluidBackend defaultFluidBackend() {
1378 
1379         return new Raylib5Backend;
1380 
1381     }
1382 
1383 }
1384 
1385 else {
1386 
1387     debug (Fluid_BuildMessages) {
1388         pragma(msg, "Fluid: No built-in backend in use");
1389     }
1390 
1391     FluidBackend defaultFluidBackend() {
1392 
1393         return null;
1394 
1395     }
1396 
1397 }
1398 
1399 // Structures
1400 version (Have_raylib_d) {
1401 
1402     debug (Fluid_BuildMessages) {
1403         pragma(msg, "Fluid: Using Raylib core structures");
1404     }
1405 
1406     import raylib;
1407 
1408     alias Rectangle = raylib.Rectangle;
1409     alias Vector2 = raylib.Vector2;
1410     alias Color = raylib.Color;
1411 
1412 }
1413 
1414 else {
1415 
1416     struct Vector2 {
1417 
1418         float x = 0;
1419         float y = 0;
1420 
1421         mixin Linear;
1422 
1423     }
1424 
1425     struct Rectangle {
1426 
1427         float x, y;
1428         float width, height;
1429 
1430         alias w = width;
1431         alias h = height;
1432 
1433     }
1434 
1435     struct Color {
1436 
1437         ubyte r, g, b, a;
1438 
1439     }
1440 
1441     /// `mixin Linear` taken from [raylib-d](https://github.com/schveiguy/raylib-d), reformatted and without Rotor3
1442     /// support.
1443     ///
1444     /// Licensed under the [z-lib license](https://github.com/schveiguy/raylib-d/blob/master/LICENSE).
1445     private mixin template Linear() {
1446 
1447         private static alias T = typeof(this);
1448         private import std.traits : FieldNameTuple;
1449 
1450         static T zero() {
1451 
1452             enum fragment = {
1453                 string result;
1454                 static foreach(i; 0 .. T.tupleof.length)
1455                     result ~= "0,";
1456                 return result;
1457             }();
1458 
1459             return mixin("T(", fragment, ")");
1460         }
1461 
1462         static T one() {
1463 
1464             enum fragment = {
1465                 string result;
1466                 static foreach(i; 0 .. T.tupleof.length)
1467                     result ~= "1,";
1468                 return result;
1469             }();
1470             return mixin("T(", fragment, ")");
1471 
1472         }
1473 
1474         inout T opUnary(string op)() if (op == "+" || op == "-") {
1475 
1476             enum fragment = {
1477                 string result;
1478                 static foreach(fn; FieldNameTuple!T)
1479                     result ~= op ~ fn ~ ",";
1480                 return result;
1481             }();
1482             return mixin("T(", fragment, ")");
1483 
1484         }
1485 
1486         inout T opBinary(string op)(inout T rhs) if (op == "+" || op == "-") {
1487 
1488             enum fragment = {
1489                 string result;
1490                 foreach(fn; FieldNameTuple!T)
1491                     result ~= fn ~ op ~ "rhs." ~ fn ~ ",";
1492                 return result;
1493             }();
1494             return mixin("T(", fragment, ")");
1495 
1496         }
1497 
1498         ref T opOpAssign(string op)(inout T rhs) if (op == "+" || op == "-") {
1499 
1500             foreach (field; FieldNameTuple!T)
1501                 mixin(field, op,  "= rhs.", field, ";");
1502 
1503             return this;
1504 
1505         }
1506 
1507         inout T opBinary(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op ==  "/") {
1508 
1509             enum fragment = {
1510                 string result;
1511                 foreach(fn; FieldNameTuple!T)
1512                     result ~= fn ~ op ~ "rhs,";
1513                 return result;
1514             }();
1515             return mixin("T(", fragment, ")");
1516 
1517         }
1518 
1519         inout T opBinaryRight(string op)(inout float lhs) if (op == "+" || op == "-" || op == "*" || op ==  "/") {
1520 
1521             enum fragment = {
1522                 string result;
1523                 foreach(fn; FieldNameTuple!T)
1524                     result ~= "lhs" ~ op ~ fn ~ ",";
1525                 return result;
1526             }();
1527             return mixin("T(", fragment, ")");
1528 
1529         }
1530 
1531         ref T opOpAssign(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op ==  "/") {
1532 
1533             foreach (field; FieldNameTuple!T)
1534                 mixin(field, op, "= rhs;");
1535             return this;
1536 
1537         }
1538     }
1539 
1540 }