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 }