1 /// This module handles input/output facilities Fluid requires to operate. It connects backends like Raylib, exposing 2 /// them under a common interface so they can be changed at will. 3 /// 4 /// Fluid comes with a built-in interface for Raylib. 5 module fluid.backend; 6 7 import std.meta; 8 import std.range; 9 import std.traits; 10 import std.datetime; 11 import std.algorithm; 12 13 import fluid.io.mouse; 14 import fluid.io.keyboard; 15 16 public import fluid.types; 17 public import fluid.backend.raylib5; 18 public import fluid.backend.headless; 19 20 alias KeyboardKey = KeyboardIO.Key; 21 alias MouseButton = MouseIO.Button; 22 23 version (Have_raylib_d) public static import raylib; 24 25 26 @safe: 27 28 29 alias VoidDelegate = void delegate() @safe; 30 31 FluidBackend defaultFluidBackend(); 32 33 /// `FluidBackend` is an interface making it possible to bind Fluid to a library other than Raylib. 34 /// 35 /// The default unit in graphical space is a **pixel** (`px`), here defined as **1/96 of an inch**. This is unless 36 /// stated otherwise, as in `Texture`. 37 /// 38 /// Warning: Backend API is unstable and functions may be added or removed with no prior warning. 39 interface FluidBackend { 40 41 /// Get system's double click time. 42 final Duration doubleClickTime() const { 43 44 // TODO This should be overridable 45 46 return 500.msecs; 47 48 } 49 50 bool opEquals(FluidBackend) const; 51 52 /// Check if the given mouse button has just been pressed/released or, if it's held down or not (up). 53 bool isPressed(MouseButton) const; 54 bool isReleased(MouseButton) const; 55 bool isDown(MouseButton) const; 56 bool isUp(MouseButton) const; 57 58 /// Check if the given keyboard key has just been pressed/released or, if it's held down or not (up). 59 bool isPressed(KeyboardKey) const; 60 bool isReleased(KeyboardKey) const; 61 bool isDown(KeyboardKey) const; 62 bool isUp(KeyboardKey) const; 63 64 /// If true, the given keyboard key has been virtually pressed again, through a long-press. 65 bool isRepeated(KeyboardKey) const; 66 67 /// Get next queued character from user's input. The queue should be cleared every frame. Return null if no 68 /// character was pressed. 69 dchar inputCharacter(); 70 71 /// Check if the given gamepad button has been pressed/released or, if it's held down or not (up) on any of the 72 /// connected gamepads. 73 /// 74 /// Returns: 0 if the event isn't taking place on any controller, or number of the controller. 75 int isPressed(GamepadButton button) const; 76 int isReleased(GamepadButton button) const; 77 int isDown(GamepadButton button) const; 78 int isUp(GamepadButton button) const; 79 80 /// If true, the given gamepad button has been virtually pressed again, through a long-press. 81 /// 82 /// Returns: 0 if no controller had a button repeat this frame, or number of the controller. 83 int isRepeated(GamepadButton button) const; 84 85 /// Get/set mouse position 86 Vector2 mousePosition(Vector2); 87 Vector2 mousePosition() const; 88 89 /// Get scroll value on both axes. 90 Vector2 scroll() const; 91 92 /// Get or set system clipboard value. 93 string clipboard(string); 94 string clipboard() const; 95 96 /// Get time elapsed since last frame in seconds. 97 float deltaTime() const; 98 99 /// True if the user has just resized the window. 100 bool hasJustResized() const; 101 102 /// Get or set the size of the window. 103 Vector2 windowSize(Vector2); 104 Vector2 windowSize() const; /// ditto 105 106 /// Set scale to apply to whatever is drawn next. 107 /// 108 /// Suggested implementation is to increase return value of `dpi`. 109 float scale() const; 110 111 /// ditto 112 float scale(float); 113 114 /// Get horizontal and vertical DPI of the window. 115 Vector2 dpi() const; 116 117 /// Get the DPI value for the window as a scale relative to 96 DPI. 118 final Vector2 hidpiScale() const { 119 120 const dpi = this.dpi; 121 return Vector2(dpi.x / 96f, dpi.y / 96f); 122 123 } 124 125 /// Set area within the window items will be drawn to; any pixel drawn outside will be discarded. 126 Rectangle area(Rectangle rect); 127 Rectangle area() const; 128 129 /// Restore the capability to draw anywhere in the window. 130 void restoreArea(); 131 132 /// Get or set mouse cursor icon. 133 FluidMouseCursor mouseCursor(FluidMouseCursor); 134 FluidMouseCursor mouseCursor() const; 135 136 /// Texture reaper used by this backend. May be null. 137 /// 138 /// Highly recommended for OpenGL-based backends. 139 TextureReaper* reaper() return scope; 140 141 /// Load a texture from memory or file. 142 Texture loadTexture(Image image) @system; 143 Texture loadTexture(string filename) @system; 144 145 /// Update a texture from an image. The texture must be valid and must be of the same size and format as the image. 146 void updateTexture(Texture texture, Image image) @system 147 in (texture.format == image.format) 148 in (texture.width == image.width) 149 in (texture.height == image.height); 150 151 /// Destroy a texture created by this backend. Always use `texture.destroy()` to ensure thread safety and invoking 152 /// the correct backend. 153 protected void unloadTexture(uint id) @system; 154 155 /// ditto 156 final void unloadTexture(Texture texture) @system { 157 158 unloadTexture(texture.id); 159 160 } 161 162 /// Set tint for all newly drawn shapes. The input color for every shape should be multiplied by this color. 163 Color tint(Color); 164 165 /// Get current tint color. 166 Color tint() const; 167 168 /// Draw a line. 169 void drawLine(Vector2 start, Vector2 end, Color color); 170 171 /// Draw a triangle, consisting of 3 vertices with counter-clockwise winding. 172 void drawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color); 173 174 /// Draw a circle. 175 void drawCircle(Vector2 center, float radius, Color color); 176 177 /// Draw a circle, but outline only. 178 void drawCircleOutline(Vector2 center, float radius, Color color); 179 180 /// Draw a rectangle. 181 void drawRectangle(Rectangle rectangle, Color color); 182 183 /// Draw a texture. 184 void drawTexture(Texture texture, Rectangle rectangle, Color tint) 185 in (texture.backend is this, "Given texture comes from a different backend"); 186 187 /// Draw a texture, but ensure it aligns with pixel boundaries, recommended for text. 188 void drawTextureAlign(Texture texture, Rectangle rectangle, Color tint) 189 in (texture.backend is this, "Given texture comes from a different backend"); 190 191 } 192 193 /// To simplify setup in some scenarios, Fluid provides a uniform `run` function to immediately display UI and start 194 /// the event loop. This function is provided by the backend using this optional interface. 195 /// 196 /// For `run` to use this backend, it has to be configured as the default backend or be passed explicitly to the `run` 197 /// function. 198 interface FluidEntrypointBackend : FluidBackend { 199 200 import fluid.node : Node; 201 202 /// Start a Fluid GUI app using this backend. 203 /// 204 /// This will draw the user interface and respond to input events in a loop, until the root node is marked for 205 /// removal (`remove()`). 206 /// 207 /// See_Also: `fluid.node.run` 208 /// Params: 209 /// root = Node to function as the root of the user interface. 210 void run(Node root); 211 212 } 213 214 /// Struct that maintains a registry of all allocated textures. It's used to finalize textures once they have been 215 /// marked for destruction. This makes it possible to mark them from any thread, while the reaper runs only on the main 216 /// thread, ensuring thread safety in OpenGL backends. 217 struct TextureReaper { 218 219 /// Number of cycles between runs of the reaper. 220 int period = 60 * 5; 221 222 int cycleAccumulator; 223 224 @system shared(TextureTombstone)*[uint] textures; 225 226 @disable this(ref TextureReaper); 227 @disable this(this); 228 229 ~this() @trusted { 230 231 destroyAll(); 232 233 } 234 235 /// Create a tombstone. 236 shared(TextureTombstone)* makeTombstone(FluidBackend backend, uint textureID) @trusted { 237 238 return textures[textureID] = TextureTombstone.make(backend); 239 240 } 241 242 /// Count number of cycles since last collection and collect if configured period has passed. 243 void check() { 244 245 // Count cycles 246 if (++cycleAccumulator >= period) { 247 248 // Run collection 249 collect(); 250 251 } 252 253 } 254 255 /// Collect all destroyed textures immediately. 256 void collect() @trusted { 257 258 // Reset the cycle accumulator 259 cycleAccumulator = 0; 260 261 // Find all destroyed textures 262 foreach (id, tombstone; textures) { 263 264 if (!tombstone.isDestroyed) continue; 265 266 auto backend = cast() tombstone.backend; 267 268 // Unload the texture 269 backend.unloadTexture(id); 270 271 // Disown the tombstone and remove it from the registry 272 tombstone.markDisowned(); 273 textures.remove(id); 274 275 } 276 277 } 278 279 /// Destroy all textures. 280 void destroyAll() @system { 281 282 cycleAccumulator = 0; 283 scope (exit) textures.clear(); 284 285 // Find all textures 286 foreach (id, tombstone; textures) { 287 288 auto backend = cast() tombstone.backend; 289 290 // Unload the texture, even if it wasn't marked for deletion 291 backend.unloadTexture(id); 292 // TODO Should this be done? The destructor may be called from the GC. Maybe check if it was? 293 // Test this! 294 295 // Disown all textures 296 tombstone.markDisowned(); 297 298 } 299 300 } 301 302 } 303 304 /// Tombstones are used to ensure textures are freed on the same thread they have been created on. 305 /// 306 /// Tombstones are kept alive until the texture is explicitly destroyed and then finalized (disowned) from the main 307 /// thread by a periodically-running `TextureReaper`. This is necessary to make Fluid safe in multithreaded 308 /// environments. 309 shared struct TextureTombstone { 310 311 import core.memory; 312 import core.atomic; 313 import core.stdc.stdlib; 314 315 /// Backend that created this texture. 316 private FluidBackend _backend; 317 318 private int _references = 1; 319 private bool _disowned; 320 321 @disable this(this); 322 323 static TextureTombstone* make(FluidBackend backend) @system { 324 325 import core.exception; 326 327 // Allocate the tombstone 328 auto data = malloc(TextureTombstone.sizeof); 329 if (data is null) throw new OutOfMemoryError("Failed to allocate a tombstone"); 330 331 // Initialize the tombstone 332 shared tombstone = cast(shared TextureTombstone*) data; 333 *tombstone = TextureTombstone.init; 334 tombstone._backend = cast(shared) backend; 335 336 assert(tombstone.references == 1); 337 338 // Make sure the backend isn't freed while the tombstone is alive 339 GC.addRoot(cast(void*) backend); 340 341 return tombstone; 342 343 } 344 345 /// Check if a request for destruction has been made for the texture. 346 bool isDestroyed() @system { 347 return _references.atomicLoad == 0; 348 } 349 350 /// Check if the texture has been disowned by the backend. A disowned tombstone refers to a texture that has been 351 /// freed. 352 private bool isDisowned() @system { 353 return _disowned.atomicLoad; 354 } 355 356 /// Get number of references to this tombstone. 357 private int references() @system { 358 return _references.atomicLoad; 359 } 360 361 /// Get the backend owning this texture. 362 inout(shared FluidBackend) backend() inout { 363 return _backend; 364 } 365 366 /// Mark the texture as destroyed. 367 void markDestroyed() @system { 368 369 assert(!isDisowned || !isDestroyed, "Texture: Double destroy()"); 370 371 _references.atomicFetchSub(1); 372 tryDestroy(); 373 374 } 375 376 /// Mark the texture as disowned. 377 private void markDisowned() @system { 378 379 assert(!isDisowned || !isDestroyed); 380 381 _disowned.atomicStore(true); 382 tryDestroy(); 383 384 } 385 386 /// Mark the texture as copied. 387 private void markCopied() @system { 388 389 _references.atomicFetchAdd(1); 390 391 } 392 393 /// As soon as the texture is both marked for destruction and disowned, the tombstone controlling its life is 394 /// destroyed. 395 /// 396 /// There are two relevant scenarios: 397 /// 398 /// * The texture is marked for destruction via a tombstone, then finalized from the main thread and disowned. 399 /// * The texture is finalized after the backend (for example, if they are both destroyed during the same GC 400 /// collection). The backend disowns and frees the texture. The tombstone, however, remains alive to 401 /// witness marking the texture as deleted. 402 /// 403 /// In both scenarios, this behavior ensures the tombstone will be freed. 404 private void tryDestroy() @system { 405 406 // Destroyed and disowned 407 if (isDestroyed && isDisowned) { 408 409 GC.removeRoot(cast(void*) _backend); 410 free(cast(void*) &this); 411 412 } 413 414 } 415 416 } 417 418 @system 419 unittest { 420 421 // This unittest checks if textures will be correctly destroyed, even if the destruction call comes from another 422 // thread. 423 424 import std.concurrency; 425 import fluid.space; 426 import fluid.image_view; 427 428 auto io = new HeadlessBackend; 429 auto image = imageView("logo.png"); 430 auto root = vspace(image); 431 432 // Draw the frame once to let everything load 433 root.io = io; 434 root.draw(); 435 436 // Tune the reaper to run every frame 437 io.reaper.period = 1; 438 439 // Get the texture 440 auto texture = image.release(); 441 auto textureID = texture.id; 442 auto tombstone = texture.tombstone; 443 444 // Texture should be allocated and assigned a tombstone 445 assert(texture.backend is io); 446 assert(!texture.tombstone.isDestroyed); 447 assert(io.isTextureValid(texture)); 448 449 // Destroy the texture on another thread 450 spawn((shared Texture sharedTexture) { 451 452 auto texture = cast() sharedTexture; 453 texture.destroy(); 454 ownerTid.send(true); 455 456 }, cast(shared) texture); 457 458 // Wait for confirmation 459 receiveOnly!bool; 460 461 // The texture should be marked for deletion but remain alive 462 assert(texture.tombstone.isDestroyed); 463 assert(io.isTextureValid(texture)); 464 465 // Draw a frame, during which the reaper should destroy the texture 466 io.nextFrame; 467 root.children = []; 468 root.updateSize(); 469 root.draw(); 470 471 assert(!io.isTextureValid(texture)); 472 // There is no way to test if the tombstone has been freed 473 474 } 475 476 @system 477 unittest { 478 479 // This unittest checks if tombstones work correctly even if the backend is destroyed before the texture. 480 481 import std.concurrency; 482 import core.atomic; 483 import fluid.image_view; 484 485 auto io = new HeadlessBackend; 486 auto root = imageView("logo.png"); 487 488 // Load the texture and draw 489 root.io = io; 490 root.draw(); 491 492 // Destroy the backend 493 destroy(io); 494 495 auto texture = root.release(); 496 497 // The texture should have been automatically freed, but not marked for destruction 498 assert(!texture.tombstone.isDestroyed); 499 assert(texture.tombstone._disowned.atomicLoad); 500 501 // Now, destroy the image 502 // If this operation succeeds, we're good 503 destroy(root); 504 // There is no way to test if the tombstone and texture have truly been freed 505 506 } 507 508 struct FluidMouseCursor { 509 510 enum SystemCursors { 511 512 systemDefault, // Default system cursor. 513 none, // No pointer. 514 pointer, // Pointer indicating a link or button, typically a pointing hand. 👆 515 crosshair, // Cross cursor, often indicating selection inside images. 516 text, // Vertical beam indicating selectable text. 517 allScroll, // Omnidirectional scroll, content can be scrolled in any direction (panned). 518 resizeEW, // Cursor indicating the content underneath can be resized horizontally. 519 resizeNS, // Cursor indicating the content underneath can be resized vertically. 520 resizeNESW, // Diagonal resize cursor, top-right + bottom-left. 521 resizeNWSE, // Diagonal resize cursor, top-left + bottom-right. 522 notAllowed, // Indicates a forbidden action. 523 524 } 525 526 enum { 527 528 systemDefault = FluidMouseCursor(SystemCursors.systemDefault), 529 none = FluidMouseCursor(SystemCursors.none), 530 pointer = FluidMouseCursor(SystemCursors.pointer), 531 crosshair = FluidMouseCursor(SystemCursors.crosshair), 532 text = FluidMouseCursor(SystemCursors.text), 533 allScroll = FluidMouseCursor(SystemCursors.allScroll), 534 resizeEW = FluidMouseCursor(SystemCursors.resizeEW), 535 resizeNS = FluidMouseCursor(SystemCursors.resizeNS), 536 resizeNESW = FluidMouseCursor(SystemCursors.resizeNESW), 537 resizeNWSE = FluidMouseCursor(SystemCursors.resizeNWSE), 538 notAllowed = FluidMouseCursor(SystemCursors.notAllowed), 539 540 } 541 542 /// Use a system-provided cursor. 543 SystemCursors system; 544 // TODO user-provided cursor image 545 546 } 547 548 enum GamepadButton { 549 550 none, // No such button 551 dpadUp, // Dpad up button. 552 dpadRight, // Dpad right button 553 dpadDown, // Dpad down button 554 dpadLeft, // Dpad left button 555 triangle, // Triangle (PS) or Y (Xbox) 556 circle, // Circle (PS) or B (Xbox) 557 cross, // Cross (PS) or A (Xbox) 558 square, // Square (PS) or X (Xbox) 559 leftButton, // Left button behind the controlller (LB). 560 leftTrigger, // Left trigger (LT). 561 rightButton, // Right button behind the controller (RB). 562 rightTrigger, // Right trigger (RT) 563 select, // "Select" button. 564 vendor, // Button with the vendor logo. 565 start, // "Start" button. 566 leftStick, // Left joystick press. 567 rightStick, // Right joystick press. 568 569 y = triangle, 570 x = square, 571 a = cross, 572 b = circle, 573 574 } 575 576 enum GamepadAxis { 577 578 leftX, // Left joystick, X axis. 579 leftY, // Left joystick, Y axis. 580 rightX, // Right joystick, X axis. 581 rightY, // Right joystick, Y axis. 582 leftTrigger, // Analog input for the left trigger. 583 rightTrigger, // Analog input for the right trigger. 584 585 } 586 587 /// Image or texture can be rendered by Fluid, for example, a texture stored in VRAM. 588 /// 589 /// Textures make use of manual memory management. See `TextureGC` for a GC-managed texture. 590 struct Texture { 591 592 /// Tombstone for this texture 593 shared(TextureTombstone)* tombstone; 594 595 /// Format of the texture. 596 Image.Format format; 597 598 /// GPU/backend ID of the texture. 599 uint id; 600 601 /// Width and height of the texture, **in dots**. The meaning of a dot is defined by `dpiX` and `dpiY` 602 int width, height; 603 604 /// Dots per inch for the X and Y axis. Defaults to 96, thus making a dot in the texture equivalent to a pixel. 605 int dpiX = 96, dpiY = 96; 606 607 /// If relevant, the texture is to use this palette. 608 Color[] palette; 609 610 bool opEquals(const Texture other) const { 611 return id == other.id 612 && width == other.width 613 && height == other.height 614 && dpiX == other.dpiX 615 && dpiY == other.dpiY; 616 617 } 618 619 version (Have_raylib_d) 620 void opAssign(raylib.Texture rayTexture) @system { 621 this = rayTexture.toFluid(); 622 } 623 624 /// Get the backend for this texture. Doesn't work after freeing the tombstone. 625 inout(FluidBackend) backend() inout @trusted { 626 return cast(inout FluidBackend) tombstone.backend; 627 } 628 629 /// DPI value of the texture. 630 Vector2 dpi() const { 631 return Vector2(dpiX, dpiY); 632 } 633 634 /// Get texture size as a vector. 635 Vector2 canvasSize() const { 636 return Vector2(width, height); 637 } 638 639 /// Get the size the texture will occupy within the viewport. 640 Vector2 viewportSize() const { 641 return Vector2( 642 width * 96 / dpiX, 643 height * 96 / dpiY 644 ); 645 } 646 647 /// Update the texture to match the given image. 648 void update(Image image) @system { 649 650 backend.updateTexture(this, image); 651 652 } 653 654 /// Draw this texture. 655 void draw(Vector2 position, Color tint = Color(0xff, 0xff, 0xff, 0xff)) { 656 657 auto rectangle = Rectangle(position.tupleof, viewportSize.tupleof); 658 659 backend.drawTexture(this, rectangle, tint); 660 661 } 662 663 void draw(Rectangle rectangle, Color tint = Color(0xff, 0xff, 0xff, 0xff)) { 664 665 backend.drawTexture(this, rectangle, tint); 666 667 } 668 669 /// Destroy this texture. This function is thread-safe. 670 void destroy() @system { 671 672 if (tombstone is null) return; 673 674 tombstone.markDestroyed(); 675 tombstone = null; 676 id = 0; 677 678 } 679 680 } 681 682 /// Wrapper over `Texture` that automates destruction via GC or RAII. 683 struct TextureGC { 684 685 /// Underlying texture. Lifetime is bound to this struct. 686 Texture texture; 687 688 alias texture this; 689 690 /// Load a texture from filename. 691 this(FluidBackend backend, string filename) @trusted { 692 693 this.texture = backend.loadTexture(filename); 694 695 } 696 697 /// Load a texture from image data. 698 this(FluidBackend backend, Image data) @trusted { 699 700 this.texture = backend.loadTexture(data); 701 702 } 703 704 /// Move constructor for TextureGC; increment the reference counter for the texture. 705 /// 706 /// While I originally did not intend to implement reference counting, it is necessary to make TextureGC work in 707 /// dynamic arrays. Changing the size of the array will copy the contents without performing a proper move of the 708 /// old items. The postblit is the only kind of move constructor that will be called in this case, and a copy 709 /// constructor does not do its job. 710 this(this) @trusted { 711 712 if (tombstone) 713 tombstone.markCopied(); 714 715 } 716 717 @system 718 unittest { 719 720 import std.string; 721 722 // This tests using TextureGC inside of a dynamic array, especially after resizing. See documentation for 723 // the postblit above. 724 725 // Test two variants: 726 // * One, where we rely on the language to finalize the copied value 727 // * And one, where we manually destroy the value 728 foreach (explicitDestruction; [false, true]) { 729 730 void makeCopy(TextureGC[] arr) { 731 732 // Create the copy 733 auto copy = arr; 734 735 assert(sameHead(arr, copy)); 736 737 // Expand the array, creating another 738 copy.length = 1024; 739 740 assert(!sameHead(arr, copy)); 741 742 // References to tombstones exist in both arrays now 743 assert(!copy[0].tombstone.isDestroyed); 744 assert(!arr[0].tombstone.isDestroyed); 745 746 // The copy should be marked as moved 747 assert(copy[0].tombstone.references == 2); 748 assert(arr[0].tombstone.references == 2); 749 750 // Destroy the tombstone 751 if (explicitDestruction) { 752 753 auto tombstone = copy[0].tombstone; 754 755 copy[0].destroy(); 756 assert(tombstone.references == 1); 757 assert(!tombstone.isDestroyed); 758 759 } 760 761 // Forget about the copy 762 copy = null; 763 764 } 765 766 static void trashStack() { 767 768 import core.memory; 769 770 // Destroy the stack to get rid of any references to `copy` 771 ubyte[2048] garbage; 772 773 // Collect it, make sure the tombstone gets eaten 774 GC.collect(); 775 776 } 777 778 auto io = new HeadlessBackend; 779 auto image = generateColorImage(10, 10, color("#fff")); 780 auto arr = [ 781 TextureGC(io, image), 782 TextureGC.init, 783 ]; 784 785 makeCopy(arr); 786 trashStack(); 787 788 assert(!arr[0].tombstone.isDestroyed, "Tombstone of a live texture was destroyed after copying an array" 789 ~ format!" (explicitDestruction %s)"(explicitDestruction)); 790 791 io.reaper.collect(); 792 793 assert(io.isTextureValid(arr[0])); 794 assert(!arr[0].tombstone.isDestroyed); 795 assert(!arr[0].tombstone.isDisowned); 796 assert(arr[0].tombstone.references == 1); 797 798 } 799 800 } 801 802 @system 803 unittest { 804 805 auto io = new HeadlessBackend; 806 auto image = generateColorImage(10, 10, color("#fff")); 807 auto arr = [ 808 TextureGC(io, image), 809 TextureGC.init, 810 ]; 811 auto copy = arr.dup; 812 813 assert(arr[0].tombstone.references == 2); 814 815 io.reaper.collect(); 816 817 assert(io.isTextureValid(arr[0])); 818 819 } 820 821 ~this() @trusted { 822 823 texture.destroy(); 824 825 } 826 827 /// Release the texture, moving it to manual management. 828 Texture release() @system { 829 830 auto result = texture; 831 texture = texture.init; 832 return result; 833 834 } 835 836 } 837 838 version (unittest) { 839 840 debug (Fluid_BuildMessages) { 841 pragma(msg, "Fluid: Using headless as the default backend (unittest)"); 842 } 843 844 FluidBackend defaultFluidBackend() { 845 846 return new HeadlessBackend; 847 848 } 849 850 } 851 852 else version (Have_raylib_d) { 853 854 debug (Fluid_BuildMessages) { 855 pragma(msg, "Fluid: Using Raylib 5 as the default backend"); 856 } 857 858 FluidBackend defaultFluidBackend() { 859 860 static Raylib5Backend backend; 861 862 if (backend) 863 return backend; 864 else 865 return new Raylib5Backend; 866 867 } 868 869 } 870 871 else { 872 873 debug (Fluid_BuildMessages) { 874 pragma(msg, "Fluid: No built-in backend in use"); 875 } 876 877 FluidBackend defaultFluidBackend() { 878 879 return null; 880 881 } 882 883 }