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