1 module fluid.backend.raylib5;
2 
3 version (Have_raylib_d):
4 
5 debug (Fluid_BuildMessages) {
6     pragma(msg, "Fluid: Building with Raylib 5 support (Raylib5Backend)");
7 }
8 
9 import raylib;
10 
11 import std.range;
12 import std.string;
13 import std.algorithm;
14 
15 import fluid.node;
16 import fluid.backend;
17 import fluid.backend : MouseButton, KeyboardKey, GamepadButton;
18 
19 public import raylib : Vector2, Rectangle, Color;
20 public static import raylib;
21 
22 static if (!__traits(compiles, IsShaderReady))
23     private alias IsShaderReady = IsShaderValid;
24 
25 @safe:
26 
27 
28 // Coordinate scaling will translate Fluid coordinates, where each pixels is 1/96th of an inch, to screen coordinates,
29 // making use of DPI information provided by the system. This flag is only set on macOS, where the system handles this
30 // automatically.
31 version (OSX)
32     version = Fluid_DisableScaling;
33 
34 class Raylib5Backend : FluidBackend, FluidEntrypointBackend {
35 
36     private {
37 
38         TextureReaper _reaper;
39         FluidMouseCursor lastMouseCursor;
40         Rectangle drawArea;
41         Color _tint = Color(0xff, 0xff, 0xff, 0xff);
42         float _scale = 1;
43         Shader _alphaImageShader;
44         Shader _palettedAlphaImageShader;
45         int _palettedAlphaImageShader_palette;
46         fluid.backend.Texture _paletteTexture;
47         int[uint] _mipmapCount;
48 
49     }
50 
51     /// Shader code for alpha images.
52     enum alphaImageShaderCode = q{
53         #version 330
54         in vec2 fragTexCoord;
55         in vec4 fragColor;
56         out vec4 finalColor;
57         uniform sampler2D texture0;
58         uniform vec4 colDiffuse;
59         void main() {
60             // Alpha masks are white to make them practical for modulation
61             vec4 texelColor = texture(texture0, fragTexCoord);
62             finalColor = vec4(1, 1, 1, texelColor.r) * colDiffuse * fragColor;
63         }
64     };
65 
66     /// Shader code for palette iamges.
67     enum palettedAlphaImageShaderCode = q{
68         #version 330
69         in vec2 fragTexCoord;
70         in vec4 fragColor;
71         out vec4 finalColor;
72         uniform sampler2D texture0;
73         uniform sampler2D palette;
74         uniform vec4 colDiffuse;
75         void main() {
76             // index.a is alpha/opacity
77             // index.r is palette index
78             vec4 index = texture(texture0, fragTexCoord);
79             vec4 texel = texture(palette, vec2(index.r, 0));
80             finalColor = texel * vec4(1, 1, 1, index.a) * colDiffuse * fragColor;
81         }
82     };
83 
84     @trusted {
85 
86         bool isPressed(MouseButton button) const {
87             return IsMouseButtonPressed(button.toRaylib);
88         }
89         bool isReleased(MouseButton button) const {
90             return IsMouseButtonReleased(button.toRaylib);
91         }
92         bool isDown(MouseButton button) const {
93             return IsMouseButtonDown(button.toRaylib);
94         }
95         bool isUp(MouseButton button) const {
96             return IsMouseButtonUp(button.toRaylib);
97         }
98 
99         bool isPressed(KeyboardKey key) const {
100             return IsKeyPressed(key.toRaylib);
101         }
102         bool isReleased(KeyboardKey key) const {
103             return IsKeyReleased(key.toRaylib);
104         }
105         bool isDown(KeyboardKey key) const {
106             return IsKeyDown(key.toRaylib);
107         }
108         bool isUp(KeyboardKey key) const {
109             return IsKeyUp(key.toRaylib);
110         }
111         bool isRepeated(KeyboardKey key) const {
112             return IsKeyPressedRepeat(key.toRaylib);
113         }
114 
115         dchar inputCharacter() {
116             return cast(dchar) GetCharPressed();
117         }
118 
119         int isPressed(GamepadButton button) const {
120             auto btn = button.toRaylib;
121             return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonPressed(a, btn));
122         }
123 
124         int isReleased(GamepadButton button) const {
125             auto btn = button.toRaylib;
126             return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonReleased(a, btn));
127         }
128 
129         int isDown(GamepadButton button) const {
130             auto btn = button.toRaylib;
131             return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonDown(a, btn));
132         }
133 
134         int isUp(GamepadButton button) const {
135             auto btn = button.toRaylib;
136             return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonUp(a, btn));
137         }
138 
139         int isRepeated(GamepadButton button) const {
140             return 0;
141         }
142 
143     }
144 
145     ~this() @trusted {
146 
147         if (IsWindowReady()) {
148 
149             UnloadShader(_alphaImageShader);
150             UnloadShader(_palettedAlphaImageShader);
151             _paletteTexture.destroy();
152 
153         }
154 
155     }
156 
157     bool opEquals(FluidBackend other) const {
158 
159         return this is other;
160 
161     }
162 
163     void run(Node root) @trusted {
164 
165         // Prepare the window
166         SetConfigFlags(ConfigFlags.FLAG_WINDOW_RESIZABLE | ConfigFlags.FLAG_WINDOW_HIDDEN);
167         SetTraceLogLevel(TraceLogLevel.LOG_WARNING);
168         InitWindow(0, 0, "");
169         SetTargetFPS(60);
170         scope (exit) CloseWindow();
171 
172         void draw() {
173             BeginDrawing();
174             ClearBackground(color("fff"));
175             root.draw();
176             EndDrawing();
177         }
178 
179         // Probe the node for information
180         draw();
181 
182         // Set window size
183         auto min = root.getMinSize;
184         int minX = cast(int) min.x;
185         int minY = cast(int) min.y;
186         SetWindowMinSize(minX, minY);
187         SetWindowSize(minX, minY);
188 
189         // Now draw
190         ClearWindowState(ConfigFlags.FLAG_WINDOW_HIDDEN);
191 
192         // Event loop
193         while (!WindowShouldClose) {
194 
195             draw();
196 
197             // Update minimum size if needed
198             min = root.getMinSize;
199             auto newMinX = cast(int) min.x;
200             auto newMinY = cast(int) min.y;
201             if (newMinX != minX || newMinY != minY) {
202 
203                 SetWindowMinSize(
204                     minX = newMinX,
205                     minY = newMinY);
206                 SetWindowSize(
207                     max(minX, GetScreenWidth),
208                     max(minY, GetScreenHeight));
209 
210             }
211 
212         }
213 
214 
215     }
216 
217     /// Get shader for images with the `alpha` format.
218     raylib.Shader alphaImageShader() @trusted {
219 
220         // Shader created and available for use
221         if (IsShaderReady(_alphaImageShader))
222             return _alphaImageShader;
223 
224         // Create the shader
225         return _alphaImageShader = LoadShaderFromMemory(null, alphaImageShaderCode.ptr);
226 
227     }
228 
229     /// Get shader for images with the `palettedAlpha` format.
230     /// Params:
231     ///     palette = Palette to use with the shader.
232     raylib.Shader palettedAlphaImageShader(Color[] palette) @trusted {
233 
234         // Load the shader
235         if (!IsShaderReady(_palettedAlphaImageShader)) {
236 
237             _palettedAlphaImageShader = LoadShaderFromMemory(null, palettedAlphaImageShaderCode.ptr);
238             _palettedAlphaImageShader_palette = GetShaderLocation(_palettedAlphaImageShader, "palette");
239 
240         }
241 
242         auto paletteTexture = this.paletteTexture(palette);
243 
244         // Load the palette
245         SetShaderValueTexture(_palettedAlphaImageShader, _palettedAlphaImageShader_palette, paletteTexture.toRaylib);
246 
247         return _palettedAlphaImageShader;
248 
249     }
250 
251     Vector2 mousePosition(Vector2 position) @trusted {
252 
253         auto positionRay = toRaylibCoords(position);
254         SetMousePosition(cast(int) positionRay.x, cast(int) positionRay.y);
255         return position;
256 
257     }
258 
259     Vector2 mousePosition() const @trusted {
260 
261         return fromRaylibCoords(GetMousePosition);
262 
263     }
264 
265     Vector2 scroll() const @trusted {
266 
267         // Normalize the value: Linux and Windows provide trinary values (-1, 0, 1) but macOS gives analog that often
268         // goes far higher than that. This is a rough guess of the proportions based on feeling.
269         version (OSX)
270             return -GetMouseWheelMoveV / 4;
271         else
272             return -GetMouseWheelMoveV;
273 
274     }
275 
276     string clipboard(string value) @trusted {
277 
278         SetClipboardText(value.toStringz);
279 
280         return value;
281 
282     }
283 
284     string clipboard() const @trusted {
285 
286         return GetClipboardText().fromStringz.dup;
287 
288     }
289 
290     float deltaTime() const @trusted {
291 
292         return GetFrameTime;
293 
294     }
295 
296     bool hasJustResized() const @trusted {
297 
298         // TODO detect and react to DPI changes
299         return IsWindowResized;
300 
301     }
302 
303     Vector2 windowSize(Vector2 size) @trusted {
304 
305         auto sizeRay = toRaylibCoords(size);
306         SetWindowSize(cast(int) sizeRay.x, cast(int) sizeRay.y);
307         return size;
308 
309     }
310 
311     Vector2 windowSize() const @trusted {
312 
313         return fromRaylibCoords(GetScreenWidth, GetScreenHeight);
314 
315     }
316 
317     float scale() const {
318 
319         return _scale;
320 
321     }
322 
323     float scale(float value) {
324 
325         return _scale = value;
326 
327     }
328 
329     Vector2 dpi() const @trusted {
330 
331         import fluid.io.canvas : getGlobalScale;
332 
333         static Vector2 value;
334 
335         if (value == value.init) {
336 
337             const globalScale = getGlobalScale();
338 
339             value = GetWindowScaleDPI;
340             value.x *= 96 * globalScale.x;
341             value.y *= 96 * globalScale.y;
342 
343         }
344 
345         return value * _scale;
346 
347     }
348 
349     Vector2 toRaylibCoords(Vector2 position) const @trusted {
350 
351         version (Fluid_DisableScaling)
352             return position;
353         else
354             return Vector2(position.x * hidpiScale.x, position.y * hidpiScale.y);
355 
356     }
357 
358     Rectangle toRaylibCoords(Rectangle rec) const @trusted {
359 
360         version (Fluid_DisableScaling)
361             return rec;
362         else
363             return Rectangle(
364                 rec.x * hidpiScale.x,
365                 rec.y * hidpiScale.y,
366                 rec.width * hidpiScale.x,
367                 rec.height * hidpiScale.y,
368             );
369 
370     }
371 
372     Vector2 fromRaylibCoords(Vector2 position) const @trusted {
373 
374         version (Fluid_DisableScaling)
375             return position;
376         else
377             return Vector2(position.x / hidpiScale.x, position.y / hidpiScale.y);
378 
379     }
380 
381     Vector2 fromRaylibCoords(float x, float y) const @trusted {
382 
383         version (Fluid_DisableScaling)
384             return Vector2(x, y);
385         else
386             return Vector2(x / hidpiScale.x, y / hidpiScale.y);
387 
388     }
389 
390     Rectangle fromRaylibCoords(Rectangle rec) const @trusted {
391 
392         version (Fluid_DisableScaling)
393             return rec;
394         else
395             return Rectangle(
396                 rec.x / hidpiScale.x,
397                 rec.y / hidpiScale.y,
398                 rec.width / hidpiScale.x,
399                 rec.height / hidpiScale.y,
400             );
401 
402     }
403 
404     Rectangle area(Rectangle rect) @trusted {
405 
406         auto rectRay = toRaylibCoords(rect);
407 
408         BeginScissorMode(
409             cast(int) rectRay.x,
410             cast(int) rectRay.y,
411             cast(int) rectRay.width,
412             cast(int) rectRay.height,
413         );
414 
415         return drawArea = rect;
416 
417     }
418 
419     Rectangle area() const {
420 
421         if (drawArea is drawArea.init)
422             return Rectangle(0, 0, windowSize.tupleof);
423         else
424             return drawArea;
425 
426     }
427 
428     void restoreArea() @trusted {
429 
430         EndScissorMode();
431         drawArea = drawArea.init;
432 
433     }
434 
435     FluidMouseCursor mouseCursor(FluidMouseCursor cursor) @trusted {
436 
437         // Hide the cursor if requested
438         if (cursor.system == cursor.system.none) {
439             HideCursor();
440         }
441 
442         // Show the cursor
443         else {
444             SetMouseCursor(cursor.system.toRaylib);
445             ShowCursor();
446         }
447         return lastMouseCursor = cursor;
448 
449     }
450 
451     FluidMouseCursor mouseCursor() const {
452 
453         return lastMouseCursor;
454 
455     }
456 
457     TextureReaper* reaper() return scope {
458 
459         return &_reaper;
460 
461     }
462 
463     fluid.backend.Texture loadTexture(fluid.backend.Image image) @system {
464 
465         return fromRaylib(LoadTextureFromImage(image.toRaylib), image.format);
466 
467     }
468 
469     fluid.backend.Texture loadTexture(string filename) @system {
470 
471         import std.string;
472 
473         return fromRaylib(LoadTexture(filename.toStringz), fluid.backend.Image.Format.rgba);
474 
475     }
476 
477     void updateTexture(fluid.backend.Texture texture, fluid.backend.Image image) @system
478     in (false)
479     do {
480 
481         UpdateTexture(texture.toRaylib, image.data.ptr);
482 
483     }
484 
485     protected fluid.backend.Texture fromRaylib(raylib.Texture rayTexture, fluid.backend.Image.Format format) @system {
486 
487         fluid.backend.Texture result;
488         result.id = rayTexture.id;
489         result.format = format;
490         result.tombstone = reaper.makeTombstone(this, result.id);
491         result.width = rayTexture.width;
492         result.height = rayTexture.height;
493         return result;
494 
495     }
496 
497     /// Destroy a texture
498     void unloadTexture(uint id) @system {
499 
500         if (!__ctfe && IsWindowReady && id != 0) {
501 
502             _mipmapCount.remove(id);
503             rlUnloadTexture(id);
504 
505         }
506 
507     }
508 
509     Color tint(Color color) {
510 
511         return _tint = color;
512 
513     }
514 
515     Color tint() const {
516 
517         return _tint;
518 
519     }
520 
521     void drawLine(Vector2 start, Vector2 end, Color color) @trusted {
522 
523         DrawLineV(toRaylibCoords(start), toRaylibCoords(end), multiply(color, tint));
524 
525     }
526 
527     void drawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color) @trusted {
528 
529         DrawTriangle(toRaylibCoords(a), toRaylibCoords(b), toRaylibCoords(c), multiply(color, tint));
530 
531     }
532 
533     void drawCircle(Vector2 center, float radius, Color color) @trusted {
534 
535         DrawCircleV(toRaylibCoords(center), radius * hidpiScale.y, multiply(color, tint));
536 
537     }
538 
539     void drawCircleOutline(Vector2 center, float radius, Color color) @trusted {
540 
541         DrawCircleLinesV(toRaylibCoords(center), radius * hidpiScale.y, multiply(color, tint));
542 
543     }
544 
545     void drawRectangle(Rectangle rectangle, Color color) @trusted {
546 
547         DrawRectangleRec(toRaylibCoords(rectangle), multiply(color, tint));
548 
549     }
550 
551     void drawTexture(fluid.backend.Texture texture, Rectangle rectangle, Color tint)
552         @trusted
553     in (false)
554     do {
555 
556         auto rayTexture = texture.toRaylib;
557 
558         // Ensure the texture has mipmaps, if possible, to enable trilinear filtering
559         if (auto mipmapCount = texture.id in _mipmapCount) {
560 
561             rayTexture.mipmaps = *mipmapCount;
562 
563         }
564 
565         else {
566 
567             // Generate mipmaps
568             GenTextureMipmaps(&rayTexture);
569             _mipmapCount[texture.id] = rayTexture.mipmaps;
570 
571         }
572 
573         // Set filter accordingly
574         const filter = rayTexture.mipmaps == 1
575             ? TextureFilter.TEXTURE_FILTER_BILINEAR
576             : TextureFilter.TEXTURE_FILTER_TRILINEAR;
577 
578         SetTextureFilter(rayTexture, filter);
579         drawTexture(texture, rectangle, tint, false);
580 
581     }
582 
583     void drawTextureAlign(fluid.backend.Texture texture, Rectangle rectangle, Color tint)
584         @trusted
585     in (false)
586     do {
587 
588         auto rayTexture = texture.toRaylib;
589 
590         SetTextureFilter(rayTexture, TextureFilter.TEXTURE_FILTER_POINT);
591         drawTexture(texture, rectangle, tint, true);
592 
593     }
594 
595     protected @trusted
596     void drawTexture(fluid.backend.Texture texture, Rectangle destination, Color tint, bool alignPixels)
597     do {
598 
599         import std.math;
600 
601         // Align texture to pixel boundaries
602         if (alignPixels) {
603             destination.x = floor(destination.x * hidpiScale.x) / hidpiScale.x;
604             destination.y = floor(destination.y * hidpiScale.y) / hidpiScale.y;
605         }
606 
607         destination = toRaylibCoords(destination);
608 
609         const source = Rectangle(0, 0, texture.width, texture.height);
610         Shader shader;
611 
612         // Enable shaders relevant to given format
613         switch (texture.format) {
614 
615             case fluid.backend.Image.Format.alpha:
616                 shader = alphaImageShader;
617                 break;
618 
619             case fluid.backend.Image.Format.palettedAlpha:
620                 shader = palettedAlphaImageShader(texture.palette);
621                 break;
622 
623             default: break;
624 
625         }
626 
627         // Start shaders, if applicable
628         if (IsShaderReady(shader))
629             BeginShaderMode(shader);
630 
631         DrawTexturePro(texture.toRaylib, source, destination, Vector2(0, 0), 0, multiply(tint, this.tint));
632 
633         // End shaders
634         if (IsShaderReady(shader))
635             EndShaderMode();
636 
637     }
638 
639     /// Create a palette texture.
640     private fluid.backend.Texture paletteTexture(scope Color[] colors) @trusted
641     in (colors.length <= 256, "There can only be at most 256 colors in a palette.")
642     do {
643 
644         // Fill empty slots in the palette with white
645         Color[256] allColors = color("#fff");
646         allColors[0 .. colors.length] = colors;
647 
648         // Prepare an image for the texture
649         scope image = fluid.backend.Image(allColors[], 256, 1);
650 
651         // Create the texture if it doesn't exist
652         if (_paletteTexture is _paletteTexture.init)
653             _paletteTexture = loadTexture(image);
654 
655         // Or, update existing palette image
656         else
657             updateTexture(_paletteTexture, image);
658 
659         return _paletteTexture;
660 
661     }
662 
663 }
664 
665 /// Get the Raylib enum for a mouse cursor.
666 raylib.MouseCursor toRaylib(FluidMouseCursor.SystemCursors cursor) {
667 
668     with (raylib.MouseCursor)
669     with (FluidMouseCursor.SystemCursors)
670     switch (cursor) {
671 
672         default:
673         case none:
674         case systemDefault:
675             return MOUSE_CURSOR_DEFAULT;
676 
677         case pointer:
678             return MOUSE_CURSOR_POINTING_HAND;
679 
680         case crosshair:
681             return MOUSE_CURSOR_CROSSHAIR;
682 
683         case text:
684             return MOUSE_CURSOR_IBEAM;
685 
686         case allScroll:
687             return MOUSE_CURSOR_RESIZE_ALL;
688 
689         case resizeEW:
690             return MOUSE_CURSOR_RESIZE_EW;
691 
692         case resizeNS:
693             return MOUSE_CURSOR_RESIZE_NS;
694 
695         case resizeNESW:
696             return MOUSE_CURSOR_RESIZE_NESW;
697 
698         case resizeNWSE:
699             return MOUSE_CURSOR_RESIZE_NWSE;
700 
701         case notAllowed:
702             return MOUSE_CURSOR_NOT_ALLOWED;
703 
704     }
705 
706 }
707 
708 /// Get the Raylib enum for a keyboard key.
709 raylib.KeyboardKey toRaylib(KeyboardKey key) {
710 
711     return cast(raylib.KeyboardKey) key;
712 
713 }
714 
715 /// Get the Raylib enum for a mouse button.
716 raylib.MouseButton toRaylib(MouseButton button) {
717 
718     with (raylib.MouseButton)
719     with (MouseButton)
720     final switch (button) {
721         case none:    assert(false);
722         case left:    return MOUSE_BUTTON_LEFT;
723         case right:   return MOUSE_BUTTON_RIGHT;
724         case middle:  return MOUSE_BUTTON_MIDDLE;
725         case extra1:  return MOUSE_BUTTON_SIDE;
726         case extra2:  return MOUSE_BUTTON_EXTRA;
727         case forward: return MOUSE_BUTTON_FORWARD;
728         case back:    return MOUSE_BUTTON_BACK;
729     }
730 
731 }
732 
733 /// Get the Raylib enum for a keyboard key.
734 raylib.GamepadButton toRaylib(GamepadButton button) {
735 
736     return cast(raylib.GamepadButton) button;
737 
738 }
739 
740 /// Convert image to a Raylib image. Do not call `UnloadImage` on the result.
741 raylib.Image toRaylib(fluid.backend.Image image) nothrow @trusted {
742 
743     raylib.Image result;
744     result.data = image.data.ptr;
745     result.width = image.width;
746     result.height = image.height;
747     result.format = image.format.toRaylib;
748     result.mipmaps = 1;
749     return result;
750 
751 }
752 
753 /// Convert Fluid image format to Raylib's closest alternative.
754 raylib.PixelFormat toRaylib(fluid.backend.Image.Format imageFormat) nothrow {
755 
756     final switch (imageFormat) {
757 
758         case imageFormat.rgba:
759             return PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
760 
761         case imageFormat.palettedAlpha:
762             return PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA;
763 
764         case imageFormat.alpha:
765             return PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE;
766 
767     }
768 
769 }
770 
771 /// Convert a fluid texture to a Raylib texture.
772 raylib.Texture toRaylib(fluid.backend.Texture texture) @trusted {
773 
774     raylib.Texture result;
775     result.id = texture.id;
776     result.width = texture.width;
777     result.height = texture.height;
778     result.format = texture.format.toRaylib;
779     result.mipmaps = 1;
780 
781     return result;
782 
783 }
784 
785 /// Convert a Raylib texture to a Fluid texture
786 fluid.backend.Texture toFluid(raylib.Texture rayTexture) @system {
787     fluid.backend.Texture result;
788     result.id = rayTexture.id;
789     result.format = fluid.backend.Image.Format.rgba;
790     result.width = rayTexture.width;
791     result.height = rayTexture.height;
792 
793     return result;
794 }