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 public import fluid.backend.raylib5; 14 public import fluid.backend.headless; 15 16 version (Have_raylib_d) public static import raylib; 17 18 19 @safe: 20 21 22 alias VoidDelegate = void delegate() @safe; 23 24 FluidBackend defaultFluidBackend(); 25 26 /// `FluidBackend` is an interface making it possible to bind Fluid to a library other than Raylib. 27 /// 28 /// The default unit in graphical space is a **pixel** (`px`), here defined as **1/96 of an inch**. This is unless 29 /// stated otherwise, as in `Texture`. 30 /// 31 /// Warning: Backend API is unstable and functions may be added or removed with no prior warning. 32 interface FluidBackend { 33 34 /// Get system's double click time. 35 final Duration doubleClickTime() const { 36 37 // TODO This should be overridable 38 39 return 500.msecs; 40 41 } 42 43 /// Check if the given mouse button has just been pressed/released or, if it's held down or not (up). 44 bool isPressed(MouseButton) const; 45 bool isReleased(MouseButton) const; 46 bool isDown(MouseButton) const; 47 bool isUp(MouseButton) const; 48 49 /// Check if the given keyboard key has just been pressed/released or, if it's held down or not (up). 50 bool isPressed(KeyboardKey) const; 51 bool isReleased(KeyboardKey) const; 52 bool isDown(KeyboardKey) const; 53 bool isUp(KeyboardKey) const; 54 55 /// If true, the given keyboard key has been virtually pressed again, through a long-press. 56 bool isRepeated(KeyboardKey) const; 57 58 /// Get next queued character from user's input. The queue should be cleared every frame. Return null if no 59 /// character was pressed. 60 dchar inputCharacter(); 61 62 /// Check if the given gamepad button has been pressed/released or, if it's held down or not (up) on any of the 63 /// connected gamepads. 64 /// 65 /// Returns: 0 if the event isn't taking place on any controller, or number of the controller. 66 int isPressed(GamepadButton button) const; 67 int isReleased(GamepadButton button) const; 68 int isDown(GamepadButton button) const; 69 int isUp(GamepadButton button) const; 70 71 /// If true, the given gamepad button has been virtually pressed again, through a long-press. 72 /// 73 /// Returns: 0 if no controller had a button repeat this frame, or number of the controller. 74 int isRepeated(GamepadButton button) const; 75 76 /// Get/set mouse position 77 Vector2 mousePosition(Vector2); 78 Vector2 mousePosition() const; 79 80 /// Get scroll value on both axes. 81 Vector2 scroll() const; 82 83 /// Get or set system clipboard value. 84 string clipboard(string); 85 string clipboard() const; 86 87 /// Get time elapsed since last frame in seconds. 88 float deltaTime() const; 89 90 /// True if the user has just resized the window. 91 bool hasJustResized() const; 92 93 /// Get or set the size of the window. 94 Vector2 windowSize(Vector2); 95 Vector2 windowSize() const; /// ditto 96 97 /// Set scale to apply to whatever is drawn next. 98 /// 99 /// Suggested implementation is to increase return value of `dpi`. 100 float scale() const; 101 102 /// ditto 103 float scale(float); 104 105 /// Get horizontal and vertical DPI of the window. 106 Vector2 dpi() const; 107 108 /// Get the DPI value for the window as a scale relative to 96 DPI. 109 final Vector2 hidpiScale() const { 110 111 const dpi = this.dpi; 112 return Vector2(dpi.x / 96f, dpi.y / 96f); 113 114 } 115 116 /// Set area within the window items will be drawn to; any pixel drawn outside will be discarded. 117 Rectangle area(Rectangle rect); 118 Rectangle area() const; 119 120 /// Restore the capability to draw anywhere in the window. 121 void restoreArea(); 122 123 /// Get or set mouse cursor icon. 124 FluidMouseCursor mouseCursor(FluidMouseCursor); 125 FluidMouseCursor mouseCursor() const; 126 127 /// Texture reaper used by this backend. May be null. 128 /// 129 /// Highly recommended for OpenGL-based backends. 130 TextureReaper* reaper() return scope; 131 132 /// Load a texture from memory or file. 133 Texture loadTexture(Image image) @system; 134 Texture loadTexture(string filename) @system; 135 136 /// Update a texture from an image. The texture must be valid and must be of the same size and format as the image. 137 void updateTexture(Texture texture, Image image) @system 138 in (texture.format == image.format) 139 in (texture.width == image.width) 140 in (texture.height == image.height); 141 142 /// Destroy a texture created by this backend. Always use `texture.destroy()` to ensure thread safety and invoking 143 /// the correct backend. 144 protected void unloadTexture(uint id) @system; 145 146 /// ditto 147 final void unloadTexture(Texture texture) @system { 148 149 unloadTexture(texture.id); 150 151 } 152 153 /// Set tint for all newly drawn shapes. The input color for every shape should be multiplied by this color. 154 Color tint(Color); 155 156 /// Get current tint color. 157 Color tint() const; 158 159 /// Draw a line. 160 void drawLine(Vector2 start, Vector2 end, Color color); 161 162 /// Draw a triangle, consisting of 3 vertices with counter-clockwise winding. 163 void drawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color); 164 165 /// Draw a circle. 166 void drawCircle(Vector2 center, float radius, Color color); 167 168 /// Draw a circle, but outline only. 169 void drawCircleOutline(Vector2 center, float radius, Color color); 170 171 /// Draw a rectangle. 172 void drawRectangle(Rectangle rectangle, Color color); 173 174 /// Draw a texture. 175 void drawTexture(Texture texture, Rectangle rectangle, Color tint) 176 in (texture.backend is this, "Given texture comes from a different backend"); 177 178 /// Draw a texture, but ensure it aligns with pixel boundaries, recommended for text. 179 void drawTextureAlign(Texture texture, Rectangle rectangle, Color tint) 180 in (texture.backend is this, "Given texture comes from a different backend"); 181 182 } 183 184 /// Struct that maintains a registry of all allocated textures. It's used to finalize textures once they have been 185 /// marked for destruction. This makes it possible to mark them from any thread, while the reaper runs only on the main 186 /// thread, ensuring thread safety in OpenGL backends. 187 struct TextureReaper { 188 189 /// Number of cycles between runs of the reaper. 190 int period = 60 * 5; 191 192 int cycleAccumulator; 193 194 @system shared(TextureTombstone)*[uint] textures; 195 196 @disable this(ref TextureReaper); 197 @disable this(this); 198 199 ~this() @trusted { 200 201 destroyAll(); 202 203 } 204 205 /// Create a tombstone. 206 shared(TextureTombstone)* makeTombstone(FluidBackend backend, uint textureID) @trusted { 207 208 return textures[textureID] = TextureTombstone.make(backend); 209 210 } 211 212 /// Count number of cycles since last collection and collect if configured period has passed. 213 void check() { 214 215 // Count cycles 216 if (++cycleAccumulator >= period) { 217 218 // Run collection 219 collect(); 220 221 } 222 223 } 224 225 /// Collect all destroyed textures immediately. 226 void collect() @trusted { 227 228 // Reset the cycle accumulator 229 cycleAccumulator = 0; 230 231 // Find all destroyed textures 232 foreach (id, tombstone; textures) { 233 234 if (!tombstone.isDestroyed) continue; 235 236 auto backend = cast() tombstone.backend; 237 238 // Unload the texture 239 backend.unloadTexture(id); 240 241 // Disown the tombstone and remove it from the registry 242 tombstone.markDisowned(); 243 textures.remove(id); 244 245 } 246 247 } 248 249 /// Destroy all textures. 250 void destroyAll() @system { 251 252 cycleAccumulator = 0; 253 scope (exit) textures.clear(); 254 255 // Find all textures 256 foreach (id, tombstone; textures) { 257 258 auto backend = cast() tombstone.backend; 259 260 // Unload the texture, even if it wasn't marked for deletion 261 backend.unloadTexture(id); 262 // TODO Should this be done? The destructor may be called from the GC. Maybe check if it was? 263 // Test this! 264 265 // Disown all textures 266 tombstone.markDisowned(); 267 268 } 269 270 } 271 272 } 273 274 /// Tombstones are used to ensure textures are freed on the same thread they have been created on. 275 /// 276 /// Tombstones are kept alive until the texture is explicitly destroyed and then finalized (disowned) from the main 277 /// thread by a periodically-running `TextureReaper`. This is necessary to make Fluid safe in multithreaded 278 /// environments. 279 shared struct TextureTombstone { 280 281 import core.memory; 282 import core.atomic; 283 import core.stdc.stdlib; 284 285 /// Backend that created this texture. 286 private FluidBackend _backend; 287 288 private int _references = 1; 289 private bool _disowned; 290 291 @disable this(this); 292 293 static TextureTombstone* make(FluidBackend backend) @system { 294 295 import core.exception; 296 297 // Allocate the tombstone 298 auto data = malloc(TextureTombstone.sizeof); 299 if (data is null) throw new OutOfMemoryError("Failed to allocate a tombstone"); 300 301 // Initialize the tombstone 302 shared tombstone = cast(shared TextureTombstone*) data; 303 *tombstone = TextureTombstone.init; 304 tombstone._backend = cast(shared) backend; 305 306 assert(tombstone.references == 1); 307 308 // Make sure the backend isn't freed while the tombstone is alive 309 GC.addRoot(cast(void*) backend); 310 311 return tombstone; 312 313 } 314 315 /// Check if a request for destruction has been made for the texture. 316 bool isDestroyed() @system { 317 return _references.atomicLoad == 0; 318 } 319 320 /// Check if the texture has been disowned by the backend. A disowned tombstone refers to a texture that has been 321 /// freed. 322 private bool isDisowned() @system { 323 return _disowned.atomicLoad; 324 } 325 326 /// Get number of references to this tombstone. 327 private int references() @system { 328 return _references.atomicLoad; 329 } 330 331 /// Get the backend owning this texture. 332 inout(shared FluidBackend) backend() inout { 333 return _backend; 334 } 335 336 /// Mark the texture as destroyed. 337 void markDestroyed() @system { 338 339 assert(!isDisowned || !isDestroyed, "Texture: Double destroy()"); 340 341 _references.atomicFetchSub(1); 342 tryDestroy(); 343 344 } 345 346 /// Mark the texture as disowned. 347 private void markDisowned() @system { 348 349 assert(!isDisowned || !isDestroyed); 350 351 _disowned.atomicStore(true); 352 tryDestroy(); 353 354 } 355 356 /// Mark the texture as copied. 357 private void markCopied() @system { 358 359 _references.atomicFetchAdd(1); 360 361 } 362 363 /// As soon as the texture is both marked for destruction and disowned, the tombstone controlling its life is 364 /// destroyed. 365 /// 366 /// There are two relevant scenarios: 367 /// 368 /// * The texture is marked for destruction via a tombstone, then finalized from the main thread and disowned. 369 /// * The texture is finalized after the backend (for example, if they are both destroyed during the same GC 370 /// collection). The backend disowns and frees the texture. The tombstone, however, remains alive to 371 /// witness marking the texture as deleted. 372 /// 373 /// In both scenarios, this behavior ensures the tombstone will be freed. 374 private void tryDestroy() @system { 375 376 // Destroyed and disowned 377 if (isDestroyed && isDisowned) { 378 379 GC.removeRoot(cast(void*) _backend); 380 free(cast(void*) &this); 381 382 } 383 384 } 385 386 } 387 388 @system 389 unittest { 390 391 // This unittest checks if textures will be correctly destroyed, even if the destruction call comes from another 392 // thread. 393 394 import std.concurrency; 395 import fluid.space; 396 import fluid.image_view; 397 398 auto io = new HeadlessBackend; 399 auto image = imageView("logo.png"); 400 auto root = vspace(image); 401 402 // Draw the frame once to let everything load 403 root.io = io; 404 root.draw(); 405 406 // Tune the reaper to run every frame 407 io.reaper.period = 1; 408 409 // Get the texture 410 auto texture = image.release(); 411 auto textureID = texture.id; 412 auto tombstone = texture.tombstone; 413 414 // Texture should be allocated and assigned a tombstone 415 assert(texture.backend is io); 416 assert(!texture.tombstone.isDestroyed); 417 assert(io.isTextureValid(texture)); 418 419 // Destroy the texture on another thread 420 spawn((shared Texture sharedTexture) { 421 422 auto texture = cast() sharedTexture; 423 texture.destroy(); 424 ownerTid.send(true); 425 426 }, cast(shared) texture); 427 428 // Wait for confirmation 429 receiveOnly!bool; 430 431 // The texture should be marked for deletion but remain alive 432 assert(texture.tombstone.isDestroyed); 433 assert(io.isTextureValid(texture)); 434 435 // Draw a frame, during which the reaper should destroy the texture 436 io.nextFrame; 437 root.children = []; 438 root.updateSize(); 439 root.draw(); 440 441 assert(!io.isTextureValid(texture)); 442 // There is no way to test if the tombstone has been freed 443 444 } 445 446 @system 447 unittest { 448 449 // This unittest checks if tombstones work correctly even if the backend is destroyed before the texture. 450 451 import std.concurrency; 452 import core.atomic; 453 import fluid.image_view; 454 455 auto io = new HeadlessBackend; 456 auto root = imageView("logo.png"); 457 458 // Load the texture and draw 459 root.io = io; 460 root.draw(); 461 462 // Destroy the backend 463 destroy(io); 464 465 auto texture = root.release(); 466 467 // The texture should have been automatically freed, but not marked for destruction 468 assert(!texture.tombstone.isDestroyed); 469 assert(texture.tombstone._disowned.atomicLoad); 470 471 // Now, destroy the image 472 // If this operation succeeds, we're good 473 destroy(root); 474 // There is no way to test if the tombstone and texture have truly been freed 475 476 } 477 478 struct FluidMouseCursor { 479 480 enum SystemCursors { 481 482 systemDefault, // Default system cursor. 483 none, // No pointer. 484 pointer, // Pointer indicating a link or button, typically a pointing hand. 👆 485 crosshair, // Cross cursor, often indicating selection inside images. 486 text, // Vertical beam indicating selectable text. 487 allScroll, // Omnidirectional scroll, content can be scrolled in any direction (panned). 488 resizeEW, // Cursor indicating the content underneath can be resized horizontally. 489 resizeNS, // Cursor indicating the content underneath can be resized vertically. 490 resizeNESW, // Diagonal resize cursor, top-right + bottom-left. 491 resizeNWSE, // Diagonal resize cursor, top-left + bottom-right. 492 notAllowed, // Indicates a forbidden action. 493 494 } 495 496 enum { 497 498 systemDefault = FluidMouseCursor(SystemCursors.systemDefault), 499 none = FluidMouseCursor(SystemCursors.none), 500 pointer = FluidMouseCursor(SystemCursors.pointer), 501 crosshair = FluidMouseCursor(SystemCursors.crosshair), 502 text = FluidMouseCursor(SystemCursors.text), 503 allScroll = FluidMouseCursor(SystemCursors.allScroll), 504 resizeEW = FluidMouseCursor(SystemCursors.resizeEW), 505 resizeNS = FluidMouseCursor(SystemCursors.resizeNS), 506 resizeNESW = FluidMouseCursor(SystemCursors.resizeNESW), 507 resizeNWSE = FluidMouseCursor(SystemCursors.resizeNWSE), 508 notAllowed = FluidMouseCursor(SystemCursors.notAllowed), 509 510 } 511 512 /// Use a system-provided cursor. 513 SystemCursors system; 514 // TODO user-provided cursor image 515 516 } 517 518 enum MouseButton { 519 none, 520 left, // Left (primary) mouse button. 521 right, // Right (secondary) mouse button. 522 middle, // Middle mouse button. 523 extra1, // Additional mouse button. 524 extra2, // ditto. 525 forward, // Mouse button going forward in browser history. 526 back, // Mouse button going back in browser history. 527 528 primary = left, 529 secondary = right, 530 531 } 532 533 enum GamepadButton { 534 535 none, // No such button 536 dpadUp, // Dpad up button. 537 dpadRight, // Dpad right button 538 dpadDown, // Dpad down button 539 dpadLeft, // Dpad left button 540 triangle, // Triangle (PS) or Y (Xbox) 541 circle, // Circle (PS) or B (Xbox) 542 cross, // Cross (PS) or A (Xbox) 543 square, // Square (PS) or X (Xbox) 544 leftButton, // Left button behind the controlller (LB). 545 leftTrigger, // Left trigger (LT). 546 rightButton, // Right button behind the controller (RB). 547 rightTrigger, // Right trigger (RT) 548 select, // "Select" button. 549 vendor, // Button with the vendor logo. 550 start, // "Start" button. 551 leftStick, // Left joystick press. 552 rightStick, // Right joystick press. 553 554 y = triangle, 555 x = square, 556 a = cross, 557 b = circle, 558 559 } 560 561 enum GamepadAxis { 562 563 leftX, // Left joystick, X axis. 564 leftY, // Left joystick, Y axis. 565 rightX, // Right joystick, X axis. 566 rightY, // Right joystick, Y axis. 567 leftTrigger, // Analog input for the left trigger. 568 rightTrigger, // Analog input for the right trigger. 569 570 } 571 572 enum KeyboardKey { 573 none = 0, // No key pressed 574 apostrophe = 39, // ' 575 comma = 44, // , 576 dash = comma, 577 minus = 45, // - 578 period = 46, // . 579 slash = 47, // / 580 digit0 = 48, // 0 581 digit1 = 49, // 1 582 digit2 = 50, // 2 583 digit3 = 51, // 3 584 digit4 = 52, // 4 585 digit5 = 53, // 5 586 digit6 = 54, // 6 587 digit7 = 55, // 7 588 digit8 = 56, // 8 589 digit9 = 57, // 9 590 semicolon = 59, // ; 591 equal = 61, // = 592 a = 65, // A | a 593 b = 66, // B | b 594 c = 67, // C | c 595 d = 68, // D | d 596 e = 69, // E | e 597 f = 70, // F | f 598 g = 71, // G | g 599 h = 72, // H | h 600 i = 73, // I | i 601 j = 74, // J | j 602 k = 75, // K | k 603 l = 76, // L | l 604 m = 77, // M | m 605 n = 78, // N | n 606 o = 79, // O | o 607 p = 80, // P | p 608 q = 81, // Q | q 609 r = 82, // R | r 610 s = 83, // S | s 611 t = 84, // T | t 612 u = 85, // U | u 613 v = 86, // V | v 614 w = 87, // W | w 615 x = 88, // X | x 616 y = 89, // Y | y 617 z = 90, // Z | z 618 leftBracket = 91, // [ 619 backslash = 92, // '\' 620 rightBracket = 93, // ] 621 backtick = 96, // ` 622 grave = backtick, 623 space = 32, // Space 624 escape = 256, // Esc 625 esc = escape, 626 enter = 257, // Enter 627 tab = 258, // Tab 628 backspace = 259, // Backspace 629 insert = 260, // Ins 630 del = 261, // Del 631 delete_ = del, 632 right = 262, // Cursor right 633 left = 263, // Cursor left 634 down = 264, // Cursor down 635 up = 265, // Cursor up 636 pageUp = 266, // Page up 637 pageDown = 267, // Page down 638 home = 268, // Home 639 end = 269, // End 640 capsLock = 280, // Caps lock 641 scrollLock = 281, // Scroll down 642 numLock = 282, // Num lock 643 printScreen = 283, // Print screen 644 pause = 284, // Pause 645 f1 = 290, // F1 646 f2 = 291, // F2 647 f3 = 292, // F3 648 f4 = 293, // F4 649 f5 = 294, // F5 650 f6 = 295, // F6 651 f7 = 296, // F7 652 f8 = 297, // F8 653 f9 = 298, // F9 654 f10 = 299, // F10 655 f11 = 300, // F11 656 f12 = 301, // F12 657 leftShift = 340, // Shift left 658 leftControl = 341, // Control left 659 leftAlt = 342, // Alt left 660 leftSuper = 343, // Super left 661 leftCommand = leftSuper, // Command left 662 leftOption = leftAlt, // Option left 663 rightShift = 344, // Shift right 664 rightControl = 345, // Control right 665 rightAlt = 346, // Alt right 666 rightSuper = 347, // Super right 667 rightCommand = rightSuper, // Command right 668 rightOption = rightAlt, // Option right 669 contextMenu = 348, // Context menu 670 keypad0 = 320, // Keypad 0 671 keypad1 = 321, // Keypad 1 672 keypad2 = 322, // Keypad 2 673 keypad3 = 323, // Keypad 3 674 keypad4 = 324, // Keypad 4 675 keypad5 = 325, // Keypad 5 676 keypad6 = 326, // Keypad 6 677 keypad7 = 327, // Keypad 7 678 keypad8 = 328, // Keypad 8 679 keypad9 = 329, // Keypad 9 680 keypadDecimal = 330, // Keypad . 681 keypadDivide = 331, // Keypad / 682 keypadMultiply = 332, // Keypad * 683 keypadSubtract = 333, // Keypad - 684 keypadSum = 334, // Keypad + 685 keypadEnter = 335, // Keypad Enter 686 keypadEqual = 336, // Keypad = 687 androidBack = 4, // Android back button 688 androidMenu = 82, // Android menu button 689 volumeUp = 24, // Android volume up button 690 volumeDown = 25 // Android volume down button 691 // Function keys for volume? 692 693 } 694 695 /// Generate an image filled with a given color. 696 /// 697 /// Note: Image data is GC-allocated. Make sure to keep a reference alive when passing to the backend. Do not use 698 /// `UnloadImage` if using Raylib. 699 static Image generateColorImage(int width, int height, Color color) { 700 701 // Generate each pixel 702 auto data = new Color[width * height]; 703 data[] = color; 704 705 return Image(data, width, height); 706 707 } 708 709 /// Generate a paletted image filled with 0-index pixels of given alpha value. 710 static Image generatePalettedImage(int width, int height, ubyte alpha) { 711 712 auto data = new PalettedColor[width * height]; 713 data[] = PalettedColor(0, alpha); 714 715 return Image(data, width, height); 716 717 } 718 719 /// Generate an alpha mask filled with given value. 720 static Image generateAlphaMask(int width, int height, ubyte value) { 721 722 auto data = new ubyte[width * height]; 723 data[] = value; 724 725 return Image(data, width, height); 726 727 } 728 729 /// A paletted pixel, for use in `palettedAlpha` images; Stores images using an index into a palette, along with an 730 /// alpha value. 731 struct PalettedColor { 732 733 ubyte index; 734 ubyte alpha; 735 736 } 737 738 /// Image available to the CPU. 739 struct Image { 740 741 enum Format { 742 743 /// RGBA, 8 bit per channel (32 bits per pixel). 744 rgba, 745 746 /// Paletted image with alpha channel (16 bits per pixel) 747 palettedAlpha, 748 749 /// Alpha-only image/mask (8 bits per pixel). 750 alpha, 751 752 } 753 754 Format format; 755 756 /// Image data. Make sure to access data relevant to the current format. 757 /// 758 /// Each format has associated data storage. `rgba` has `rgbaPixels`, `palettedAlpha` has `palettedAlphaPixels` and 759 /// `alpha` has `alphaPixels`. 760 Color[] rgbaPixels; 761 762 /// ditto 763 PalettedColor[] palettedAlphaPixels; 764 765 /// ditto 766 ubyte[] alphaPixels; 767 768 /// Palette data, if relevant. Access into an invalid palette index is equivalent to full white. 769 /// 770 /// For `palettedAlpha` images (and `PalettedColor` in general), the alpha value of each color in the palette is 771 /// ignored. 772 Color[] palette; 773 774 int width, height; 775 776 /// Create an RGBA image. 777 this(Color[] rgbaPixels, int width, int height) { 778 779 this.format = Format.rgba; 780 this.rgbaPixels = rgbaPixels; 781 this.width = width; 782 this.height = height; 783 784 } 785 786 /// Create a paletted image. 787 this(PalettedColor[] palettedAlphaPixels, int width, int height) { 788 789 this.format = Format.palettedAlpha; 790 this.palettedAlphaPixels = palettedAlphaPixels; 791 this.width = width; 792 this.height = height; 793 794 } 795 796 /// Create an alpha mask. 797 this(ubyte[] alphaPixels, int width, int height) { 798 799 this.format = Format.alpha; 800 this.alphaPixels = alphaPixels; 801 this.width = width; 802 this.height = height; 803 804 } 805 806 Vector2 size() const { 807 808 return Vector2(width, height); 809 810 } 811 812 int area() const { 813 814 return width * height; 815 816 } 817 818 /// Get a palette entry at given index. 819 Color paletteColor(PalettedColor pixel) const { 820 821 // Valid index, return the color; Set alpha to match the pixel 822 if (pixel.index < palette.length) 823 return palette[pixel.index].setAlpha(pixel.alpha); 824 825 // Invalid index, return white 826 else 827 return Color(0xff, 0xff, 0xff, pixel.alpha); 828 829 } 830 831 /// Get data of the image in raw form. 832 inout(void)[] data() inout { 833 834 final switch (format) { 835 836 case Format.rgba: 837 return rgbaPixels; 838 case Format.palettedAlpha: 839 return palettedAlphaPixels; 840 case Format.alpha: 841 return alphaPixels; 842 843 } 844 845 } 846 847 /// Get color at given position. Position must be in image bounds. 848 Color get(int x, int y) const { 849 850 const index = y * width + x; 851 852 final switch (format) { 853 854 case Format.rgba: 855 return rgbaPixels[index]; 856 case Format.palettedAlpha: 857 return paletteColor(palettedAlphaPixels[index]); 858 case Format.alpha: 859 return Color(0xff, 0xff, 0xff, alphaPixels[index]); 860 861 } 862 863 } 864 865 unittest { 866 867 auto colors = [ 868 PalettedColor(0, ubyte(0)), 869 PalettedColor(1, ubyte(127)), 870 PalettedColor(2, ubyte(127)), 871 PalettedColor(3, ubyte(255)), 872 ]; 873 874 auto image = Image(colors, 2, 2); 875 image.palette = [ 876 Color(0, 0, 0, 255), 877 Color(255, 0, 0, 255), 878 Color(0, 255, 0, 255), 879 Color(0, 0, 255, 255), 880 ]; 881 882 assert(image.get(0, 0) == Color(0, 0, 0, 0)); 883 assert(image.get(1, 0) == Color(255, 0, 0, 127)); 884 assert(image.get(0, 1) == Color(0, 255, 0, 127)); 885 assert(image.get(1, 1) == Color(0, 0, 255, 255)); 886 887 } 888 889 /// Set color at given position. Does nothing if position is out of bounds. 890 /// 891 /// The `set(int, int, Color)` overload only supports true color images. For paletted images, use 892 /// `set(int, int, PalettedColor)`. The latter can also be used for building true color images using a palette, if 893 /// one is supplied in the image at the time. 894 void set(int x, int y, Color color) { 895 896 if (x < 0 || y < 0) return; 897 if (x >= width || y >= height) return; 898 899 const index = y * width + x; 900 901 final switch (format) { 902 903 case Format.rgba: 904 rgbaPixels[index] = color; 905 return; 906 case Format.palettedAlpha: 907 assert(false, "Unsupported image format: Cannot `set` pixels by color in a paletted image."); 908 case Format.alpha: 909 alphaPixels[index] = color.a; 910 return; 911 912 } 913 914 } 915 916 /// ditto 917 void set(int x, int y, PalettedColor entry) { 918 919 if (x < 0 || y < 0) return; 920 if (x >= width || y >= height) return; 921 922 const index = y * width + x; 923 const color = paletteColor(entry); 924 925 final switch (format) { 926 927 case Format.rgba: 928 rgbaPixels[index] = color; 929 return; 930 case Format.palettedAlpha: 931 palettedAlphaPixels[index] = entry; 932 return; 933 case Format.alpha: 934 alphaPixels[index] = color.a; 935 return; 936 937 } 938 939 } 940 941 /// Clear the image, replacing every pixel with given color. 942 /// 943 /// The `clear(Color)` overload only supports true color images. For paletted images, use `clear(PalettedColor)`. 944 /// The latter can also be used for building true color images using a palette, if one is supplied in the image at 945 /// the time. 946 void clear(Color color) { 947 948 final switch (format) { 949 950 case Format.rgba: 951 rgbaPixels[] = color; 952 return; 953 case Format.palettedAlpha: 954 assert(false, "Unsupported image format: Cannot `clear` by color in a paletted image."); 955 case Format.alpha: 956 alphaPixels[] = color.a; 957 return; 958 959 } 960 961 } 962 963 /// ditto 964 void clear(PalettedColor entry) { 965 966 const color = paletteColor(entry); 967 968 final switch (format) { 969 970 case Format.rgba: 971 rgbaPixels[] = color; 972 return; 973 case Format.palettedAlpha: 974 palettedAlphaPixels[] = entry; 975 return; 976 case Format.alpha: 977 alphaPixels[] = color.a; 978 return; 979 980 } 981 982 } 983 984 } 985 986 987 /// Image or texture can be rendered by Fluid, for example, a texture stored in VRAM. 988 /// 989 /// Textures make use of manual memory management. See `TextureGC` for a GC-managed texture. 990 struct Texture { 991 992 /// Tombstone for this texture 993 shared(TextureTombstone)* tombstone; 994 995 /// Format of the texture. 996 Image.Format format; 997 998 /// GPU/backend ID of the texture. 999 uint id; 1000 1001 /// Width and height of the texture, **in dots**. The meaning of a dot is defined by `dpiX` and `dpiY` 1002 int width, height; 1003 1004 /// Dots per inch for the X and Y axis. Defaults to 96, thus making a dot in the texture equivalent to a pixel. 1005 int dpiX = 96, dpiY = 96; 1006 1007 /// If relevant, the texture is to use this palette. 1008 Color[] palette; 1009 1010 bool opEquals(const Texture other) const { 1011 return id == other.id 1012 && width == other.width 1013 && height == other.height 1014 && dpiX == other.dpiX 1015 && dpiY == other.dpiY; 1016 1017 } 1018 1019 version (Have_raylib_d) 1020 void opAssign(raylib.Texture rayTexture) @system { 1021 this = rayTexture.toFluid(); 1022 } 1023 1024 /// Get the backend for this texture. Doesn't work after freeing the tombstone. 1025 inout(FluidBackend) backend() inout @trusted { 1026 return cast(inout FluidBackend) tombstone.backend; 1027 } 1028 1029 /// DPI value of the texture. 1030 Vector2 dpi() const { 1031 return Vector2(dpiX, dpiY); 1032 } 1033 1034 /// Get texture size as a vector. 1035 Vector2 canvasSize() const { 1036 return Vector2(width, height); 1037 } 1038 1039 /// Get the size the texture will occupy within the viewport. 1040 Vector2 viewportSize() const { 1041 return Vector2( 1042 width * 96 / dpiX, 1043 height * 96 / dpiY 1044 ); 1045 } 1046 1047 /// Update the texture to match the given image. 1048 void update(Image image) @system { 1049 1050 backend.updateTexture(this, image); 1051 1052 } 1053 1054 /// Draw this texture. 1055 void draw(Vector2 position, Color tint = Color(0xff, 0xff, 0xff, 0xff)) { 1056 1057 auto rectangle = Rectangle(position.tupleof, viewportSize.tupleof); 1058 1059 backend.drawTexture(this, rectangle, tint); 1060 1061 } 1062 1063 void draw(Rectangle rectangle, Color tint = Color(0xff, 0xff, 0xff, 0xff)) { 1064 1065 backend.drawTexture(this, rectangle, tint); 1066 1067 } 1068 1069 /// Destroy this texture. This function is thread-safe. 1070 void destroy() @system { 1071 1072 if (tombstone is null) return; 1073 1074 tombstone.markDestroyed(); 1075 tombstone = null; 1076 id = 0; 1077 1078 } 1079 1080 } 1081 1082 /// Wrapper over `Texture` that automates destruction via GC or RAII. 1083 struct TextureGC { 1084 1085 /// Underlying texture. Lifetime is bound to this struct. 1086 Texture texture; 1087 1088 alias texture this; 1089 1090 /// Load a texture from filename. 1091 this(FluidBackend backend, string filename) @trusted { 1092 1093 this.texture = backend.loadTexture(filename); 1094 1095 } 1096 1097 /// Load a texture from image data. 1098 this(FluidBackend backend, Image data) @trusted { 1099 1100 this.texture = backend.loadTexture(data); 1101 1102 } 1103 1104 /// Move constructor for TextureGC; increment the reference counter for the texture. 1105 /// 1106 /// While I originally did not intend to implement reference counting, it is necessary to make TextureGC work in 1107 /// dynamic arrays. Changing the size of the array will copy the contents without performing a proper move of the 1108 /// old items. The postblit is the only kind of move constructor that will be called in this case, and a copy 1109 /// constructor does not do its job. 1110 this(this) @trusted { 1111 1112 if (tombstone) 1113 tombstone.markCopied(); 1114 1115 } 1116 1117 @system 1118 unittest { 1119 1120 import std.string; 1121 1122 // This tests using TextureGC inside of a dynamic array, especially after resizing. See documentation for 1123 // the postblit above. 1124 1125 // Test two variants: 1126 // * One, where we rely on the language to finalize the copied value 1127 // * And one, where we manually destroy the value 1128 foreach (explicitDestruction; [false, true]) { 1129 1130 void makeCopy(TextureGC[] arr) { 1131 1132 // Create the copy 1133 auto copy = arr; 1134 1135 assert(sameHead(arr, copy)); 1136 1137 // Expand the array, creating another 1138 copy.length = 1024; 1139 1140 assert(!sameHead(arr, copy)); 1141 1142 // References to tombstones exist in both arrays now 1143 assert(!copy[0].tombstone.isDestroyed); 1144 assert(!arr[0].tombstone.isDestroyed); 1145 1146 // The copy should be marked as moved 1147 assert(copy[0].tombstone.references == 2); 1148 assert(arr[0].tombstone.references == 2); 1149 1150 // Destroy the tombstone 1151 if (explicitDestruction) { 1152 1153 auto tombstone = copy[0].tombstone; 1154 1155 copy[0].destroy(); 1156 assert(tombstone.references == 1); 1157 assert(!tombstone.isDestroyed); 1158 1159 } 1160 1161 // Forget about the copy 1162 copy = null; 1163 1164 } 1165 1166 static void trashStack() { 1167 1168 import core.memory; 1169 1170 // Destroy the stack to get rid of any references to `copy` 1171 ubyte[2048] garbage; 1172 1173 // Collect it, make sure the tombstone gets eaten 1174 GC.collect(); 1175 1176 } 1177 1178 auto io = new HeadlessBackend; 1179 auto image = generateColorImage(10, 10, color("#fff")); 1180 auto arr = [ 1181 TextureGC(io, image), 1182 TextureGC.init, 1183 ]; 1184 1185 makeCopy(arr); 1186 trashStack(); 1187 1188 assert(!arr[0].tombstone.isDestroyed, "Tombstone of a live texture was destroyed after copying an array" 1189 ~ format!" (explicitDestruction %s)"(explicitDestruction)); 1190 1191 io.reaper.collect(); 1192 1193 assert(io.isTextureValid(arr[0])); 1194 assert(!arr[0].tombstone.isDestroyed); 1195 assert(!arr[0].tombstone.isDisowned); 1196 assert(arr[0].tombstone.references == 1); 1197 1198 } 1199 1200 } 1201 1202 @system 1203 unittest { 1204 1205 auto io = new HeadlessBackend; 1206 auto image = generateColorImage(10, 10, color("#fff")); 1207 auto arr = [ 1208 TextureGC(io, image), 1209 TextureGC.init, 1210 ]; 1211 auto copy = arr.dup; 1212 1213 assert(arr[0].tombstone.references == 2); 1214 1215 io.reaper.collect(); 1216 1217 assert(io.isTextureValid(arr[0])); 1218 1219 } 1220 1221 ~this() @trusted { 1222 1223 texture.destroy(); 1224 1225 } 1226 1227 /// Release the texture, moving it to manual management. 1228 Texture release() @system { 1229 1230 auto result = texture; 1231 texture = texture.init; 1232 return result; 1233 1234 } 1235 1236 } 1237 1238 /// Get a hex code from color. 1239 string toHex(string prefix = "#")(Color color) { 1240 1241 import std.format; 1242 1243 // Full alpha, use a six digit code 1244 if (color.a == 0xff) { 1245 1246 return format!(prefix ~ "%02x%02x%02x")(color.r, color.g, color.b); 1247 1248 } 1249 1250 // Include alpha otherwise 1251 else return format!(prefix ~ "%02x%02x%02x%02x")(color.tupleof); 1252 1253 } 1254 1255 unittest { 1256 1257 // No relevant alpha 1258 assert(color("fff").toHex == "#ffffff"); 1259 assert(color("ffff").toHex == "#ffffff"); 1260 assert(color("ffffff").toHex == "#ffffff"); 1261 assert(color("ffffffff").toHex == "#ffffff"); 1262 assert(color("fafbfc").toHex == "#fafbfc"); 1263 assert(color("123").toHex == "#112233"); 1264 1265 // Alpha set 1266 assert(color("c0fe").toHex == "#cc00ffee"); 1267 assert(color("1234").toHex == "#11223344"); 1268 assert(color("0000").toHex == "#00000000"); 1269 assert(color("12345678").toHex == "#12345678"); 1270 1271 } 1272 1273 /// Create a color from hex code. 1274 Color color(string hexCode)() { 1275 1276 return color(hexCode); 1277 1278 } 1279 1280 /// ditto 1281 Color color(string hexCode) pure { 1282 1283 import std.conv: to; 1284 import std.string : chompPrefix; 1285 1286 // Remove the # if there is any 1287 const hex = hexCode.chompPrefix("#"); 1288 1289 Color result; 1290 result.a = 0xff; 1291 1292 switch (hex.length) { 1293 1294 // 4 digit RGBA 1295 case 4: 1296 result.a = hex[3..4].to!ubyte(16); 1297 result.a *= 17; 1298 1299 // Parse the rest like RGB 1300 goto case; 1301 1302 // 3 digit RGB 1303 case 3: 1304 result.r = hex[0..1].to!ubyte(16); 1305 result.g = hex[1..2].to!ubyte(16); 1306 result.b = hex[2..3].to!ubyte(16); 1307 result.r *= 17; 1308 result.g *= 17; 1309 result.b *= 17; 1310 break; 1311 1312 // 8 digit RGBA 1313 case 8: 1314 result.a = hex[6..8].to!ubyte(16); 1315 goto case; 1316 1317 // 6 digit RGB 1318 case 6: 1319 result.r = hex[0..2].to!ubyte(16); 1320 result.g = hex[2..4].to!ubyte(16); 1321 result.b = hex[4..6].to!ubyte(16); 1322 break; 1323 1324 default: 1325 assert(false, "Invalid hex code length"); 1326 1327 } 1328 1329 return result; 1330 1331 } 1332 1333 unittest { 1334 1335 import std.exception; 1336 1337 assert(color!"#123" == Color(0x11, 0x22, 0x33, 0xff)); 1338 assert(color!"#1234" == Color(0x11, 0x22, 0x33, 0x44)); 1339 assert(color!"1234" == Color(0x11, 0x22, 0x33, 0x44)); 1340 assert(color!"123456" == Color(0x12, 0x34, 0x56, 0xff)); 1341 assert(color!"2a5592f0" == Color(0x2a, 0x55, 0x92, 0xf0)); 1342 1343 assertThrown(color!"ag5"); 1344 1345 } 1346 1347 /// Set the alpha channel for the given color, as a float. 1348 Color setAlpha(Color color, float alpha) { 1349 1350 import std.algorithm : clamp; 1351 1352 color.a = cast(ubyte) clamp(ubyte.max * alpha, 0, ubyte.max); 1353 return color; 1354 1355 } 1356 1357 Color setAlpha()(Color color, int alpha) { 1358 1359 static assert(false, "Overload setAlpha(Color, int). Explicitly choose setAlpha(Color, float) (0...1 range) or " 1360 ~ "setAlpha(Color, ubyte) (0...255 range)"); 1361 1362 } 1363 1364 /// Set the alpha channel for the given color, as a float. 1365 Color setAlpha(Color color, ubyte alpha) { 1366 1367 color.a = alpha; 1368 return color; 1369 1370 } 1371 1372 /// Blend two colors together; apply `top` on top of the `bottom` color. If `top` has maximum alpha, returns `top`. If 1373 /// alpha is zero, returns `bottom`. 1374 /// 1375 /// BUG: This function is currently broken and returns incorrect results. 1376 Color alphaBlend(Color bottom, Color top) { 1377 1378 auto topA = cast(float) top.a / ubyte.max; 1379 auto bottomA = (1 - topA) * cast(float) bottom.a / ubyte.max; 1380 1381 return Color( 1382 cast(ubyte) (bottom.r * bottomA + top.r * topA), 1383 cast(ubyte) (bottom.g * bottomA + top.g * topA), 1384 cast(ubyte) (bottom.b * bottomA + top.b * topA), 1385 cast(ubyte) (bottom.a * bottomA + top.a * topA), 1386 ); 1387 1388 } 1389 1390 /// Multiple color values. 1391 Color multiply(Color a, Color b) { 1392 1393 return Color( 1394 cast(ubyte) (a.r * b.r / 255.0), 1395 cast(ubyte) (a.g * b.g / 255.0), 1396 cast(ubyte) (a.b * b.b / 255.0), 1397 cast(ubyte) (a.a * b.a / 255.0), 1398 ); 1399 1400 } 1401 1402 unittest { 1403 1404 assert(multiply(color!"#fff", color!"#a00") == color!"#a00"); 1405 assert(multiply(color!"#1eff00", color!"#009bdd") == color!"#009b00"); 1406 assert(multiply(color!"#aaaa", color!"#1234") == color!"#0b16222d"); 1407 1408 } 1409 1410 version (unittest) { 1411 1412 debug (Fluid_BuildMessages) { 1413 pragma(msg, "Fluid: Using headless as the default backend (unittest)"); 1414 } 1415 1416 FluidBackend defaultFluidBackend() { 1417 1418 return new HeadlessBackend; 1419 1420 } 1421 1422 } 1423 1424 else version (Have_raylib_d) { 1425 1426 debug (Fluid_BuildMessages) { 1427 pragma(msg, "Fluid: Using Raylib 5 as the default backend"); 1428 } 1429 1430 FluidBackend defaultFluidBackend() { 1431 1432 return new Raylib5Backend; 1433 1434 } 1435 1436 } 1437 1438 else { 1439 1440 debug (Fluid_BuildMessages) { 1441 pragma(msg, "Fluid: No built-in backend in use"); 1442 } 1443 1444 FluidBackend defaultFluidBackend() { 1445 1446 return null; 1447 1448 } 1449 1450 } 1451 1452 // Structures 1453 version (Have_raylib_d) { 1454 1455 debug (Fluid_BuildMessages) { 1456 pragma(msg, "Fluid: Using Raylib core structures"); 1457 } 1458 1459 import raylib; 1460 1461 alias Rectangle = raylib.Rectangle; 1462 alias Vector2 = raylib.Vector2; 1463 alias Color = raylib.Color; 1464 1465 } 1466 1467 else { 1468 1469 struct Vector2 { 1470 1471 float x = 0; 1472 float y = 0; 1473 1474 mixin Linear; 1475 1476 } 1477 1478 struct Rectangle { 1479 1480 float x, y; 1481 float width, height; 1482 1483 alias w = width; 1484 alias h = height; 1485 1486 } 1487 1488 struct Color { 1489 1490 ubyte r, g, b, a; 1491 1492 } 1493 1494 /// `mixin Linear` taken from [raylib-d](https://github.com/schveiguy/raylib-d), reformatted and without Rotor3 1495 /// support. 1496 /// 1497 /// Licensed under the [z-lib license](https://github.com/schveiguy/raylib-d/blob/master/LICENSE). 1498 private mixin template Linear() { 1499 1500 private static alias T = typeof(this); 1501 private import std.traits : FieldNameTuple; 1502 1503 static T zero() { 1504 1505 enum fragment = { 1506 string result; 1507 static foreach(i; 0 .. T.tupleof.length) 1508 result ~= "0,"; 1509 return result; 1510 }(); 1511 1512 return mixin("T(", fragment, ")"); 1513 } 1514 1515 static T one() { 1516 1517 enum fragment = { 1518 string result; 1519 static foreach(i; 0 .. T.tupleof.length) 1520 result ~= "1,"; 1521 return result; 1522 }(); 1523 return mixin("T(", fragment, ")"); 1524 1525 } 1526 1527 inout T opUnary(string op)() if (op == "+" || op == "-") { 1528 1529 enum fragment = { 1530 string result; 1531 static foreach(fn; FieldNameTuple!T) 1532 result ~= op ~ fn ~ ","; 1533 return result; 1534 }(); 1535 return mixin("T(", fragment, ")"); 1536 1537 } 1538 1539 inout T opBinary(string op)(inout T rhs) if (op == "+" || op == "-") { 1540 1541 enum fragment = { 1542 string result; 1543 foreach(fn; FieldNameTuple!T) 1544 result ~= fn ~ op ~ "rhs." ~ fn ~ ","; 1545 return result; 1546 }(); 1547 return mixin("T(", fragment, ")"); 1548 1549 } 1550 1551 ref T opOpAssign(string op)(inout T rhs) if (op == "+" || op == "-") { 1552 1553 foreach (field; FieldNameTuple!T) 1554 mixin(field, op, "= rhs.", field, ";"); 1555 1556 return this; 1557 1558 } 1559 1560 inout T opBinary(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 1561 1562 enum fragment = { 1563 string result; 1564 foreach(fn; FieldNameTuple!T) 1565 result ~= fn ~ op ~ "rhs,"; 1566 return result; 1567 }(); 1568 return mixin("T(", fragment, ")"); 1569 1570 } 1571 1572 inout T opBinaryRight(string op)(inout float lhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 1573 1574 enum fragment = { 1575 string result; 1576 foreach(fn; FieldNameTuple!T) 1577 result ~= "lhs" ~ op ~ fn ~ ","; 1578 return result; 1579 }(); 1580 return mixin("T(", fragment, ")"); 1581 1582 } 1583 1584 ref T opOpAssign(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 1585 1586 foreach (field; FieldNameTuple!T) 1587 mixin(field, op, "= rhs;"); 1588 return this; 1589 1590 } 1591 } 1592 1593 }