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 }