1 /// Raylib connection layer for Fluid. This makes it possible to render Fluid apps and user interfaces through Raylib.
2 ///
3 /// Use `raylibStack` for a complete implementation, and `raylibView` for a minimal one. The complete stack
4 /// is recommended for most usages, as it bundles full mouse and keyboard support, while the chain node may
5 /// be preferred for advanced usage and requires manual setup. See `RaylibView`'s documentation for more
6 /// information.
7 ///
8 /// Note that because Raylib introduces breaking changes in every version, the current version of Raylib should
9 /// be specified using `raylibStack.v5_5()`. Raylib 5.5 is currently the oldest version supported,
10 /// and is the default in case no version is chosen explicitly.
11 ///
12 /// Unlike `fluid.backend.Raylib5Backend`, this uses the new I/O system introduced in Fluid 0.8.0. This layer
13 /// is recommended for new apps, but disabled by default.
14 module fluid.raylib_view;
15 
16 version (Have_raylib_d):
17 
18 debug (Fluid_BuildMessages) {
19     pragma(msg, "Fluid: Building with Raylib 5.5 support (RaylibView)");
20 }
21 
22 // Coordinate scaling will translate Fluid coordinates, where each pixels is 1/96th of an inch, to screen coordinates,
23 // making use of DPI information provided by the system. This flag is only set on macOS, where the system handles this
24 // automatically.
25 version (OSX) {
26     version = Fluid_DisableScaling;
27 
28     debug (Fluid_BuildMessages) {
29         pragma(msg, "Fluid: Disabling coordinate scaling on macOS");
30     }
31 }
32 
33 import raylib;
34 import optional;
35 
36 import std.meta;
37 import std.traits;
38 import std.array;
39 import std.string;
40 
41 import fluid.node;
42 import fluid.utils;
43 import fluid.types;
44 import fluid.node_chain;
45 
46 import fluid.future.arena;
47 
48 import fluid.backend.raylib5 : Raylib5Backend, toRaylib;
49 import fluid.backend.headless : HeadlessBackend;
50 
51 import fluid.io.time;
52 import fluid.io.canvas;
53 import fluid.io.hover;
54 import fluid.io.mouse;
55 import fluid.io.focus;
56 import fluid.io.keyboard;
57 import fluid.io.clipboard;
58 import fluid.io.image_load;
59 import fluid.io.preference;
60 import fluid.io.overlay;
61 
62 static if (!__traits(compiles, IsShaderReady))
63     private alias IsShaderReady = IsShaderValid;
64 
65 @safe:
66 
67 /// `raylibStack` implements all I/O functionality needed for Fluid to function, using Raylib to read user input
68 /// and present visuals on the screen.
69 ///
70 /// Specify Raylib version by using a member: `raylibStack.v5_5()` will create a stack for Raylib 5.5.
71 ///
72 /// `raylibStack` provides a default implementation for `TimeIO`, `PreferenceIO`,  `HoverIO`, `FocusIO`, `ActionIO`
73 /// and `FileIO`, on top of all the systems provided by Raylib itself: `CanvasIO`, `KeyboardIO`, `MouseIO`,
74 /// `ClipboardIO` and `ImageLoadIO`.
75 enum raylibStack = RaylibViewBuilder!RaylibStack.init;
76 
77 /// `raylibView` implements some I/O functionality using the Raylib library, namely `CanvasIO`, `KeyboardIO`,
78 /// `MouseIO`, `ClipboardIO` and `ImageLoadIO`.
79 ///
80 /// These systems are not enough for Fluid to function. Use `raylibStack` to also initialize all other necessary
81 /// systems.
82 ///
83 /// Specify Raylib version by using a member: `raylibView.v5_5()` will create a stack for Raylib 5.5.
84 enum raylibView = RaylibViewBuilder!RaylibView.init;
85 
86 /// Use this enum to pick version of Raylib to use.
87 enum RaylibViewVersion {
88     v5_5,
89 }
90 
91 /// Wrapper over `NodeBuilder` which enables specifying Raylib version.
92 struct RaylibViewBuilder(alias T) {
93 
94     alias v5_5 this;
95     enum v5_5 = nodeBuilder!(T!(RaylibViewVersion.v5_5));
96 
97 }
98 
99 /// Implements Raylib support through Fluid's I/O system. Use `raylibStack` or `raylibView` to construct.
100 ///
101 /// `RaylibView` relies on a number of I/O systems that it does not implement, but must be provided for it
102 /// to function. Use `RaylibStack` to initialize the chain along with default choices for these systems,
103 /// suitable for most uses, or provide these systems as parent nodes:
104 ///
105 /// * `HoverIO` for mouse support. Fluid does not presently support mobile devices through Raylib, and Raylib's
106 ///   desktop version does not fully support touchscreens (as GLFW does not).
107 /// * `FocusIO` for keyboard and gamepad support. Gamepad support may currently be limited.
108 /// * `TimeIO` for measuring time between mouse clicks.
109 /// * `PreferenceIO` for user preferences from the system.
110 ///
111 /// There is a few systems that `RaylibView` does not require, but are included in `RaylibStack` to support
112 /// commonly needed functionality:
113 ///
114 /// * `ActionIO` for translating user input into a format Fluid nodes can understand.
115 /// * `FileIO` for loading and writing files.
116 ///
117 /// `RaylibView` itself provides a number of I/O systems using functionality from the Raylib library:
118 ///
119 /// * `CanvasIO` for drawing nodes and providing visual output.
120 /// * `MouseIO` to provide mouse support.
121 /// * `KeyboardIO` to provide keyboard support.
122 /// * `ClipboardIO` to access system keyboard.
123 /// * `ImageLoadIO` to load images using codecs available in Raylib.
124 class RaylibView(RaylibViewVersion raylibVersion) : Node, CanvasIO, MouseIO, KeyboardIO, ClipboardIO, ImageLoadIO {
125 
126     HoverIO hoverIO;
127     FocusIO focusIO;
128     TimeIO timeIO;
129     PreferenceIO preferenceIO;
130 
131     public {
132 
133         /// Node drawn by this view.
134         Node next;
135 
136         /// Scale set for this view. It can be controlled through the `FLUID_SCALE` environment
137         /// variable (expected to be a float, e.g. `1.5` or a pair of floats `1.5x1.25`)
138         ///
139         /// Changing the scale requires an `updateSize` call.
140         auto scale = Vector2(1, 1);
141 
142     }
143 
144     private struct RaylibImage {
145         fluid.Image image;
146         raylib.Texture texture;
147     }
148 
149     private {
150 
151         // Window state
152         Vector2 _dpi;
153         Vector2 _dpiScale;
154         Vector2 _windowSize;
155         Rectangle _cropArea;
156 
157         // Resources
158         ResourceArena!RaylibImage _images;
159         raylib.Texture _paletteTexture;
160         Shader _alphaImageShader;
161         Shader _palettedAlphaImageShader;
162         int _palettedAlphaImageShader_palette;
163 
164         /// Map of image pointers (image.data.ptr) to indices in the resource arena (_images)
165         int[size_t] _imageIndices;
166 
167         // I/O
168         HoverPointer _mousePointer;
169         Appender!(KeyboardKey[]) _heldKeys;
170         MultipleClickSensor _multiClickSensor;
171 
172     }
173 
174     this(Node next = null) {
175 
176         this.next = next;
177         this.scale = getGlobalScale();
178 
179         // Initialize the mouse
180         _mousePointer.device = this;
181         _mousePointer.number = 0;
182 
183     }
184 
185     override void resizeImpl(Vector2) @trusted {
186 
187         require(focusIO);
188         require(hoverIO);
189         require(timeIO);
190         require(preferenceIO);
191         hoverIO.loadTo(_mousePointer);
192 
193         // Fetch data from Raylib
194         _dpiScale = GetWindowScaleDPI;
195         _dpi = Vector2(_dpiScale.x * scale.x * 96, _dpiScale.y * scale.y * 96);
196         _windowSize = toFluid(GetScreenWidth, GetScreenHeight);
197         resetCropArea();
198 
199         // Load shaders
200         if (!IsShaderReady(_alphaImageShader)) {
201             _alphaImageShader = LoadShaderFromMemory(null, Raylib5Backend.alphaImageShaderCode.ptr);
202         }
203         if (!IsShaderReady(_palettedAlphaImageShader)) {
204             _palettedAlphaImageShader = LoadShaderFromMemory(null, Raylib5Backend.palettedAlphaImageShaderCode.ptr);
205             _palettedAlphaImageShader_palette = GetShaderLocation(_palettedAlphaImageShader, "palette");
206         }
207 
208         // Free resources
209         _images.startCycle((newIndex, ref resource) @trusted {
210 
211             const id = cast(size_t) resource.image.data.ptr;
212 
213             // Resource freed
214             if (newIndex == -1) {
215                 _imageIndices.remove(id);
216                 UnloadTexture(resource.texture);
217             }
218 
219             // Moved
220             else {
221                 _imageIndices[id] = newIndex;
222             }
223 
224         });
225 
226         // Enable the system
227         auto io = this.implementIO();
228 
229         // Resize the node
230         if (next) {
231             resizeChild(next, _windowSize);
232         }
233 
234         // RaylibView does not take space in whatever it is placed in
235         minSize = Vector2();
236 
237     }
238 
239     override void drawImpl(Rectangle, Rectangle) @trusted {
240 
241         updateMouse();
242         updateKeyboard();
243 
244         if (next) {
245             resetCropArea();
246             drawChild(next, _cropArea);
247         }
248 
249         if (IsWindowResized) {
250             updateSize();
251         }
252 
253     }
254 
255     protected void updateMouse() @trusted {
256 
257         import fluid.backend : FluidMouseCursor;
258 
259         // Update mouse status
260         _mousePointer.position     = toFluid(GetMousePosition);
261         _mousePointer.scroll       = scroll();
262         _mousePointer.isScrollHeld = false;
263         _mousePointer.clickCount   = 0;
264 
265         // Detect multiple mouse clicks
266         if (IsMouseButtonDown(MouseButton.MOUSE_BUTTON_LEFT)) {
267             _multiClickSensor.hold(timeIO, preferenceIO, _mousePointer);
268             _mousePointer.clickCount   = _multiClickSensor.clicks;
269         }
270         else if (IsMouseButtonReleased(MouseButton.MOUSE_BUTTON_LEFT)) {
271             _multiClickSensor.activate();
272             _mousePointer.clickCount   = _multiClickSensor.clicks;
273         }
274 
275         hoverIO.loadTo(_mousePointer);
276 
277         // Set cursor icon
278         if (auto node = cast(Node) hoverIO.hoverOf(_mousePointer)) {
279 
280             const cursor = node.pickStyle().mouseCursor;
281 
282             // Hide the cursor if requested
283             if (cursor.system == cursor.system.none) {
284                 HideCursor();
285             }
286             // Show the cursor
287             else {
288                 SetMouseCursor(cursor.system.toRaylib);
289                 ShowCursor();
290             }
291 
292         }
293         else {
294             SetMouseCursor(FluidMouseCursor.systemDefault.system.toRaylib);
295             ShowCursor();
296         }
297 
298         // Send buttons
299         foreach (button; NoDuplicates!(EnumMembers!(MouseIO.Button))) {
300 
301             const buttonRay = button.toRaylibEx;
302 
303             if (buttonRay == -1) continue;
304 
305             // Active event
306             if (IsMouseButtonReleased(buttonRay)) {
307                 hoverIO.emitEvent(_mousePointer, MouseIO.createEvent(button, true));
308             }
309 
310             else if (IsMouseButtonDown(buttonRay)) {
311                 hoverIO.emitEvent(_mousePointer, MouseIO.createEvent(button, false));
312             }
313 
314         }
315 
316     }
317 
318     protected void updateKeyboard() @trusted {
319 
320         import std.utf;
321 
322         // Take text input character by character
323         while (true) {
324 
325             // TODO take more at once
326             char[4] buffer;
327 
328             const ch = cast(dchar) GetCharPressed();
329             if (ch == 0) break;
330 
331             const size = buffer.encode(ch);
332             focusIO.typeText(buffer[0 .. size]);
333 
334         }
335 
336         // Find all newly pressed keyboard keys
337         while (true) {
338 
339             const keyRay = cast(KeyboardKey) GetKeyPressed();
340             if (keyRay == 0) break;
341 
342             _heldKeys ~= keyRay;
343 
344         }
345 
346         size_t newIndex;
347         foreach (keyRay; _heldKeys[]) {
348 
349             const key = keyRay.toFluid;
350 
351             // Pressed
352             if (IsKeyPressed(keyRay) || IsKeyPressedRepeat(keyRay)) {
353                 focusIO.emitEvent(KeyboardIO.createEvent(key, true));
354                 _heldKeys[][newIndex++] = keyRay;
355             }
356 
357             // Held
358             else if (IsKeyDown(keyRay)) {
359                 focusIO.emitEvent(KeyboardIO.createEvent(key, false));
360                 _heldKeys[][newIndex++] = keyRay;
361             }
362 
363         }
364         _heldKeys.shrinkTo(newIndex);
365 
366     }
367 
368     /// Returns:
369     ///     Distance travelled by the mouse in Fluid coordinates.
370     private Vector2 scroll() @trusted {
371 
372         const move = -GetMouseWheelMoveV;
373         const speed = preferenceIO.scrollSpeed;
374 
375         return Vector2(move.x * speed.x, move.y * speed.y);
376 
377     }
378 
379     override Vector2 dpi() const nothrow {
380         return _dpi;
381     }
382 
383     override Optional!Rectangle cropArea() const nothrow {
384         return typeof(return)(_cropArea);
385     }
386 
387     override void cropArea(Rectangle area) nothrow @trusted {
388         _cropArea = area;
389         const rectRay = toRaylib(area);
390         BeginScissorMode(
391             cast(int) rectRay.x,
392             cast(int) rectRay.y,
393             cast(int) rectRay.width,
394             cast(int) rectRay.height,
395         );
396     }
397 
398     override void resetCropArea() nothrow {
399         cropArea(Rectangle(0, 0, _windowSize.tupleof));
400     }
401 
402     /// Convert position of a point or rectangle in Fluid space to Raylib space.
403     /// Params:
404     ///     position = Fluid position, where each coordinate is specified in pixels (1/96th of an inch).
405     ///     rectangle = Rectangle in Fluid space.
406     /// Returns:
407     ///     Raylib position or rectangle, where each coordinate is specified in screen dots.
408     Vector2 toRaylib(Vector2 position) const nothrow {
409 
410         version (Fluid_DisableScaling)
411             return position;
412         else
413             return toDots(position);
414 
415     }
416 
417     /// ditto
418     Rectangle toRaylib(Rectangle rectangle) const nothrow {
419 
420         version (Fluid_DisableScaling)
421             return rectangle;
422         else
423             return Rectangle(
424                 toDots(rectangle.start).tupleof,
425                 toDots(rectangle.size).tupleof,
426             );
427 
428     }
429 
430     /// Convert position of a point or rectangle in Raylib space to Fluid space.
431     /// Params:
432     ///     position = Raylib position, where each coordinate is specified in screen dots.
433     ///     rectangle = Rectangle in Raylib space.
434     /// Returns:
435     ///     Position in Fluid space
436     Vector2 toFluid(Vector2 position) const nothrow {
437 
438         version (Fluid_DisableScaling)
439             return position;
440         else
441             return fromDots(position);
442 
443     }
444 
445     /// ditto
446     Vector2 toFluid(float x, float y) const nothrow {
447 
448         version (Fluid_DisableScaling)
449             return Vector2(x, y);
450         else
451             return fromDots(Vector2(x, y));
452 
453     }
454 
455     /// ditto
456     Rectangle toFluid(Rectangle rectangle) const nothrow {
457 
458         version (Fluid_DisableScaling)
459             return rectangle;
460         else
461             return Rectangle(
462                 fromDots(rectangle.start).tupleof,
463                 fromDots(rectangle.size).tupleof,
464             );
465 
466     }
467 
468     /// Get a Raylib texture for the corresponding drawable image. The image MUST be loaded.
469     raylib.Texture textureFor(DrawableImage image) nothrow @trusted {
470         return _images[image.id].texture;
471     }
472 
473     /// Get the shader used for `alpha` images. This shader is loaded on the first resize,
474     /// and is not accessible before.
475     /// Returns:
476     ///     Shader used for images with the `alpha` format set.
477     Shader alphaImageShader() nothrow @trusted {
478         assert(IsShaderReady(_alphaImageShader), "alphaImageShader is not accessible before resize");
479         return _alphaImageShader;
480     }
481 
482     /// Get the shader used for `palettedAlpha` images. This shader is loaded on the first resize,
483     /// and is not accessible before.
484     /// Params:
485     ///     palette = Palette to use with the shader.
486     /// Returns:
487     ///     Shader used for images with the `palettedAlpha` format set.
488     Shader palettedAlphaImageShader(Color[] palette) nothrow @trusted {
489         assert(IsShaderReady(_palettedAlphaImageShader), "palettedAlphaImageShader is not accessible before resize");
490 
491         auto paletteTexture = this.paletteTexture(palette);
492 
493         // Load the palette
494         SetShaderValueTexture(_palettedAlphaImageShader, _palettedAlphaImageShader_palette, paletteTexture);
495 
496         return _palettedAlphaImageShader;
497     }
498 
499     /// Create a palette texture.
500     private raylib.Texture paletteTexture(scope Color[] colors) nothrow @trusted
501     in (colors.length <= 256, "There can only be at most 256 colors in a palette.")
502     do {
503 
504         // Fill empty slots in the palette with white
505         Color[256] allColors = Color(0xff, 0xff, 0xff, 0xff);
506         allColors[0 .. colors.length] = colors;
507 
508         // Prepare an image for the texture
509         scope image = fluid.Image(allColors[], 256, 1);
510 
511         // Create the texture if it doesn't exist
512         if (_paletteTexture is _paletteTexture.init)
513             _paletteTexture = LoadTextureFromImage(image.toRaylib);
514 
515         // Or, update existing palette image
516         else
517             UpdateTexture(_paletteTexture, image.data.ptr);
518 
519         return _paletteTexture;
520 
521     }
522 
523     override void drawTriangleImpl(Vector2 a, Vector2 b, Vector2 c, Color color) nothrow @trusted {
524         DrawTriangle(
525             toRaylib(a),
526             toRaylib(b),
527             toRaylib(c),
528             color);
529     }
530 
531     override void drawCircleImpl(Vector2 center, float radius, Color color) nothrow @trusted {
532         const centerRay = toRaylib(center);
533         const radiusRay = toRaylib(Vector2(radius, radius));
534         DrawEllipse(
535             cast(int) centerRay.x,
536             cast(int) centerRay.y,
537             radiusRay.tupleof,
538             color);
539     }
540 
541     override void drawCircleOutlineImpl(Vector2 center, float radius, float width, Color color) nothrow @trusted {
542         const centerRay = toRaylib(center);
543         const radiusRay = toRaylib(Vector2(radius, radius));
544         const previousLineWidth = rlGetLineWidth();
545         // Note: This isn't very accurate at greater widths
546         rlSetLineWidth(width);
547         DrawEllipseLines(
548             cast(int) centerRay.x,
549             cast(int) centerRay.y,
550             radiusRay.tupleof,
551             color);
552         rlDrawRenderBatchActive();
553         rlSetLineWidth(previousLineWidth);
554     }
555 
556     override void drawRectangleImpl(Rectangle rectangle, Color color) nothrow @trusted {
557         DrawRectangleRec(
558             toRaylib(rectangle),
559             color);
560     }
561 
562     override void drawLineImpl(Vector2 start, Vector2 end, float width, Color color) nothrow @trusted {
563         DrawLineEx(
564             toRaylib(start),
565             toRaylib(end),
566             width,
567             color);
568     }
569 
570     override void drawImageImpl(DrawableImage image, Rectangle destination, Color tint) nothrow {
571         drawImageImpl(image, destination, tint, false);
572     }
573 
574     override void drawHintedImageImpl(DrawableImage image, Rectangle destination, Color tint) nothrow {
575         drawImageImpl(image, destination, tint, true);
576     }
577 
578     private void drawImageImpl(DrawableImage image, Rectangle destination, Color tint, bool hinted) nothrow @trusted {
579 
580         import std.math;
581 
582         // Perform hinting if enabled
583         auto start = destination.start;
584         if (hinted) {
585             start = toDots(destination.start);
586             start.x = floor(start.x);
587             start.y = floor(start.y);
588             start = fromDots(start);
589         }
590 
591         const destinationRay = Rectangle(
592             toRaylib(start).tupleof,
593             toRaylib(destination.size).tupleof
594         );
595 
596         const source = Rectangle(0, 0, image.width, image.height);
597         Shader shader;
598 
599         // Enable shaders relevant to given format
600         switch (image.format) {
601 
602             case fluid.Image.Format.alpha:
603                 shader = alphaImageShader;
604                 break;
605 
606             case fluid.Image.Format.palettedAlpha:
607                 shader = palettedAlphaImageShader(image.palette);
608                 break;
609 
610             default: break;
611 
612         }
613 
614         // Start shaders, if applicable
615         if (IsShaderReady(shader))
616             BeginShaderMode(shader);
617 
618         auto texture = textureFor(image);
619 
620         DrawTexturePro(texture, source, destinationRay, Vector2(0, 0), 0, tint);
621 
622         // End shaders
623         if (IsShaderReady(shader))
624             EndShaderMode();
625 
626     }
627 
628     override int load(fluid.Image image) nothrow @trusted {
629 
630         const empty = image.width * image.height == 0;
631         const id = empty
632             ? 0
633             : cast(size_t) image.data.ptr;
634 
635         // Image already loaded, reuse
636         if (auto indexPtr = id in _imageIndices) {
637 
638             auto resource = _images[*indexPtr];
639 
640             // Image was updated, mirror the changes
641             if (image.revisionNumber > resource.image.revisionNumber) {
642 
643                 const sameFormat = resource.image.width == image.width
644                     && resource.image.height == image.height
645                     && resource.image.format == image.format;
646 
647                 resource.image = image;
648 
649                 if (empty) { }
650 
651                 // Update the texture in place if the format is the same
652                 if (sameFormat) {
653                     UpdateTexture(resource.texture, image.data.ptr);
654                 }
655 
656                 // Reupload the image if not
657                 else {
658                     UnloadTexture(resource.texture);
659                     resource.texture = LoadTextureFromImage(image.toRaylib);
660                 }
661 
662             }
663 
664             _images.reload(*indexPtr, resource);
665             return *indexPtr;
666         }
667 
668         // Empty image; do not upload
669         else if (empty) {
670             auto internalImage = RaylibImage(image, raylib.Texture.init);
671             return _imageIndices[id] = _images.load(internalImage);
672         }
673 
674         // Load the image
675         else {
676             auto texture = LoadTextureFromImage(image.toRaylib);
677             auto internalImage = RaylibImage(image, texture);
678 
679             return _imageIndices[id] = _images.load(internalImage);
680         }
681 
682     }
683 
684     override bool writeClipboard(string text) nothrow @trusted {
685 
686         SetClipboardText(text.toStringz);
687         return true;
688 
689     }
690 
691     override char[] readClipboard(return scope char[] buffer, ref int offset) nothrow @trusted {
692 
693         import std.algorithm : min;
694 
695         // This is horrible but this API will change https://git.samerion.com/Samerion/Fluid/issues/276
696         const scope clipboard = GetClipboardText().fromStringz;
697 
698         // Read the entire text, nothing remains to be read
699         if (offset >= clipboard.length) return null;
700 
701         // Get remaining text
702         const text = clipboard[offset .. $];
703         const length = min(text.length, buffer.length);
704 
705         offset += length;
706         return buffer[0 .. length] = text[0 .. length];
707 
708     }
709 
710     override fluid.Image loadImage(const ubyte[] image) @trusted {
711 
712         assert(image.length < int.max, "Image is too big to load");
713 
714         const fileType = identifyImageType(image);
715 
716         auto imageRay = LoadImageFromMemory(fileType.ptr, image.ptr, cast(int) image.length);
717         auto colors = LoadImageColors(imageRay);
718         scope (exit) UnloadImageColors(colors);
719 
720         const size = imageRay.width * imageRay.height;
721 
722         return fluid.Image(colors[0 .. size].dup, imageRay.width, imageRay.height);
723 
724     }
725 
726 }
727 
728 /// A complete implementation of all systems Fluid needs to function, using Raylib as the base for communicating with
729 /// the operating system. Use `raylibStack` to construct.
730 ///
731 /// For a minimal installation that only includes systems provided by Raylib use `RaylibView`.
732 /// Note that `RaylibView` does not provide all the systems Fluid needs to function. See its documentation for more
733 /// information.
734 ///
735 /// On top of systems already provided by `RaylibView`, `RaylibStack` also includes `HoverIO`, `FocusIO`, `ActionIO`,
736 /// `PreferenceIO`, `TimeIO` and `FileIO`. You can access them through fields named `hoverIO`, `focusIO`, `actionIO`,
737 /// `preferenceIO`, `timeIO` and `fileIO` respectively.
738 class RaylibStack(RaylibViewVersion raylibVersion) : Node {
739 
740     import fluid.hover_chain;
741     import fluid.focus_chain;
742     import fluid.input_map_chain;
743     import fluid.preference_chain;
744     import fluid.time_chain;
745     import fluid.file_chain;
746     import fluid.overlay_chain;
747 
748     public {
749 
750         /// I/O implementations provided by the stack.
751         FocusChain focusIO;
752 
753         /// ditto
754         HoverChain hoverIO;
755 
756         /// ditto
757         InputMapChain actionIO;
758 
759         /// ditto
760         PreferenceChain preferenceIO;
761 
762         /// ditto
763         TimeChain timeIO;
764 
765         /// ditto
766         FileChain fileIO;
767 
768         /// ditto
769         RaylibView!raylibVersion raylibIO;
770 
771         /// ditto
772         OverlayChain overlayIO;
773 
774     }
775 
776     private {
777 
778         HeadlessBackend _headlessBackend;
779 
780     }
781 
782     /// Initialize the stack.
783     /// Params:
784     ///     next = Node to draw using the stack.
785     this(Node next = null) {
786 
787         import fluid.structs : layout;
788 
789         _headlessBackend = new HeadlessBackend;
790         chain(
791             preferenceIO = preferenceChain(),
792             timeIO       = timeChain(),
793             actionIO     = inputMapChain(),
794             focusIO      = focusChain(),
795             hoverIO      = hoverChain(),
796             fileIO       = fileChain(),
797             raylibIO     = raylibView(
798                 chain(
799                     overlayIO = overlayChain(
800                         layout!(1, "fill")
801                     ),
802                     next,
803                 ),
804             ),
805         );
806 
807     }
808 
809     /// Returns:
810     ///     The first node in the stack.
811     inout(NodeChain) root() inout {
812         return preferenceIO;
813     }
814 
815     /// Returns:
816     ///     Top node of the stack, before `next`
817     inout(NodeChain) top() inout {
818         return overlayIO;
819     }
820 
821     /// Returns:
822     ///     The node contained by the stack, child node of the `top`.
823     inout(Node) next() inout {
824         return top.next;
825     }
826 
827     /// Change the node contained by the stack.
828     /// Params:
829     ///     value = Value to set.
830     /// Returns:
831     ///     Newly set node.
832     Node next(Node value) {
833         return top.next = value;
834     }
835 
836     override void resizeImpl(Vector2 space) {
837         resizeChild(root, space);
838         minSize = root.minSize;
839 
840         // If RaylibStack is used as the root, disable the legacy backend
841         if (tree.root == this) {
842             this.backend = _headlessBackend;
843         }
844     }
845 
846     override void drawImpl(Rectangle, Rectangle inner) {
847         drawChild(root, inner);
848     }
849 
850 }
851 
852 /// Convert a `MouseIO.Button` to a `raylib.MouseButton`.
853 ///
854 /// Temporarily named `toRaylibEx` instead of `toRaylib` to avoid conflicts with legacy Raylib functionality.
855 ///
856 /// Note:
857 ///     `raylib.MouseButton` does not have a dedicated invalid value so this function will instead
858 ///     return `-1`.
859 /// Params:
860 ///     button = A Fluid `MouseIO` button code.
861 /// Returns:
862 ///     A corresponding `raylib.MouseButton` value, `-1` if there isn't one.
863 int toRaylibEx(MouseIO.Button button) {
864 
865     with (MouseButton)
866     with (button)
867     final switch (button) {
868 
869         case none:    return -1;
870         case left:    return MOUSE_BUTTON_LEFT;
871         case right:   return MOUSE_BUTTON_RIGHT;
872         case middle:  return MOUSE_BUTTON_MIDDLE;
873         case extra1:  return MOUSE_BUTTON_SIDE;
874         case extra2:  return MOUSE_BUTTON_EXTRA;
875         case forward: return MOUSE_BUTTON_FORWARD;
876         case back:    return MOUSE_BUTTON_BACK;
877 
878     }
879 
880 }
881 
882 /// Convert a Raylib keyboard key to a `KeyboardIO.Key` code.
883 ///
884 /// Params:
885 ///     button = A Raylib `KeyboardKey` key code.
886 /// Returns:
887 ///     A corresponding `KeyboardIO.Key` value. `KeyboardIO.Key.none` if the key is not recognized.
888 KeyboardIO.Key toFluid(KeyboardKey key) {
889 
890     with (KeyboardIO.Key)
891     with (KeyboardKey)
892     final switch (key) {
893 
894         case KEY_NULL:          return none;
895         case KEY_APOSTROPHE:    return apostrophe;
896         case KEY_COMMA:         return comma;
897         case KEY_MINUS:         return minus;
898         case KEY_PERIOD:        return period;
899         case KEY_SLASH:         return slash;
900         case KEY_ZERO:          return digit0;
901         case KEY_ONE:           return digit1;
902         case KEY_TWO:           return digit2;
903         case KEY_THREE:         return digit3;
904         case KEY_FOUR:          return digit4;
905         case KEY_FIVE:          return digit5;
906         case KEY_SIX:           return digit6;
907         case KEY_SEVEN:         return digit7;
908         case KEY_EIGHT:         return digit8;
909         case KEY_NINE:          return digit9;
910         case KEY_SEMICOLON:     return semicolon;
911         case KEY_EQUAL:         return equal;
912         case KEY_A:             return a;
913         case KEY_B:             return b;
914         case KEY_C:             return c;
915         case KEY_D:             return d;
916         case KEY_E:             return e;
917         case KEY_F:             return f;
918         case KEY_G:             return g;
919         case KEY_H:             return h;
920         case KEY_I:             return i;
921         case KEY_J:             return j;
922         case KEY_K:             return k;
923         case KEY_L:             return l;
924         case KEY_M:             return m;
925         case KEY_N:             return n;
926         case KEY_O:             return o;
927         case KEY_P:             return p;
928         case KEY_Q:             return q;
929         case KEY_R:             return r;
930         case KEY_S:             return s;
931         case KEY_T:             return t;
932         case KEY_U:             return u;
933         case KEY_V:             return v;
934         case KEY_W:             return w;
935         case KEY_X:             return x;
936         case KEY_Y:             return y;
937         case KEY_Z:             return z;
938         case KEY_LEFT_BRACKET:  return leftBracket;
939         case KEY_BACKSLASH:     return backslash;
940         case KEY_RIGHT_BRACKET: return rightBracket;
941         case KEY_GRAVE:         return grave;
942         case KEY_SPACE:         return space;
943         case KEY_ESCAPE:        return escape;
944         case KEY_ENTER:         return enter;
945         case KEY_TAB:           return tab;
946         case KEY_BACKSPACE:     return backspace;
947         case KEY_INSERT:        return insert;
948         case KEY_DELETE:        return delete_;
949         case KEY_RIGHT:         return right;
950         case KEY_LEFT:          return left;
951         case KEY_DOWN:          return down;
952         case KEY_UP:            return up;
953         case KEY_PAGE_UP:       return pageUp;
954         case KEY_PAGE_DOWN:     return pageDown;
955         case KEY_HOME:          return home;
956         case KEY_END:           return end;
957         case KEY_CAPS_LOCK:     return capsLock;
958         case KEY_SCROLL_LOCK:   return scrollLock;
959         case KEY_NUM_LOCK:      return numLock;
960         case KEY_PRINT_SCREEN:  return printScreen;
961         case KEY_PAUSE:         return pause;
962         case KEY_F1:            return f1;
963         case KEY_F2:            return f2;
964         case KEY_F3:            return f3;
965         case KEY_F4:            return f4;
966         case KEY_F5:            return f5;
967         case KEY_F6:            return f6;
968         case KEY_F7:            return f7;
969         case KEY_F8:            return f8;
970         case KEY_F9:            return f9;
971         case KEY_F10:           return f10;
972         case KEY_F11:           return f11;
973         case KEY_F12:           return f12;
974         case KEY_LEFT_SHIFT:    return leftShift;
975         case KEY_LEFT_CONTROL:  return leftControl;
976         case KEY_LEFT_ALT:      return leftAlt;
977         case KEY_LEFT_SUPER:    return leftSuper;
978         case KEY_RIGHT_SHIFT:   return rightShift;
979         case KEY_RIGHT_CONTROL: return rightControl;
980         case KEY_RIGHT_ALT:     return rightAlt;
981         case KEY_RIGHT_SUPER:   return rightSuper;
982         case KEY_KB_MENU:       return contextMenu;
983         case KEY_KP_0:          return keypad0;
984         case KEY_KP_1:          return keypad1;
985         case KEY_KP_2:          return keypad2;
986         case KEY_KP_3:          return keypad3;
987         case KEY_KP_4:          return keypad4;
988         case KEY_KP_5:          return keypad5;
989         case KEY_KP_6:          return keypad6;
990         case KEY_KP_7:          return keypad7;
991         case KEY_KP_8:          return keypad8;
992         case KEY_KP_9:          return keypad9;
993         case KEY_KP_DECIMAL:    return keypadDecimal;
994         case KEY_KP_DIVIDE:     return keypadDivide;
995         case KEY_KP_MULTIPLY:   return keypadMultiply;
996         case KEY_KP_SUBTRACT:   return keypadSubtract;
997         case KEY_KP_ADD:        return keypadSum;
998         case KEY_KP_ENTER:      return keypadEnter;
999         case KEY_KP_EQUAL:      return keypadEqual;
1000         case KEY_BACK:          return androidBack;
1001         case KEY_MENU:          return androidMenu;
1002         case KEY_VOLUME_UP:     return volumeUp;
1003         case KEY_VOLUME_DOWN:   return volumeDown;
1004 
1005     }
1006 
1007 }
1008 
1009 enum ImageType : string {
1010 
1011     none = "",
1012     png = ".png",
1013     bmp = ".bmp",
1014     tga = ".tga",
1015     jpg = ".jpg",
1016     gif = ".gif",
1017     qoi = ".qoi",
1018     psd = ".psd",
1019     dds = ".dds",
1020     hdr = ".hdr",
1021     pic = ".pic",
1022     ktx = ".ktx",
1023     astc = ".astc",
1024     pkm = ".pkm",
1025     pvr = ".pvr",
1026 
1027 }
1028 
1029 /// Identify image type by contents.
1030 /// Params:
1031 ///     image = File data of the image to identify.
1032 /// Returns:
1033 ///     String containing the image extension, or an empty string indicating unknown file.
1034 ImageType identifyImageType(const ubyte[] data) {
1035 
1036     import std.algorithm : predSwitch;
1037     import std.conv : hexString;
1038 
1039 
1040     return data.predSwitch!"a.startsWith(cast(const ubyte[]) b)"(
1041         // Source: https://en.wikipedia.org/wiki/List_of_file_signatures
1042         hexString!"89 50 4E 47 0D 0A 1A 0A",             ImageType.png,
1043         hexString!"42 4D",                               ImageType.bmp,
1044         hexString!"FF D8 FF E0 00 10 4A 46 49 46 00 01", ImageType.jpg,
1045         hexString!"FF D8 FF EE",                         ImageType.jpg,
1046         hexString!"FF D8 FF E1",                         ImageType.jpg,
1047         hexString!"FF D8 FF E0",                         ImageType.jpg,
1048         hexString!"00 00 00 0C 6A 50 20 20 0D 0A 87 0A", ImageType.jpg,
1049         hexString!"FF 4F FF 51",                         ImageType.jpg,
1050         hexString!"47 49 46 38 37 61",                   ImageType.gif,
1051         hexString!"47 49 46 38 39 61",                   ImageType.gif,
1052         hexString!"71 6f 69 66",                         ImageType.qoi,
1053         hexString!"38 42 50 53",                         ImageType.psd,
1054         hexString!"23 3F 52 41 44 49 41 4E 43 45 0A",    ImageType.hdr,
1055         hexString!"6E 69 31 00",                         ImageType.hdr,
1056         hexString!"00",                                  ImageType.pic,
1057         // Source: https://en.wikipedia.org/wiki/DirectDraw_Surface
1058         hexString!"44 44 53 20",                         ImageType.dds,
1059         // Source: https://paulbourke.net/dataformats/ktx/
1060         hexString!"AB 4B 54 58 20 31 31 BB 0D 0A 1A 0A", ImageType.ktx,
1061         // Source: https://github.com/ARM-software/astc-encoder/blob/main/Docs/FileFormat.md
1062         hexString!"13 AB A1 5C",                         ImageType.astc,
1063         // Source: https://stackoverflow.com/questions/35881537/how-to-decode-this-image
1064         hexString!"50 4B 4D 20",                         ImageType.pkm,
1065         // Source: http://powervr-graphics.github.io/WebGL_SDK/WebGL_SDK/Documentation/Specifications/PVR%20File%20Format.Specification.pdf
1066         hexString!"03 52 56 50",                         ImageType.pvr,
1067         hexString!"50 56 52 03",                         ImageType.pvr,
1068         // Source: https://en.wikipedia.org/wiki/Truevision_TGA
1069         data.endsWith("TRUEVISION-XFILE.\0")
1070             ? ImageType.tga
1071             : ImageType.none,
1072     );
1073 
1074 }