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