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 (Raylib5Backend)"); 7 } 8 9 import raylib; 10 11 import std.range; 12 import std.string; 13 import std.algorithm; 14 15 import fluid.node; 16 import fluid.backend; 17 import fluid.backend : MouseButton, KeyboardKey, GamepadButton; 18 19 public import raylib : Vector2, Rectangle, Color; 20 public static import raylib; 21 22 static if (!__traits(compiles, IsShaderReady)) 23 private alias IsShaderReady = IsShaderValid; 24 25 @safe: 26 27 28 // Coordinate scaling will translate Fluid coordinates, where each pixels is 1/96th of an inch, to screen coordinates, 29 // making use of DPI information provided by the system. This flag is only set on macOS, where the system handles this 30 // automatically. 31 version (OSX) 32 version = Fluid_DisableScaling; 33 34 class Raylib5Backend : FluidBackend, FluidEntrypointBackend { 35 36 private { 37 38 TextureReaper _reaper; 39 FluidMouseCursor lastMouseCursor; 40 Rectangle drawArea; 41 Color _tint = Color(0xff, 0xff, 0xff, 0xff); 42 float _scale = 1; 43 Shader _alphaImageShader; 44 Shader _palettedAlphaImageShader; 45 int _palettedAlphaImageShader_palette; 46 fluid.backend.Texture _paletteTexture; 47 int[uint] _mipmapCount; 48 49 } 50 51 /// Shader code for alpha images. 52 enum alphaImageShaderCode = q{ 53 #version 330 54 in vec2 fragTexCoord; 55 in vec4 fragColor; 56 out vec4 finalColor; 57 uniform sampler2D texture0; 58 uniform vec4 colDiffuse; 59 void main() { 60 // Alpha masks are white to make them practical for modulation 61 vec4 texelColor = texture(texture0, fragTexCoord); 62 finalColor = vec4(1, 1, 1, texelColor.r) * colDiffuse * fragColor; 63 } 64 }; 65 66 /// Shader code for palette iamges. 67 enum palettedAlphaImageShaderCode = q{ 68 #version 330 69 in vec2 fragTexCoord; 70 in vec4 fragColor; 71 out vec4 finalColor; 72 uniform sampler2D texture0; 73 uniform sampler2D palette; 74 uniform vec4 colDiffuse; 75 void main() { 76 // index.a is alpha/opacity 77 // index.r is palette index 78 vec4 index = texture(texture0, fragTexCoord); 79 vec4 texel = texture(palette, vec2(index.r, 0)); 80 finalColor = texel * vec4(1, 1, 1, index.a) * colDiffuse * fragColor; 81 } 82 }; 83 84 @trusted { 85 86 bool isPressed(MouseButton button) const { 87 return IsMouseButtonPressed(button.toRaylib); 88 } 89 bool isReleased(MouseButton button) const { 90 return IsMouseButtonReleased(button.toRaylib); 91 } 92 bool isDown(MouseButton button) const { 93 return IsMouseButtonDown(button.toRaylib); 94 } 95 bool isUp(MouseButton button) const { 96 return IsMouseButtonUp(button.toRaylib); 97 } 98 99 bool isPressed(KeyboardKey key) const { 100 return IsKeyPressed(key.toRaylib); 101 } 102 bool isReleased(KeyboardKey key) const { 103 return IsKeyReleased(key.toRaylib); 104 } 105 bool isDown(KeyboardKey key) const { 106 return IsKeyDown(key.toRaylib); 107 } 108 bool isUp(KeyboardKey key) const { 109 return IsKeyUp(key.toRaylib); 110 } 111 bool isRepeated(KeyboardKey key) const { 112 return IsKeyPressedRepeat(key.toRaylib); 113 } 114 115 dchar inputCharacter() { 116 return cast(dchar) GetCharPressed(); 117 } 118 119 int isPressed(GamepadButton button) const { 120 auto btn = button.toRaylib; 121 return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonPressed(a, btn)); 122 } 123 124 int isReleased(GamepadButton button) const { 125 auto btn = button.toRaylib; 126 return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonReleased(a, btn)); 127 } 128 129 int isDown(GamepadButton button) const { 130 auto btn = button.toRaylib; 131 return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonDown(a, btn)); 132 } 133 134 int isUp(GamepadButton button) const { 135 auto btn = button.toRaylib; 136 return 1 + cast(int) iota(0, 4).countUntil!(a => IsGamepadButtonUp(a, btn)); 137 } 138 139 int isRepeated(GamepadButton button) const { 140 return 0; 141 } 142 143 } 144 145 ~this() @trusted { 146 147 if (IsWindowReady()) { 148 149 UnloadShader(_alphaImageShader); 150 UnloadShader(_palettedAlphaImageShader); 151 _paletteTexture.destroy(); 152 153 } 154 155 } 156 157 bool opEquals(FluidBackend other) const { 158 159 return this is other; 160 161 } 162 163 void run(Node root) @trusted { 164 165 // Prepare the window 166 SetConfigFlags(ConfigFlags.FLAG_WINDOW_RESIZABLE | ConfigFlags.FLAG_WINDOW_HIDDEN); 167 SetTraceLogLevel(TraceLogLevel.LOG_WARNING); 168 InitWindow(0, 0, ""); 169 SetTargetFPS(60); 170 scope (exit) CloseWindow(); 171 172 void draw() { 173 BeginDrawing(); 174 ClearBackground(color("fff")); 175 root.draw(); 176 EndDrawing(); 177 } 178 179 // Probe the node for information 180 draw(); 181 182 // Set window size 183 auto min = root.getMinSize; 184 int minX = cast(int) min.x; 185 int minY = cast(int) min.y; 186 SetWindowMinSize(minX, minY); 187 SetWindowSize(minX, minY); 188 189 // Now draw 190 ClearWindowState(ConfigFlags.FLAG_WINDOW_HIDDEN); 191 192 // Event loop 193 while (!WindowShouldClose) { 194 195 draw(); 196 197 // Update minimum size if needed 198 min = root.getMinSize; 199 auto newMinX = cast(int) min.x; 200 auto newMinY = cast(int) min.y; 201 if (newMinX != minX || newMinY != minY) { 202 203 SetWindowMinSize( 204 minX = newMinX, 205 minY = newMinY); 206 SetWindowSize( 207 max(minX, GetScreenWidth), 208 max(minY, GetScreenHeight)); 209 210 } 211 212 } 213 214 215 } 216 217 /// Get shader for images with the `alpha` format. 218 raylib.Shader alphaImageShader() @trusted { 219 220 // Shader created and available for use 221 if (IsShaderReady(_alphaImageShader)) 222 return _alphaImageShader; 223 224 // Create the shader 225 return _alphaImageShader = LoadShaderFromMemory(null, alphaImageShaderCode.ptr); 226 227 } 228 229 /// Get shader for images with the `palettedAlpha` format. 230 /// Params: 231 /// palette = Palette to use with the shader. 232 raylib.Shader palettedAlphaImageShader(Color[] palette) @trusted { 233 234 // Load the shader 235 if (!IsShaderReady(_palettedAlphaImageShader)) { 236 237 _palettedAlphaImageShader = LoadShaderFromMemory(null, palettedAlphaImageShaderCode.ptr); 238 _palettedAlphaImageShader_palette = GetShaderLocation(_palettedAlphaImageShader, "palette"); 239 240 } 241 242 auto paletteTexture = this.paletteTexture(palette); 243 244 // Load the palette 245 SetShaderValueTexture(_palettedAlphaImageShader, _palettedAlphaImageShader_palette, paletteTexture.toRaylib); 246 247 return _palettedAlphaImageShader; 248 249 } 250 251 Vector2 mousePosition(Vector2 position) @trusted { 252 253 auto positionRay = toRaylibCoords(position); 254 SetMousePosition(cast(int) positionRay.x, cast(int) positionRay.y); 255 return position; 256 257 } 258 259 Vector2 mousePosition() const @trusted { 260 261 return fromRaylibCoords(GetMousePosition); 262 263 } 264 265 Vector2 scroll() const @trusted { 266 267 // Normalize the value: Linux and Windows provide trinary values (-1, 0, 1) but macOS gives analog that often 268 // goes far higher than that. This is a rough guess of the proportions based on feeling. 269 version (OSX) 270 return -GetMouseWheelMoveV / 4; 271 else 272 return -GetMouseWheelMoveV; 273 274 } 275 276 string clipboard(string value) @trusted { 277 278 SetClipboardText(value.toStringz); 279 280 return value; 281 282 } 283 284 string clipboard() const @trusted { 285 286 return GetClipboardText().fromStringz.dup; 287 288 } 289 290 float deltaTime() const @trusted { 291 292 return GetFrameTime; 293 294 } 295 296 bool hasJustResized() const @trusted { 297 298 // TODO detect and react to DPI changes 299 return IsWindowResized; 300 301 } 302 303 Vector2 windowSize(Vector2 size) @trusted { 304 305 auto sizeRay = toRaylibCoords(size); 306 SetWindowSize(cast(int) sizeRay.x, cast(int) sizeRay.y); 307 return size; 308 309 } 310 311 Vector2 windowSize() const @trusted { 312 313 return fromRaylibCoords(GetScreenWidth, GetScreenHeight); 314 315 } 316 317 float scale() const { 318 319 return _scale; 320 321 } 322 323 float scale(float value) { 324 325 return _scale = value; 326 327 } 328 329 Vector2 dpi() const @trusted { 330 331 import fluid.io.canvas : getGlobalScale; 332 333 static Vector2 value; 334 335 if (value == value.init) { 336 337 const globalScale = getGlobalScale(); 338 339 value = GetWindowScaleDPI; 340 value.x *= 96 * globalScale.x; 341 value.y *= 96 * globalScale.y; 342 343 } 344 345 return value * _scale; 346 347 } 348 349 Vector2 toRaylibCoords(Vector2 position) const @trusted { 350 351 version (Fluid_DisableScaling) 352 return position; 353 else 354 return Vector2(position.x * hidpiScale.x, position.y * hidpiScale.y); 355 356 } 357 358 Rectangle toRaylibCoords(Rectangle rec) const @trusted { 359 360 version (Fluid_DisableScaling) 361 return rec; 362 else 363 return Rectangle( 364 rec.x * hidpiScale.x, 365 rec.y * hidpiScale.y, 366 rec.width * hidpiScale.x, 367 rec.height * hidpiScale.y, 368 ); 369 370 } 371 372 Vector2 fromRaylibCoords(Vector2 position) const @trusted { 373 374 version (Fluid_DisableScaling) 375 return position; 376 else 377 return Vector2(position.x / hidpiScale.x, position.y / hidpiScale.y); 378 379 } 380 381 Vector2 fromRaylibCoords(float x, float y) const @trusted { 382 383 version (Fluid_DisableScaling) 384 return Vector2(x, y); 385 else 386 return Vector2(x / hidpiScale.x, y / hidpiScale.y); 387 388 } 389 390 Rectangle fromRaylibCoords(Rectangle rec) const @trusted { 391 392 version (Fluid_DisableScaling) 393 return rec; 394 else 395 return Rectangle( 396 rec.x / hidpiScale.x, 397 rec.y / hidpiScale.y, 398 rec.width / hidpiScale.x, 399 rec.height / hidpiScale.y, 400 ); 401 402 } 403 404 Rectangle area(Rectangle rect) @trusted { 405 406 auto rectRay = toRaylibCoords(rect); 407 408 BeginScissorMode( 409 cast(int) rectRay.x, 410 cast(int) rectRay.y, 411 cast(int) rectRay.width, 412 cast(int) rectRay.height, 413 ); 414 415 return drawArea = rect; 416 417 } 418 419 Rectangle area() const { 420 421 if (drawArea is drawArea.init) 422 return Rectangle(0, 0, windowSize.tupleof); 423 else 424 return drawArea; 425 426 } 427 428 void restoreArea() @trusted { 429 430 EndScissorMode(); 431 drawArea = drawArea.init; 432 433 } 434 435 FluidMouseCursor mouseCursor(FluidMouseCursor cursor) @trusted { 436 437 // Hide the cursor if requested 438 if (cursor.system == cursor.system.none) { 439 HideCursor(); 440 } 441 442 // Show the cursor 443 else { 444 SetMouseCursor(cursor.system.toRaylib); 445 ShowCursor(); 446 } 447 return lastMouseCursor = cursor; 448 449 } 450 451 FluidMouseCursor mouseCursor() const { 452 453 return lastMouseCursor; 454 455 } 456 457 TextureReaper* reaper() return scope { 458 459 return &_reaper; 460 461 } 462 463 fluid.backend.Texture loadTexture(fluid.backend.Image image) @system { 464 465 return fromRaylib(LoadTextureFromImage(image.toRaylib), image.format); 466 467 } 468 469 fluid.backend.Texture loadTexture(string filename) @system { 470 471 import std.string; 472 473 return fromRaylib(LoadTexture(filename.toStringz), fluid.backend.Image.Format.rgba); 474 475 } 476 477 void updateTexture(fluid.backend.Texture texture, fluid.backend.Image image) @system 478 in (false) 479 do { 480 481 UpdateTexture(texture.toRaylib, image.data.ptr); 482 483 } 484 485 protected fluid.backend.Texture fromRaylib(raylib.Texture rayTexture, fluid.backend.Image.Format format) @system { 486 487 fluid.backend.Texture result; 488 result.id = rayTexture.id; 489 result.format = format; 490 result.tombstone = reaper.makeTombstone(this, result.id); 491 result.width = rayTexture.width; 492 result.height = rayTexture.height; 493 return result; 494 495 } 496 497 /// Destroy a texture 498 void unloadTexture(uint id) @system { 499 500 if (!__ctfe && IsWindowReady && id != 0) { 501 502 _mipmapCount.remove(id); 503 rlUnloadTexture(id); 504 505 } 506 507 } 508 509 Color tint(Color color) { 510 511 return _tint = color; 512 513 } 514 515 Color tint() const { 516 517 return _tint; 518 519 } 520 521 void drawLine(Vector2 start, Vector2 end, Color color) @trusted { 522 523 DrawLineV(toRaylibCoords(start), toRaylibCoords(end), multiply(color, tint)); 524 525 } 526 527 void drawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color) @trusted { 528 529 DrawTriangle(toRaylibCoords(a), toRaylibCoords(b), toRaylibCoords(c), multiply(color, tint)); 530 531 } 532 533 void drawCircle(Vector2 center, float radius, Color color) @trusted { 534 535 DrawCircleV(toRaylibCoords(center), radius * hidpiScale.y, multiply(color, tint)); 536 537 } 538 539 void drawCircleOutline(Vector2 center, float radius, Color color) @trusted { 540 541 DrawCircleLinesV(toRaylibCoords(center), radius * hidpiScale.y, multiply(color, tint)); 542 543 } 544 545 void drawRectangle(Rectangle rectangle, Color color) @trusted { 546 547 DrawRectangleRec(toRaylibCoords(rectangle), multiply(color, tint)); 548 549 } 550 551 void drawTexture(fluid.backend.Texture texture, Rectangle rectangle, Color tint) 552 @trusted 553 in (false) 554 do { 555 556 auto rayTexture = texture.toRaylib; 557 558 // Ensure the texture has mipmaps, if possible, to enable trilinear filtering 559 if (auto mipmapCount = texture.id in _mipmapCount) { 560 561 rayTexture.mipmaps = *mipmapCount; 562 563 } 564 565 else { 566 567 // Generate mipmaps 568 GenTextureMipmaps(&rayTexture); 569 _mipmapCount[texture.id] = rayTexture.mipmaps; 570 571 } 572 573 // Set filter accordingly 574 const filter = rayTexture.mipmaps == 1 575 ? TextureFilter.TEXTURE_FILTER_BILINEAR 576 : TextureFilter.TEXTURE_FILTER_TRILINEAR; 577 578 SetTextureFilter(rayTexture, filter); 579 drawTexture(texture, rectangle, tint, false); 580 581 } 582 583 void drawTextureAlign(fluid.backend.Texture texture, Rectangle rectangle, Color tint) 584 @trusted 585 in (false) 586 do { 587 588 auto rayTexture = texture.toRaylib; 589 590 SetTextureFilter(rayTexture, TextureFilter.TEXTURE_FILTER_POINT); 591 drawTexture(texture, rectangle, tint, true); 592 593 } 594 595 protected @trusted 596 void drawTexture(fluid.backend.Texture texture, Rectangle destination, Color tint, bool alignPixels) 597 do { 598 599 import std.math; 600 601 // Align texture to pixel boundaries 602 if (alignPixels) { 603 destination.x = floor(destination.x * hidpiScale.x) / hidpiScale.x; 604 destination.y = floor(destination.y * hidpiScale.y) / hidpiScale.y; 605 } 606 607 destination = toRaylibCoords(destination); 608 609 const source = Rectangle(0, 0, texture.width, texture.height); 610 Shader shader; 611 612 // Enable shaders relevant to given format 613 switch (texture.format) { 614 615 case fluid.backend.Image.Format.alpha: 616 shader = alphaImageShader; 617 break; 618 619 case fluid.backend.Image.Format.palettedAlpha: 620 shader = palettedAlphaImageShader(texture.palette); 621 break; 622 623 default: break; 624 625 } 626 627 // Start shaders, if applicable 628 if (IsShaderReady(shader)) 629 BeginShaderMode(shader); 630 631 DrawTexturePro(texture.toRaylib, source, destination, Vector2(0, 0), 0, multiply(tint, this.tint)); 632 633 // End shaders 634 if (IsShaderReady(shader)) 635 EndShaderMode(); 636 637 } 638 639 /// Create a palette texture. 640 private fluid.backend.Texture paletteTexture(scope Color[] colors) @trusted 641 in (colors.length <= 256, "There can only be at most 256 colors in a palette.") 642 do { 643 644 // Fill empty slots in the palette with white 645 Color[256] allColors = color("#fff"); 646 allColors[0 .. colors.length] = colors; 647 648 // Prepare an image for the texture 649 scope image = fluid.backend.Image(allColors[], 256, 1); 650 651 // Create the texture if it doesn't exist 652 if (_paletteTexture is _paletteTexture.init) 653 _paletteTexture = loadTexture(image); 654 655 // Or, update existing palette image 656 else 657 updateTexture(_paletteTexture, image); 658 659 return _paletteTexture; 660 661 } 662 663 } 664 665 /// Get the Raylib enum for a mouse cursor. 666 raylib.MouseCursor toRaylib(FluidMouseCursor.SystemCursors cursor) { 667 668 with (raylib.MouseCursor) 669 with (FluidMouseCursor.SystemCursors) 670 switch (cursor) { 671 672 default: 673 case none: 674 case systemDefault: 675 return MOUSE_CURSOR_DEFAULT; 676 677 case pointer: 678 return MOUSE_CURSOR_POINTING_HAND; 679 680 case crosshair: 681 return MOUSE_CURSOR_CROSSHAIR; 682 683 case text: 684 return MOUSE_CURSOR_IBEAM; 685 686 case allScroll: 687 return MOUSE_CURSOR_RESIZE_ALL; 688 689 case resizeEW: 690 return MOUSE_CURSOR_RESIZE_EW; 691 692 case resizeNS: 693 return MOUSE_CURSOR_RESIZE_NS; 694 695 case resizeNESW: 696 return MOUSE_CURSOR_RESIZE_NESW; 697 698 case resizeNWSE: 699 return MOUSE_CURSOR_RESIZE_NWSE; 700 701 case notAllowed: 702 return MOUSE_CURSOR_NOT_ALLOWED; 703 704 } 705 706 } 707 708 /// Get the Raylib enum for a keyboard key. 709 raylib.KeyboardKey toRaylib(KeyboardKey key) { 710 711 return cast(raylib.KeyboardKey) key; 712 713 } 714 715 /// Get the Raylib enum for a mouse button. 716 raylib.MouseButton toRaylib(MouseButton button) { 717 718 with (raylib.MouseButton) 719 with (MouseButton) 720 final switch (button) { 721 case none: assert(false); 722 case left: return MOUSE_BUTTON_LEFT; 723 case right: return MOUSE_BUTTON_RIGHT; 724 case middle: return MOUSE_BUTTON_MIDDLE; 725 case extra1: return MOUSE_BUTTON_SIDE; 726 case extra2: return MOUSE_BUTTON_EXTRA; 727 case forward: return MOUSE_BUTTON_FORWARD; 728 case back: return MOUSE_BUTTON_BACK; 729 } 730 731 } 732 733 /// Get the Raylib enum for a keyboard key. 734 raylib.GamepadButton toRaylib(GamepadButton button) { 735 736 return cast(raylib.GamepadButton) button; 737 738 } 739 740 /// Convert image to a Raylib image. Do not call `UnloadImage` on the result. 741 raylib.Image toRaylib(fluid.backend.Image image) nothrow @trusted { 742 743 raylib.Image result; 744 result.data = image.data.ptr; 745 result.width = image.width; 746 result.height = image.height; 747 result.format = image.format.toRaylib; 748 result.mipmaps = 1; 749 return result; 750 751 } 752 753 /// Convert Fluid image format to Raylib's closest alternative. 754 raylib.PixelFormat toRaylib(fluid.backend.Image.Format imageFormat) nothrow { 755 756 final switch (imageFormat) { 757 758 case imageFormat.rgba: 759 return PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; 760 761 case imageFormat.palettedAlpha: 762 return PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA; 763 764 case imageFormat.alpha: 765 return PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE; 766 767 } 768 769 } 770 771 /// Convert a fluid texture to a Raylib texture. 772 raylib.Texture toRaylib(fluid.backend.Texture texture) @trusted { 773 774 raylib.Texture result; 775 result.id = texture.id; 776 result.width = texture.width; 777 result.height = texture.height; 778 result.format = texture.format.toRaylib; 779 result.mipmaps = 1; 780 781 return result; 782 783 } 784 785 /// Convert a Raylib texture to a Fluid texture 786 fluid.backend.Texture toFluid(raylib.Texture rayTexture) @system { 787 fluid.backend.Texture result; 788 result.id = rayTexture.id; 789 result.format = fluid.backend.Image.Format.rgba; 790 result.width = rayTexture.width; 791 result.height = rayTexture.height; 792 793 return result; 794 }