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(0, alpha); 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 index; 722 ubyte alpha; 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 unittest { 854 855 auto colors = [ 856 PalettedColor(0, ubyte(0)), 857 PalettedColor(1, ubyte(127)), 858 PalettedColor(2, ubyte(127)), 859 PalettedColor(3, ubyte(255)), 860 ]; 861 862 auto image = Image(colors, 2, 2); 863 image.palette = [ 864 Color(0, 0, 0, 255), 865 Color(255, 0, 0, 255), 866 Color(0, 255, 0, 255), 867 Color(0, 0, 255, 255), 868 ]; 869 870 assert(image.get(0, 0) == Color(0, 0, 0, 0)); 871 assert(image.get(1, 0) == Color(255, 0, 0, 127)); 872 assert(image.get(0, 1) == Color(0, 255, 0, 127)); 873 assert(image.get(1, 1) == Color(0, 0, 255, 255)); 874 875 } 876 877 /// Set color at given position. Does nothing if position is out of bounds. 878 /// 879 /// The `set(int, int, Color)` overload only supports true color images. For paletted images, use 880 /// `set(int, int, PalettedColor)`. The latter can also be used for building true color images using a palette, if 881 /// one is supplied in the image at the time. 882 void set(int x, int y, Color color) { 883 884 if (x < 0 || y < 0) return; 885 if (x >= width || y >= height) return; 886 887 const index = y * width + x; 888 889 final switch (format) { 890 891 case Format.rgba: 892 rgbaPixels[index] = color; 893 return; 894 case Format.palettedAlpha: 895 assert(false, "Unsupported image format: Cannot `set` pixels by color in a paletted image."); 896 case Format.alpha: 897 alphaPixels[index] = color.a; 898 return; 899 900 } 901 902 } 903 904 /// ditto 905 void set(int x, int y, PalettedColor entry) { 906 907 if (x < 0 || y < 0) return; 908 if (x >= width || y >= height) return; 909 910 const index = y * width + x; 911 const color = paletteColor(entry); 912 913 final switch (format) { 914 915 case Format.rgba: 916 rgbaPixels[index] = color; 917 return; 918 case Format.palettedAlpha: 919 palettedAlphaPixels[index] = entry; 920 return; 921 case Format.alpha: 922 alphaPixels[index] = color.a; 923 return; 924 925 } 926 927 } 928 929 /// Clear the image, replacing every pixel with given color. 930 /// 931 /// The `clear(Color)` overload only supports true color images. For paletted images, use `clear(PalettedColor)`. 932 /// The latter can also be used for building true color images using a palette, if one is supplied in the image at 933 /// the time. 934 void clear(Color color) { 935 936 final switch (format) { 937 938 case Format.rgba: 939 rgbaPixels[] = color; 940 return; 941 case Format.palettedAlpha: 942 assert(false, "Unsupported image format: Cannot `clear` by color in a paletted image."); 943 case Format.alpha: 944 alphaPixels[] = color.a; 945 return; 946 947 } 948 949 } 950 951 /// ditto 952 void clear(PalettedColor entry) { 953 954 const color = paletteColor(entry); 955 956 final switch (format) { 957 958 case Format.rgba: 959 rgbaPixels[] = color; 960 return; 961 case Format.palettedAlpha: 962 palettedAlphaPixels[] = entry; 963 return; 964 case Format.alpha: 965 alphaPixels[] = color.a; 966 return; 967 968 } 969 970 } 971 972 } 973 974 975 /// Image or texture can be rendered by Fluid, for example, a texture stored in VRAM. 976 /// 977 /// Textures make use of manual memory management. See `TextureGC` for a GC-managed texture. 978 struct Texture { 979 980 /// Tombstone for this texture 981 shared(TextureTombstone)* tombstone; 982 983 /// Format of the texture. 984 Image.Format format; 985 986 /// GPU/backend ID of the texture. 987 uint id; 988 989 /// Width and height of the texture, **in dots**. The meaning of a dot is defined by `dpiX` and `dpiY` 990 int width, height; 991 992 /// Dots per inch for the X and Y axis. Defaults to 96, thus making a dot in the texture equivalent to a pixel. 993 int dpiX = 96, dpiY = 96; 994 995 /// If relevant, the texture is to use this palette. 996 Color[] palette; 997 998 bool opEquals(const Texture other) const 999 1000 => id == other.id 1001 && width == other.width 1002 && height == other.height 1003 && dpiX == other.dpiX 1004 && dpiY == other.dpiY; 1005 1006 version (Have_raylib_d)void opAssign(raylib.Texture rayTexture) @system { 1007 this = rayTexture.toFluid(); 1008 } 1009 1010 /// Get the backend for this texture. Doesn't work after freeing the tombstone. 1011 inout(FluidBackend) backend() inout @trusted 1012 1013 => cast(inout FluidBackend) tombstone.backend; 1014 1015 /// DPI value of the texture. 1016 Vector2 dpi() const 1017 1018 => Vector2(dpiX, dpiY); 1019 1020 /// Get texture size as a vector. 1021 Vector2 canvasSize() const 1022 1023 => Vector2(width, height); 1024 1025 /// Get the size the texture will occupy within the viewport. 1026 Vector2 viewportSize() const 1027 1028 => Vector2( 1029 width * 96 / dpiX, 1030 height * 96 / dpiY 1031 ); 1032 1033 /// Update the texture to match the given image. 1034 void update(Image image) @system { 1035 1036 backend.updateTexture(this, image); 1037 1038 } 1039 1040 /// Draw this texture. 1041 void draw(Vector2 position, Color tint = Color(0xff, 0xff, 0xff, 0xff)) { 1042 1043 auto rectangle = Rectangle(position.tupleof, viewportSize.tupleof); 1044 1045 backend.drawTexture(this, rectangle, tint); 1046 1047 } 1048 1049 void draw(Rectangle rectangle, Color tint = Color(0xff, 0xff, 0xff, 0xff)) { 1050 1051 backend.drawTexture(this, rectangle, tint); 1052 1053 } 1054 1055 /// Destroy this texture. This function is thread-safe. 1056 void destroy() @system { 1057 1058 if (tombstone is null) return; 1059 1060 tombstone.markDestroyed(); 1061 tombstone = null; 1062 id = 0; 1063 1064 } 1065 1066 } 1067 1068 /// Wrapper over `Texture` that automates destruction via GC or RAII. 1069 struct TextureGC { 1070 1071 /// Underlying texture. Lifetime is bound to this struct. 1072 Texture texture; 1073 1074 alias texture this; 1075 1076 /// Load a texture from filename. 1077 this(FluidBackend backend, string filename) @trusted { 1078 1079 this.texture = backend.loadTexture(filename); 1080 1081 } 1082 1083 /// Load a texture from image data. 1084 this(FluidBackend backend, Image data) @trusted { 1085 1086 this.texture = backend.loadTexture(data); 1087 1088 } 1089 1090 /// Move constructor for TextureGC; increment the reference counter for the texture. 1091 /// 1092 /// While I originally did not intend to implement reference counting, it is necessary to make TextureGC work in 1093 /// dynamic arrays. Changing the size of the array will copy the contents without performing a proper move of the 1094 /// old items. The postblit is the only kind of move constructor that will be called in this case, and a copy 1095 /// constructor does not do its job. 1096 this(this) @trusted { 1097 1098 if (tombstone) 1099 tombstone.markCopied(); 1100 1101 } 1102 1103 @system 1104 unittest { 1105 1106 import std.string; 1107 1108 // This tests using TextureGC inside of a dynamic array, especially after resizing. See documentation for 1109 // the postblit above. 1110 1111 // Test two variants: 1112 // * One, where we rely on the language to finalize the copied value 1113 // * And one, where we manually destroy the value 1114 foreach (explicitDestruction; [false, true]) { 1115 1116 void makeCopy(TextureGC[] arr) { 1117 1118 // Create the copy 1119 auto copy = arr; 1120 1121 assert(sameHead(arr, copy)); 1122 1123 // Expand the array, creating another 1124 copy.length = 1024; 1125 1126 assert(!sameHead(arr, copy)); 1127 1128 // References to tombstones exist in both arrays now 1129 assert(!copy[0].tombstone.isDestroyed); 1130 assert(!arr[0].tombstone.isDestroyed); 1131 1132 // The copy should be marked as moved 1133 assert(copy[0].tombstone.references == 2); 1134 assert(arr[0].tombstone.references == 2); 1135 1136 // Destroy the tombstone 1137 if (explicitDestruction) { 1138 1139 auto tombstone = copy[0].tombstone; 1140 1141 copy[0].destroy(); 1142 assert(tombstone.references == 1); 1143 assert(!tombstone.isDestroyed); 1144 1145 } 1146 1147 // Forget about the copy 1148 copy = null; 1149 1150 } 1151 1152 static void trashStack() { 1153 1154 import core.memory; 1155 1156 // Destroy the stack to get rid of any references to `copy` 1157 ubyte[2048] garbage; 1158 1159 // Collect it, make sure the tombstone gets eaten 1160 GC.collect(); 1161 1162 } 1163 1164 auto io = new HeadlessBackend; 1165 auto image = generateColorImage(10, 10, color("#fff")); 1166 auto arr = [ 1167 TextureGC(io, image), 1168 TextureGC.init, 1169 ]; 1170 1171 makeCopy(arr); 1172 trashStack(); 1173 1174 assert(!arr[0].tombstone.isDestroyed, "Tombstone of a live texture was destroyed after copying an array" 1175 ~ format!" (explicitDestruction %s)"(explicitDestruction)); 1176 1177 io.reaper.collect(); 1178 1179 assert(io.isTextureValid(arr[0])); 1180 assert(!arr[0].tombstone.isDestroyed); 1181 assert(!arr[0].tombstone.isDisowned); 1182 assert(arr[0].tombstone.references == 1); 1183 1184 } 1185 1186 } 1187 1188 @system 1189 unittest { 1190 1191 auto io = new HeadlessBackend; 1192 auto image = generateColorImage(10, 10, color("#fff")); 1193 auto arr = [ 1194 TextureGC(io, image), 1195 TextureGC.init, 1196 ]; 1197 auto copy = arr.dup; 1198 1199 assert(arr[0].tombstone.references == 2); 1200 1201 io.reaper.collect(); 1202 1203 assert(io.isTextureValid(arr[0])); 1204 1205 } 1206 1207 ~this() @trusted { 1208 1209 texture.destroy(); 1210 1211 } 1212 1213 /// Release the texture, moving it to manual management. 1214 Texture release() @system { 1215 1216 auto result = texture; 1217 texture = texture.init; 1218 return result; 1219 1220 } 1221 1222 } 1223 1224 /// Get a hex code from color. 1225 string toHex(string prefix = "#")(Color color) { 1226 1227 import std.format; 1228 1229 // Full alpha, use a six digit code 1230 if (color.a == 0xff) { 1231 1232 return format!(prefix ~ "%02x%02x%02x")(color.r, color.g, color.b); 1233 1234 } 1235 1236 // Include alpha otherwise 1237 else return format!(prefix ~ "%02x%02x%02x%02x")(color.tupleof); 1238 1239 } 1240 1241 unittest { 1242 1243 // No relevant alpha 1244 assert(color("fff").toHex == "#ffffff"); 1245 assert(color("ffff").toHex == "#ffffff"); 1246 assert(color("ffffff").toHex == "#ffffff"); 1247 assert(color("ffffffff").toHex == "#ffffff"); 1248 assert(color("fafbfc").toHex == "#fafbfc"); 1249 assert(color("123").toHex == "#112233"); 1250 1251 // Alpha set 1252 assert(color("c0fe").toHex == "#cc00ffee"); 1253 assert(color("1234").toHex == "#11223344"); 1254 assert(color("0000").toHex == "#00000000"); 1255 assert(color("12345678").toHex == "#12345678"); 1256 1257 } 1258 1259 /// Create a color from hex code. 1260 Color color(string hexCode)() { 1261 1262 return color(hexCode); 1263 1264 } 1265 1266 /// ditto 1267 Color color(string hexCode) pure { 1268 1269 import std.conv: to; 1270 import std.string : chompPrefix; 1271 1272 // Remove the # if there is any 1273 const hex = hexCode.chompPrefix("#"); 1274 1275 Color result; 1276 result.a = 0xff; 1277 1278 switch (hex.length) { 1279 1280 // 4 digit RGBA 1281 case 4: 1282 result.a = hex[3..4].to!ubyte(16); 1283 result.a *= 17; 1284 1285 // Parse the rest like RGB 1286 goto case; 1287 1288 // 3 digit RGB 1289 case 3: 1290 result.r = hex[0..1].to!ubyte(16); 1291 result.g = hex[1..2].to!ubyte(16); 1292 result.b = hex[2..3].to!ubyte(16); 1293 result.r *= 17; 1294 result.g *= 17; 1295 result.b *= 17; 1296 break; 1297 1298 // 8 digit RGBA 1299 case 8: 1300 result.a = hex[6..8].to!ubyte(16); 1301 goto case; 1302 1303 // 6 digit RGB 1304 case 6: 1305 result.r = hex[0..2].to!ubyte(16); 1306 result.g = hex[2..4].to!ubyte(16); 1307 result.b = hex[4..6].to!ubyte(16); 1308 break; 1309 1310 default: 1311 assert(false, "Invalid hex code length"); 1312 1313 } 1314 1315 return result; 1316 1317 } 1318 1319 unittest { 1320 1321 import std.exception; 1322 1323 assert(color!"#123" == Color(0x11, 0x22, 0x33, 0xff)); 1324 assert(color!"#1234" == Color(0x11, 0x22, 0x33, 0x44)); 1325 assert(color!"1234" == Color(0x11, 0x22, 0x33, 0x44)); 1326 assert(color!"123456" == Color(0x12, 0x34, 0x56, 0xff)); 1327 assert(color!"2a5592f0" == Color(0x2a, 0x55, 0x92, 0xf0)); 1328 1329 assertThrown(color!"ag5"); 1330 1331 } 1332 1333 /// Set the alpha channel for the given color, as a float. 1334 Color setAlpha(Color color, float alpha) { 1335 1336 import std.algorithm : clamp; 1337 1338 color.a = cast(ubyte) clamp(ubyte.max * alpha, 0, ubyte.max); 1339 return color; 1340 1341 } 1342 1343 Color setAlpha()(Color color, int alpha) { 1344 1345 static assert(false, "Overload setAlpha(Color, int). Explicitly choose setAlpha(Color, float) (0...1 range) or " 1346 ~ "setAlpha(Color, ubyte) (0...255 range)"); 1347 1348 } 1349 1350 /// Set the alpha channel for the given color, as a float. 1351 Color setAlpha(Color color, ubyte alpha) { 1352 1353 color.a = alpha; 1354 return color; 1355 1356 } 1357 1358 /// Blend two colors together; apply `top` on top of the `bottom` color. If `top` has maximum alpha, returns `top`. If 1359 /// alpha is zero, returns `bottom`. 1360 /// 1361 /// BUG: This function is currently broken and returns incorrect results. 1362 Color alphaBlend(Color bottom, Color top) { 1363 1364 auto topA = cast(float) top.a / ubyte.max; 1365 auto bottomA = (1 - topA) * cast(float) bottom.a / ubyte.max; 1366 1367 return Color( 1368 cast(ubyte) (bottom.r * bottomA + top.r * topA), 1369 cast(ubyte) (bottom.g * bottomA + top.g * topA), 1370 cast(ubyte) (bottom.b * bottomA + top.b * topA), 1371 cast(ubyte) (bottom.a * bottomA + top.a * topA), 1372 ); 1373 1374 } 1375 1376 /// Multiple color values. 1377 Color multiply(Color a, Color b) { 1378 1379 return Color( 1380 cast(ubyte) (a.r * b.r / 255.0), 1381 cast(ubyte) (a.g * b.g / 255.0), 1382 cast(ubyte) (a.b * b.b / 255.0), 1383 cast(ubyte) (a.a * b.a / 255.0), 1384 ); 1385 1386 } 1387 1388 unittest { 1389 1390 assert(multiply(color!"#fff", color!"#a00") == color!"#a00"); 1391 assert(multiply(color!"#1eff00", color!"#009bdd") == color!"#009b00"); 1392 assert(multiply(color!"#aaaa", color!"#1234") == color!"#0b16222d"); 1393 1394 } 1395 1396 version (unittest) { 1397 1398 debug (Fluid_BuildMessages) { 1399 pragma(msg, "Fluid: Using headless as the default backend (unittest)"); 1400 } 1401 1402 FluidBackend defaultFluidBackend() { 1403 1404 return new HeadlessBackend; 1405 1406 } 1407 1408 } 1409 1410 else version (Have_raylib_d) { 1411 1412 debug (Fluid_BuildMessages) { 1413 pragma(msg, "Fluid: Using Raylib 5 as the default backend"); 1414 } 1415 1416 FluidBackend defaultFluidBackend() { 1417 1418 return new Raylib5Backend; 1419 1420 } 1421 1422 } 1423 1424 else { 1425 1426 debug (Fluid_BuildMessages) { 1427 pragma(msg, "Fluid: No built-in backend in use"); 1428 } 1429 1430 FluidBackend defaultFluidBackend() { 1431 1432 return null; 1433 1434 } 1435 1436 } 1437 1438 // Structures 1439 version (Have_raylib_d) { 1440 1441 debug (Fluid_BuildMessages) { 1442 pragma(msg, "Fluid: Using Raylib core structures"); 1443 } 1444 1445 import raylib; 1446 1447 alias Rectangle = raylib.Rectangle; 1448 alias Vector2 = raylib.Vector2; 1449 alias Color = raylib.Color; 1450 1451 } 1452 1453 else { 1454 1455 struct Vector2 { 1456 1457 float x = 0; 1458 float y = 0; 1459 1460 mixin Linear; 1461 1462 } 1463 1464 struct Rectangle { 1465 1466 float x, y; 1467 float width, height; 1468 1469 alias w = width; 1470 alias h = height; 1471 1472 } 1473 1474 struct Color { 1475 1476 ubyte r, g, b, a; 1477 1478 } 1479 1480 /// `mixin Linear` taken from [raylib-d](https://github.com/schveiguy/raylib-d), reformatted and without Rotor3 1481 /// support. 1482 /// 1483 /// Licensed under the [z-lib license](https://github.com/schveiguy/raylib-d/blob/master/LICENSE). 1484 private mixin template Linear() { 1485 1486 private static alias T = typeof(this); 1487 private import std.traits : FieldNameTuple; 1488 1489 static T zero() { 1490 1491 enum fragment = { 1492 string result; 1493 static foreach(i; 0 .. T.tupleof.length) 1494 result ~= "0,"; 1495 return result; 1496 }(); 1497 1498 return mixin("T(", fragment, ")"); 1499 } 1500 1501 static T one() { 1502 1503 enum fragment = { 1504 string result; 1505 static foreach(i; 0 .. T.tupleof.length) 1506 result ~= "1,"; 1507 return result; 1508 }(); 1509 return mixin("T(", fragment, ")"); 1510 1511 } 1512 1513 inout T opUnary(string op)() if (op == "+" || op == "-") { 1514 1515 enum fragment = { 1516 string result; 1517 static foreach(fn; FieldNameTuple!T) 1518 result ~= op ~ fn ~ ","; 1519 return result; 1520 }(); 1521 return mixin("T(", fragment, ")"); 1522 1523 } 1524 1525 inout T opBinary(string op)(inout T rhs) if (op == "+" || op == "-") { 1526 1527 enum fragment = { 1528 string result; 1529 foreach(fn; FieldNameTuple!T) 1530 result ~= fn ~ op ~ "rhs." ~ fn ~ ","; 1531 return result; 1532 }(); 1533 return mixin("T(", fragment, ")"); 1534 1535 } 1536 1537 ref T opOpAssign(string op)(inout T rhs) if (op == "+" || op == "-") { 1538 1539 foreach (field; FieldNameTuple!T) 1540 mixin(field, op, "= rhs.", field, ";"); 1541 1542 return this; 1543 1544 } 1545 1546 inout T opBinary(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 1547 1548 enum fragment = { 1549 string result; 1550 foreach(fn; FieldNameTuple!T) 1551 result ~= fn ~ op ~ "rhs,"; 1552 return result; 1553 }(); 1554 return mixin("T(", fragment, ")"); 1555 1556 } 1557 1558 inout T opBinaryRight(string op)(inout float lhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 1559 1560 enum fragment = { 1561 string result; 1562 foreach(fn; FieldNameTuple!T) 1563 result ~= "lhs" ~ op ~ fn ~ ","; 1564 return result; 1565 }(); 1566 return mixin("T(", fragment, ")"); 1567 1568 } 1569 1570 ref T opOpAssign(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 1571 1572 foreach (field; FieldNameTuple!T) 1573 mixin(field, op, "= rhs;"); 1574 return this; 1575 1576 } 1577 } 1578 1579 }