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.algorithm;
13 
14 import fluid.backend;
15 import fluid.backend : MouseButton, KeyboardKey, GamepadButton;
16 
17 public import raylib : Vector2, Rectangle, Color;
18 
19 
20 @safe:
21 
22 
23 // Coordinate scaling will translate Fluid coordinates, where each pixels is 1/96th of an inch, to screen coordinates,
24 // making use of DPI information provided by the system. It is disabled on macOS, since the system already handles this
25 // for us.
26 version (OSX)
27     version = Fluid_DisableScaling;
28 
29 class Raylib5Backend : FluidBackend {
30 
31     private {
32 
33         TextureReaper _reaper;
34         FluidMouseCursor lastMouseCursor;
35         Rectangle drawArea;
36         float _scale = 1;
37 
38     }
39 
40     @trusted {
41 
42         bool isPressed(MouseButton button) const
43             => IsMouseButtonPressed(button.toRaylib);
44         bool isReleased(MouseButton button) const
45             => IsMouseButtonReleased(button.toRaylib);
46         bool isDown(MouseButton button) const
47             => IsMouseButtonDown(button.toRaylib);
48         bool isUp(MouseButton button) const
49             => IsMouseButtonUp(button.toRaylib);
50 
51         bool isPressed(KeyboardKey key) const => IsKeyPressed(key.toRaylib);
52         bool isReleased(KeyboardKey key) const => IsKeyReleased(key.toRaylib);
53         bool isDown(KeyboardKey key) const => IsKeyDown(key.toRaylib);
54         bool isUp(KeyboardKey key) const => IsKeyUp(key.toRaylib);
55         bool isRepeated(KeyboardKey key) const => IsKeyPressedRepeat(key.toRaylib);
56 
57         dchar inputCharacter() => cast(dchar) GetCharPressed();
58 
59         int isPressed(GamepadButton button) const {
60             auto btn = button.toRaylib;
61             return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonPressed(a, btn));
62         }
63 
64         int isReleased(GamepadButton button) const {
65             auto btn = button.toRaylib;
66             return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonReleased(a, btn));
67         }
68 
69         int isDown(GamepadButton button) const {
70             auto btn = button.toRaylib;
71             return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonDown(a, btn));
72         }
73 
74         int isUp(GamepadButton button) const {
75             auto btn = button.toRaylib;
76             return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonUp(a, btn));
77         }
78 
79         int isRepeated(GamepadButton button) const
80             => 0;
81 
82     }
83 
84     Vector2 mousePosition(Vector2 position) @trusted {
85 
86         auto positionRay = toRaylibCoords(position);
87         SetMousePosition(cast(int) positionRay.x, cast(int) positionRay.y);
88         return position;
89 
90     }
91 
92     Vector2 mousePosition() const @trusted {
93 
94         return toFluidCoords(GetMousePosition);
95 
96     }
97 
98     Vector2 scroll() const @trusted {
99 
100         // Normalize the value: Linux and Windows provide trinary values (-1, 0, 1) but macOS gives analog that often
101         // goes far higher than that. This is a rough guess of the proportions based on feeling.
102         version (OSX)
103             return -GetMouseWheelMoveV / 4;
104         else
105             return -GetMouseWheelMoveV;
106 
107     }
108 
109     float deltaTime() const @trusted {
110 
111         return GetFrameTime;
112 
113     }
114 
115     bool hasJustResized() const @trusted {
116 
117         // TODO detect and react to DPI changes
118         return IsWindowResized;
119 
120     }
121 
122     Vector2 windowSize(Vector2 size) @trusted {
123 
124         auto sizeRay = toRaylibCoords(size);
125         SetWindowSize(cast(int) sizeRay.x, cast(int) sizeRay.y);
126         return size;
127 
128     }
129 
130     Vector2 windowSize() const @trusted {
131 
132         return toFluidCoords(GetScreenWidth, GetScreenHeight);
133 
134     }
135 
136     float scale() const {
137 
138         return _scale;
139 
140     }
141 
142     float scale(float value) {
143 
144         return _scale = value;
145 
146     }
147 
148     Vector2 dpi() const @trusted {
149 
150         static Vector2 value;
151 
152         if (value == value.init) {
153 
154             value = GetWindowScaleDPI;
155             value.x *= 96;
156             value.y *= 96;
157 
158         }
159 
160         return value * _scale;
161 
162     }
163 
164     Vector2 toRaylibCoords(Vector2 position) const @trusted {
165 
166         version (Fluid_DisableScaling)
167             return position;
168         else
169             return Vector2(position.x * hidpiScale.x, position.y * hidpiScale.y);
170 
171     }
172 
173     Rectangle toRaylibCoords(Rectangle rec) const @trusted {
174 
175         version (Fluid_DisableScaling)
176             return rec;
177         else
178             return Rectangle(
179                 rec.x * hidpiScale.x,
180                 rec.y * hidpiScale.y,
181                 rec.width * hidpiScale.x,
182                 rec.height * hidpiScale.y,
183             );
184 
185     }
186 
187     Vector2 toFluidCoords(Vector2 position) const @trusted {
188 
189         version (Fluid_DisableScaling)
190             return position;
191         else
192             return Vector2(position.x / hidpiScale.x, position.y / hidpiScale.y);
193 
194     }
195 
196     Vector2 toFluidCoords(float x, float y) const @trusted {
197 
198         version (Fluid_DisableScaling)
199             return Vector2(x, y);
200         else
201             return Vector2(x / hidpiScale.x, y / hidpiScale.y);
202 
203     }
204 
205     Rectangle toFluidCoords(Rectangle rec) const @trusted {
206 
207         version (Fluid_DisableScaling)
208             return rec;
209         else
210             return Rectangle(
211                 rec.x / hidpiScale.x,
212                 rec.y / hidpiScale.y,
213                 rec.width / hidpiScale.x,
214                 rec.height / hidpiScale.y,
215             );
216 
217     }
218 
219     Rectangle area(Rectangle rect) @trusted {
220 
221         auto rectRay = toRaylibCoords(rect);
222 
223         BeginScissorMode(
224             cast(int) rectRay.x,
225             cast(int) rectRay.y,
226             cast(int) rectRay.width,
227             cast(int) rectRay.height,
228         );
229 
230         return drawArea = rect;
231 
232     }
233 
234     Rectangle area() const {
235 
236         if (drawArea is drawArea.init)
237             return Rectangle(0, 0, windowSize.tupleof);
238         else
239             return drawArea;
240 
241     }
242 
243     void restoreArea() @trusted {
244 
245         EndScissorMode();
246         drawArea = drawArea.init;
247 
248     }
249 
250     FluidMouseCursor mouseCursor(FluidMouseCursor cursor) @trusted {
251 
252         // Hide the cursor if requested
253         if (cursor.system == cursor.system.none) {
254             HideCursor();
255         }
256 
257         // Show the cursor
258         else {
259             SetMouseCursor(cursor.system.toRaylib);
260             ShowCursor();
261         }
262         return lastMouseCursor = cursor;
263 
264     }
265 
266     FluidMouseCursor mouseCursor() const {
267 
268         return lastMouseCursor;
269 
270     }
271 
272     TextureReaper* reaper() return scope {
273 
274         return &_reaper;
275 
276     }
277 
278     fluid.backend.Texture loadTexture(fluid.backend.Image image) @system {
279 
280         return fromRaylib(LoadTextureFromImage(image.toRaylib));
281 
282     }
283 
284     fluid.backend.Texture loadTexture(string filename) @system {
285 
286         import std.string;
287 
288         return fromRaylib(LoadTexture(filename.toStringz));
289 
290     }
291 
292     fluid.backend.Texture fromRaylib(raylib.Texture texture) {
293 
294         fluid.backend.Texture result;
295         result.id = texture.id;
296         result.tombstone = reaper.makeTombstone(this, result.id);
297         result.width = texture.width;
298         result.height = texture.height;
299         return result;
300 
301     }
302 
303     /// Destroy a texture
304     void unloadTexture(uint id) @system {
305 
306         if (!__ctfe && IsWindowReady && id != 0) {
307 
308             rlUnloadTexture(id);
309 
310         }
311 
312     }
313 
314     void drawLine(Vector2 start, Vector2 end, Color color) @trusted {
315 
316         DrawLineV(toRaylibCoords(start), toRaylibCoords(end), color);
317 
318     }
319 
320     void drawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color) @trusted {
321 
322         DrawTriangle(toRaylibCoords(a), toRaylibCoords(b), toRaylibCoords(c), color);
323 
324     }
325 
326     void drawRectangle(Rectangle rectangle, Color color) @trusted {
327 
328         DrawRectangleRec(toRaylibCoords(rectangle), color);
329 
330     }
331 
332     void drawTexture(fluid.backend.Texture texture, Rectangle rectangle, Color tint, string alt = "")
333     in (false)
334     do {
335 
336         // TODO filtering?
337         drawTexture(texture, rectangle, tint, alt, true);
338 
339     }
340 
341     void drawTextureAlign(fluid.backend.Texture texture, Rectangle rectangle, Color tint, string alt = "")
342     in (false)
343     do {
344 
345         drawTexture(texture, rectangle, tint, alt, true);
346 
347     }
348 
349     protected @trusted
350     void drawTexture(fluid.backend.Texture texture, Rectangle destination, Color tint, string alt, bool alignPixels)
351     do {
352 
353         import std.math;
354 
355         destination = toRaylibCoords(destination);
356 
357         // Align texture to pixel boundaries
358         if (alignPixels) {
359             destination.x = floor(destination.x);
360             destination.y = floor(destination.y);
361         }
362 
363         const dpi = this.dpi;
364         const source = Rectangle(0, 0, texture.width, texture.height);
365 
366         DrawTexturePro(texture.toRaylib, source, destination, Vector2(0, 0), 0, tint);
367 
368     }
369 
370 }
371 
372 /// Get the Raylib enum for a mouse cursor.
373 raylib.MouseCursor toRaylib(FluidMouseCursor.SystemCursors cursor) {
374 
375     with (raylib.MouseCursor)
376     with (FluidMouseCursor.SystemCursors)
377     switch (cursor) {
378 
379         default:
380         case none:
381         case systemDefault:
382             return MOUSE_CURSOR_DEFAULT;
383 
384         case pointer:
385             return MOUSE_CURSOR_POINTING_HAND;
386 
387         case crosshair:
388             return MOUSE_CURSOR_CROSSHAIR;
389 
390         case text:
391             return MOUSE_CURSOR_IBEAM;
392 
393         case allScroll:
394             return MOUSE_CURSOR_RESIZE_ALL;
395 
396         case resizeEW:
397             return MOUSE_CURSOR_RESIZE_EW;
398 
399         case resizeNS:
400             return MOUSE_CURSOR_RESIZE_NS;
401 
402         case resizeNESW:
403             return MOUSE_CURSOR_RESIZE_NESW;
404 
405         case resizeNWSE:
406             return MOUSE_CURSOR_RESIZE_NWSE;
407 
408         case notAllowed:
409             return MOUSE_CURSOR_NOT_ALLOWED;
410 
411     }
412 
413 }
414 
415 /// Get the Raylib enum for a keyboard key.
416 raylib.KeyboardKey toRaylib(KeyboardKey key) {
417 
418     return cast(raylib.KeyboardKey) key;
419 
420 }
421 
422 /// Get the Raylib enum for a mouse button.
423 raylib.MouseButton toRaylib(MouseButton button) {
424 
425     with (raylib.MouseButton)
426     with (MouseButton)
427     final switch (button) {
428         case none:    assert(false);
429         case left:    return MOUSE_BUTTON_LEFT;
430         case right:   return MOUSE_BUTTON_RIGHT;
431         case middle:  return MOUSE_BUTTON_MIDDLE;
432         case extra1:  return MOUSE_BUTTON_SIDE;
433         case extra2:  return MOUSE_BUTTON_EXTRA;
434         case forward: return MOUSE_BUTTON_FORWARD;
435         case back:    return MOUSE_BUTTON_BACK;
436     }
437 
438 }
439 
440 /// Get the Raylib enum for a keyboard key.
441 raylib.GamepadButton toRaylib(GamepadButton button) {
442 
443     return cast(raylib.GamepadButton) button;
444 
445 }
446 
447 /// Convert image to a Raylib image. Do not call `UnloadImage` on the result.
448 raylib.Image toRaylib(fluid.backend.Image image) @trusted {
449 
450     raylib.Image result;
451     result.data = cast(void*) image.pixels.ptr;
452     result.width = image.width;
453     result.height = image.height;
454     result.format = PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
455     result.mipmaps = 1;
456 
457     return result;
458 
459 }
460 
461 /// Convert a fluid texture to a Raylib texture.
462 raylib.Texture toRaylib(fluid.backend.Texture texture) @trusted {
463 
464     raylib.Texture result;
465     result.id = texture.id;
466     result.width = texture.width;
467     result.height = texture.height;
468     result.format = PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
469     result.mipmaps = 1;
470 
471     return result;
472 
473 }