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 }