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