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.algorithm; 11 12 public import fluid.backend.raylib5; 13 public import fluid.backend.headless; 14 public import fluid.backend.simpledisplay; 15 16 17 @safe: 18 19 20 alias VoidDelegate = void delegate() @safe; 21 22 static FluidBackend defaultFluidBackend; 23 24 /// `FluidBackend` is an interface making it possible to bind Fluid to a library other than Raylib. Another built-in 25 /// backend is `fluid.simpledisplay.SimpledisplayBackend` for `arsd.simpledisplay`. 26 /// 27 /// The default unit in graphical space is a **pixel** (`px`), here defined as **1/96 of an inch**. This is unless 28 /// stated otherwise, as in `Texture`. 29 interface FluidBackend { 30 31 /// Check if the given mouse button has just been pressed/released or, if it's held down or not (up). 32 bool isPressed(MouseButton) const; 33 bool isReleased(MouseButton) const; 34 bool isDown(MouseButton) const; 35 bool isUp(MouseButton) const; 36 37 /// Check if the given keyboard key has just been pressed/released or, if it's held down or not (up). 38 bool isPressed(KeyboardKey) const; 39 bool isReleased(KeyboardKey) const; 40 bool isDown(KeyboardKey) const; 41 bool isUp(KeyboardKey) const; 42 43 /// If true, the given keyboard key has been virtually pressed again, through a long-press. 44 bool isRepeated(KeyboardKey) const; 45 46 /// Get next queued character from user's input. The queue should be cleared every frame. Return null if no 47 /// character was pressed. 48 dchar inputCharacter(); 49 50 /// Check if the given gamepad button has been pressed/released or, if it's held down or not (up) on any of the 51 /// connected gamepads. 52 /// 53 /// Returns: 0 if the event isn't taking place on any controller, or number of the controller. 54 int isPressed(GamepadButton button) const; 55 int isReleased(GamepadButton button) const; 56 int isDown(GamepadButton button) const; 57 int isUp(GamepadButton button) const; 58 59 /// If true, the given gamepad button has been virtually pressed again, through a long-press. 60 /// 61 /// Returns: 0 if no controller had a button repeat this frame, or number of the controller. 62 int isRepeated(GamepadButton button) const; 63 64 /// Get/set mouse position 65 Vector2 mousePosition(Vector2); 66 Vector2 mousePosition() const; 67 68 /// Get time elapsed since last frame in seconds. 69 float deltaTime() const; 70 71 /// True if the user has just resized the window. 72 bool hasJustResized() const; 73 74 /// Get or set the size of the window. 75 Vector2 windowSize(Vector2); 76 Vector2 windowSize() const; /// ditto 77 78 /// Set scale to apply to whatever is drawn next. 79 /// 80 /// Suggested implementation is to increase return value of `dpi`. 81 float scale() const; 82 83 /// ditto 84 float scale(float); 85 86 /// Get horizontal and vertical DPI of the window. 87 Vector2 dpi() const; 88 89 /// Get the DPI value for the window as a scale relative to 96 DPI. 90 final Vector2 hidpiScale() const { 91 92 const dpi = this.dpi; 93 return Vector2(dpi.x / 96f, dpi.y / 96f); 94 95 } 96 97 /// Set area within the window items will be drawn to; any pixel drawn outside will be discarded. 98 Rectangle area(Rectangle rect); 99 Rectangle area() const; 100 101 /// Restore the capability to draw anywhere in the window. 102 void restoreArea(); 103 104 /// Get or set mouse cursor icon. 105 FluidMouseCursor mouseCursor(FluidMouseCursor); 106 FluidMouseCursor mouseCursor() const; 107 108 /// Texture reaper used by this backend. May be null. 109 /// 110 /// Highly recommended for OpenGL-based backends. 111 TextureReaper* reaper() return scope; 112 113 /// Load a texture from memory or file. 114 Texture loadTexture(Image image) @system; 115 Texture loadTexture(string filename) @system; 116 117 /// Destroy a texture created by this backend. Always use `texture.destroy()` to ensure thread safety and invoking 118 /// the correct backend. 119 protected void unloadTexture(uint id) @system; 120 121 /// ditto 122 final void unloadTexture(Texture texture) @system { 123 124 unloadTexture(texture.id); 125 126 } 127 128 /// Draw a line. 129 void drawLine(Vector2 start, Vector2 end, Color color); 130 131 /// Draw a triangle, consisting of 3 vertices with counter-clockwise winding. 132 void drawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color); 133 134 /// Draw a rectangle. 135 void drawRectangle(Rectangle rectangle, Color color); 136 137 /// Draw a texture. 138 void drawTexture(Texture texture, Rectangle rectangle, Color tint, string altText = "") 139 in (texture.backend is this, "Given texture comes from a different backend"); 140 141 /// Draw a texture, but ensure it aligns with pixel boundaries, recommended for text. 142 void drawTextureAlign(Texture texture, Rectangle rectangle, Color tint, string altText = "") 143 in (texture.backend is this, "Given texture comes from a different backend"); 144 145 } 146 147 /// Struct that maintains a registry of all allocated textures. It's used to finalize textures once they have been 148 /// marked for destruction. This makes it possible to mark them from any thread, while the reaper runs only on the main 149 /// thread, ensuring thread safety in OpenGL backends. 150 struct TextureReaper { 151 152 /// Number of cycles between runs of the reaper. 153 int period = 60 * 5; 154 155 int cycleAccumulator; 156 157 @system shared(TextureTombstone)*[uint] textures; 158 159 @disable this(ref TextureReaper); 160 @disable this(this); 161 162 ~this() @trusted { 163 164 destroyAll(); 165 166 } 167 168 /// Create a tombstone. 169 shared(TextureTombstone)* makeTombstone(FluidBackend backend, uint textureID) @trusted { 170 171 return textures[textureID] = TextureTombstone.make(backend); 172 173 } 174 175 /// Count number of cycles since last collection and collect if configured period has passed. 176 void check() { 177 178 // Count cycles 179 if (++cycleAccumulator >= period) { 180 181 // Run collection 182 collect(); 183 184 } 185 186 } 187 188 /// Collect all destroyed textures immediately. 189 void collect() @trusted { 190 191 // Reset the cycle accumulator 192 cycleAccumulator = 0; 193 194 // Find all destroyed textures 195 foreach (id, tombstone; textures) { 196 197 // Texture marked for deletion 198 if (tombstone.isDestroyed) { 199 200 auto backend = cast() tombstone.backend; 201 202 // Unload it 203 backend.unloadTexture(id); 204 tombstone.markDisowned(); 205 206 // Remove the texture from registry 207 textures.remove(id); 208 209 } 210 211 } 212 213 } 214 215 /// Destroy all textures. 216 void destroyAll() @system { 217 218 cycleAccumulator = 0; 219 scope (exit) textures.clear(); 220 221 // Find all textures 222 foreach (id, tombstone; textures) { 223 224 auto backend = cast() tombstone.backend; 225 226 // Unload the texture, even if it wasn't marked for deletion 227 backend.unloadTexture(id); 228 229 // Disown all textures 230 tombstone.markDisowned(); 231 232 } 233 234 } 235 236 } 237 238 /// Tombstones are used to ensure textures are freed on the same thread they have been created on. 239 /// 240 /// Tombstones are kept alive until the texture is explicitly destroyed and then finalized (disowned) from the main 241 /// thread by a periodically-running `TextureReaper`. This is necessary to make Fluid safe in multithreaded 242 /// environments. 243 shared struct TextureTombstone { 244 245 import core.memory; 246 import core.atomic; 247 import core.stdc.stdlib; 248 249 /// Backend that created this texture. 250 private FluidBackend _backend; 251 252 private bool _destroyed, _disowned; 253 254 static TextureTombstone* make(FluidBackend backend) @system { 255 256 import core.exception; 257 258 // Allocate the tombstone 259 auto data = malloc(TextureTombstone.sizeof); 260 if (data is null) throw new OutOfMemoryError("Failed to allocate a tombstone"); 261 262 // Initialize the tombstone 263 shared tombstone = cast(shared TextureTombstone*) data; 264 *tombstone = TextureTombstone.init; 265 tombstone._backend = cast(shared) backend; 266 267 // Make sure the backend isn't freed while the tombstone is alive 268 GC.addRoot(cast(void*) backend); 269 270 return tombstone; 271 272 } 273 274 /// Check if the texture has been destroyed. 275 bool isDestroyed() @system => _destroyed.atomicLoad; 276 277 /// Get the backend owning this texture. 278 inout(shared FluidBackend) backend() inout => _backend; 279 280 /// Mark the texture as destroyed. 281 void markDestroyed() @system { 282 283 _destroyed.atomicStore(true); 284 tryDestroy(); 285 286 } 287 288 /// Mark the texture as disowned. 289 void markDisowned() @system { 290 291 _disowned.atomicStore(true); 292 tryDestroy(); 293 294 } 295 296 /// As soon as the texture is both marked for destruction and disowned, the tombstone controlling its life is 297 /// destroyed. 298 /// 299 /// There are two relevant scenarios: 300 /// 301 /// * The texture is marked for destruction via a tombstone, then finalized from the main thread and disowned. 302 /// * The texture is finalized after the backend (for example, if they are both destroyed during the same GC 303 /// collection). The backend disowns and frees the texture. The tombstone, however, remains alive to 304 /// witness marking the texture as deleted. 305 /// 306 /// In both scenarios, this behavior ensures the tombstone will be freed. 307 private void tryDestroy() @system { 308 309 // Destroyed and disowned 310 if (_destroyed.atomicLoad && _disowned.atomicLoad) { 311 312 GC.removeRoot(cast(void*) _backend); 313 free(cast(void*) &this); 314 315 } 316 317 } 318 319 } 320 321 @system 322 unittest { 323 324 // This unittest checks if textures will be correctly destroyed, even if the destruction call comes from another 325 // thread. 326 327 import std.concurrency; 328 import fluid.space; 329 import fluid.image_view; 330 331 auto io = new HeadlessBackend; 332 auto image = imageView("logo.png"); 333 auto root = vspace(image); 334 335 // Draw the frame once to let everything load 336 root.io = io; 337 root.draw(); 338 339 // Tune the reaper to run every frame 340 io.reaper.period = 1; 341 342 // Get the texture 343 auto texture = image.release(); 344 auto textureID = texture.id; 345 auto tombstone = texture.tombstone; 346 347 // Texture should be allocated and assigned a tombstone 348 assert(texture.backend is io); 349 assert(!texture.tombstone.isDestroyed); 350 assert(io.isTextureValid(texture)); 351 352 // Destroy the texture on another thread 353 spawn((Texture texture) { 354 355 texture.destroy(); 356 ownerTid.send(true); 357 358 }, texture); 359 360 // Wait for confirmation 361 receiveOnly!bool; 362 363 // The texture should be marked for deletion but remain alive 364 assert(texture.tombstone.isDestroyed); 365 assert(io.isTextureValid(texture)); 366 367 // Draw a frame, during which the reaper should destroy the texture 368 io.nextFrame; 369 root.children = []; 370 root.updateSize(); 371 root.draw(); 372 373 assert(!io.isTextureValid(texture)); 374 // There is no way to test if the tombstone has been freed 375 376 } 377 378 @system 379 unittest { 380 381 // This unittest checks if tombstones work correctly even if the backend is destroyed before the texture. 382 383 import std.concurrency; 384 import core.atomic; 385 import fluid.image_view; 386 387 auto io = new HeadlessBackend; 388 auto root = imageView("logo.png"); 389 390 // Load the texture and draw 391 root.io = io; 392 root.draw(); 393 394 // Destroy the backend 395 destroy(io); 396 397 auto texture = root.release(); 398 399 // The texture should have been automatically freed, but not marked for destruction 400 assert(!texture.tombstone.isDestroyed); 401 assert(texture.tombstone._disowned.atomicLoad); 402 403 // Now, destroy the image 404 // If this operation succeeds, we're good 405 destroy(root); 406 // There is no way to test if the tombstone and texture have truly been freed 407 408 } 409 410 struct FluidMouseCursor { 411 412 enum SystemCursors { 413 414 systemDefault, // Default system cursor. 415 none, // No pointer. 416 pointer, // Pointer indicating a link or button, typically a pointing hand. 👆 417 crosshair, // Cross cursor, often indicating selection inside images. 418 text, // Vertical beam indicating selectable text. 419 allScroll, // Omnidirectional scroll, content can be scrolled in any direction (panned). 420 resizeEW, // Cursor indicating the content underneath can be resized horizontally. 421 resizeNS, // Cursor indicating the content underneath can be resized vertically. 422 resizeNESW, // Diagonal resize cursor, top-right + bottom-left. 423 resizeNWSE, // Diagonal resize cursor, top-left + bottom-right. 424 notAllowed, // Indicates a forbidden action. 425 426 } 427 428 enum { 429 430 systemDefault = FluidMouseCursor(SystemCursors.systemDefault), 431 none = FluidMouseCursor(SystemCursors.none), 432 pointer = FluidMouseCursor(SystemCursors.pointer), 433 crosshair = FluidMouseCursor(SystemCursors.crosshair), 434 text = FluidMouseCursor(SystemCursors.text), 435 allScroll = FluidMouseCursor(SystemCursors.allScroll), 436 resizeEW = FluidMouseCursor(SystemCursors.resizeEW), 437 resizeNS = FluidMouseCursor(SystemCursors.resizeNS), 438 resizeNESW = FluidMouseCursor(SystemCursors.resizeNESW), 439 resizeNWSE = FluidMouseCursor(SystemCursors.resizeNWSE), 440 notAllowed = FluidMouseCursor(SystemCursors.notAllowed), 441 442 } 443 444 /// Use a system-provided cursor. 445 SystemCursors system; 446 // TODO user-provided cursor image 447 448 } 449 450 enum MouseButton { 451 none, 452 left, // Left (primary) mouse button. 453 right, // Right (secondary) mouse button. 454 middle, // Middle mouse button. 455 extra1, // Additional mouse button. 456 extra2, // ditto. 457 forward, // Mouse button going forward in browser history. 458 back, // Mouse button going back in browser history. 459 scrollUp, // Scroll one step up. 460 scrollDown, // Scroll one step down. 461 scrollLeft, // Scroll one step left. 462 scrollRight, // Scroll one step right. 463 464 primary = left, 465 secondary = right, 466 467 } 468 469 /// Check if the given mouse button is a scroll wheel step. 470 bool isScroll(MouseButton button) { 471 472 return button.scrollUp <= button && button <= button.scrollRight; 473 474 } 475 476 enum GamepadButton { 477 478 none, // No such button 479 dpadUp, // Dpad up button. 480 dpadRight, // Dpad right button 481 dpadDown, // Dpad down button 482 dpadLeft, // Dpad left button 483 triangle, // Triangle (PS) or Y (Xbox) 484 circle, // Circle (PS) or B (Xbox) 485 cross, // Cross (PS) or A (Xbox) 486 square, // Square (PS) or X (Xbox) 487 leftButton, // Left button behind the controlller (LB). 488 leftTrigger, // Left trigger (LT). 489 rightButton, // Right button behind the controller (RB). 490 rightTrigger, // Right trigger (RT) 491 select, // "Select" button. 492 vendor, // Button with the vendor logo. 493 start, // "Start" button. 494 leftStick, // Left joystick press. 495 rightStick, // Right joystick press. 496 497 y = triangle, 498 x = square, 499 a = cross, 500 b = circle, 501 502 } 503 504 enum GamepadAxis { 505 506 leftX, // Left joystick, X axis. 507 leftY, // Left joystick, Y axis. 508 rightX, // Right joystick, X axis. 509 rightY, // Right joystick, Y axis. 510 leftTrigger, // Analog input for the left trigger. 511 rightTrigger, // Analog input for the right trigger. 512 513 } 514 515 enum KeyboardKey { 516 none = 0, // No key pressed 517 apostrophe = 39, // ' 518 comma = 44, // , 519 dash = comma, 520 minus = 45, // - 521 period = 46, // . 522 slash = 47, // / 523 digit0 = 48, // 0 524 digit1 = 49, // 1 525 digit2 = 50, // 2 526 digit3 = 51, // 3 527 digit4 = 52, // 4 528 digit5 = 53, // 5 529 digit6 = 54, // 6 530 digit7 = 55, // 7 531 digit8 = 56, // 8 532 digit9 = 57, // 9 533 semicolon = 59, // ; 534 equal = 61, // = 535 a = 65, // A | a 536 b = 66, // B | b 537 c = 67, // C | c 538 d = 68, // D | d 539 e = 69, // E | e 540 f = 70, // F | f 541 g = 71, // G | g 542 h = 72, // H | h 543 i = 73, // I | i 544 j = 74, // J | j 545 k = 75, // K | k 546 l = 76, // L | l 547 m = 77, // M | m 548 n = 78, // N | n 549 o = 79, // O | o 550 p = 80, // P | p 551 q = 81, // Q | q 552 r = 82, // R | r 553 s = 83, // S | s 554 t = 84, // T | t 555 u = 85, // U | u 556 v = 86, // V | v 557 w = 87, // W | w 558 x = 88, // X | x 559 y = 89, // Y | y 560 z = 90, // Z | z 561 leftBracket = 91, // [ 562 backslash = 92, // '\' 563 rightBracket = 93, // ] 564 backtick = 96, // ` 565 grave = backtick, 566 space = 32, // Space 567 escape = 256, // Esc 568 esc = escape, 569 enter = 257, // Enter 570 tab = 258, // Tab 571 backspace = 259, // Backspace 572 insert = 260, // Ins 573 del = 261, // Del 574 delete_ = del, 575 right = 262, // Cursor right 576 left = 263, // Cursor left 577 down = 264, // Cursor down 578 up = 265, // Cursor up 579 pageUp = 266, // Page up 580 pageDown = 267, // Page down 581 home = 268, // Home 582 end = 269, // End 583 capsLock = 280, // Caps lock 584 scrollLock = 281, // Scroll down 585 numLock = 282, // Num lock 586 printScreen = 283, // Print screen 587 pause = 284, // Pause 588 f1 = 290, // F1 589 f2 = 291, // F2 590 f3 = 292, // F3 591 f4 = 293, // F4 592 f5 = 294, // F5 593 f6 = 295, // F6 594 f7 = 296, // F7 595 f8 = 297, // F8 596 f9 = 298, // F9 597 f10 = 299, // F10 598 f11 = 300, // F11 599 f12 = 301, // F12 600 leftShift = 340, // Shift left 601 leftControl = 341, // Control left 602 leftAlt = 342, // Alt left 603 leftSuper = 343, // Super left 604 rightShift = 344, // Shift right 605 rightControl = 345, // Control right 606 rightAlt = 346, // Alt right 607 rightSuper = 347, // Super right 608 contextMenu = 348, // Context menu 609 keypad0 = 320, // Keypad 0 610 keypad1 = 321, // Keypad 1 611 keypad2 = 322, // Keypad 2 612 keypad3 = 323, // Keypad 3 613 keypad4 = 324, // Keypad 4 614 keypad5 = 325, // Keypad 5 615 keypad6 = 326, // Keypad 6 616 keypad7 = 327, // Keypad 7 617 keypad8 = 328, // Keypad 8 618 keypad9 = 329, // Keypad 9 619 keypadDecimal = 330, // Keypad . 620 keypadDivide = 331, // Keypad / 621 keypadMultiply = 332, // Keypad * 622 keypadSubtract = 333, // Keypad - 623 keypadSum = 334, // Keypad + 624 keypadEnter = 335, // Keypad Enter 625 keypadEqual = 336, // Keypad = 626 androidBack = 4, // Android back button 627 androidMenu = 82, // Android menu button 628 volumeUp = 24, // Android volume up button 629 volumeDown = 25 // Android volume down button 630 // Function keys for volume? 631 632 } 633 634 /// Generate an image filled with a given color. 635 /// 636 /// Note: Image data is GC-allocated. Make sure to keep a reference alive when passing to the backend. Do not use 637 /// `UnloadImage` if using Raylib. 638 static Image generateColorImage(int width, int height, Color color) { 639 640 // Generate each pixel 641 auto data = new Color[width * height]; 642 data[] = color; 643 644 // Prepare the result 645 Image image; 646 image.pixels = data; 647 image.width = width; 648 image.height = height; 649 650 return image; 651 652 } 653 654 /// Image available to the CPU. 655 struct Image { 656 657 /// Image data. 658 Color[] pixels; 659 int width, height; 660 661 Vector2 size() const => Vector2(width, height); 662 663 ref inout(Color) get(int x, int y) inout { 664 665 return pixels[y * width + x]; 666 667 } 668 669 /// Safer alternative to `get`, doesn't draw out of bounds. 670 void set(int x, int y, Color color) { 671 672 if (x < 0 || y < 0) return; 673 if (x >= width || y >= height) return; 674 675 get(x, y) = color; 676 677 } 678 679 } 680 681 /// Represents a GPU texture. 682 /// 683 /// Textures make use of manual memory management. 684 struct Texture { 685 686 /// Tombstone for this texture 687 shared(TextureTombstone)* tombstone; 688 689 /// GPU/backend ID of the texture. 690 uint id; 691 692 /// Width and height of the texture, **in dots**. The meaning of a dot is defined by `dpiX` and `dpiY` 693 int width, height; 694 695 /// Dots per inch for the X and Y axis. Defaults to 96, thus making a dot in the texture equivalent to a pixel. 696 int dpiX = 96, dpiY = 96; 697 698 bool opEquals(const Texture other) const 699 700 => id == other.id 701 && width == other.width 702 && height == other.height 703 && dpiX == other.dpiX 704 && dpiY == other.dpiY; 705 706 /// Get the backend for this texture. Doesn't work after freeing the tombstone. 707 inout(FluidBackend) backend() inout @trusted 708 709 => cast(inout FluidBackend) tombstone.backend; 710 711 /// DPI value of the texture. 712 Vector2 dpi() const 713 714 => Vector2(dpiX, dpiY); 715 716 /// Get texture size as a vector. 717 Vector2 canvasSize() const 718 719 => Vector2(width, height); 720 721 /// Get the size the texture will occupy within the viewport. 722 Vector2 viewportSize() const 723 724 => Vector2( 725 width * 96 / dpiX, 726 height * 96 / dpiY 727 ); 728 729 /// Draw this texture. 730 void draw(Vector2 position, Color tint = color!"fff") { 731 732 auto rectangle = Rectangle(position.tupleof, viewportSize.tupleof); 733 734 backend.drawTexture(this, rectangle, tint); 735 736 } 737 738 void draw(Rectangle rectangle, Color tint = color!"fff") { 739 740 backend.drawTexture(this, rectangle, tint); 741 742 } 743 744 /// Destroy this texture. This function is thread-safe. 745 void destroy() @system { 746 747 if (tombstone is null) return; 748 749 tombstone.markDestroyed(); 750 tombstone = null; 751 id = 0; 752 753 } 754 755 } 756 757 /// Get a hex code from color. 758 string toHex(string prefix = "#")(Color color) { 759 760 import std.format; 761 762 // Full alpha, use a six digit code 763 if (color.a == 0xff) { 764 765 return format!(prefix ~ "%02x%02x%02x")(color.r, color.g, color.b); 766 767 } 768 769 // Include alpha otherwise 770 else return format!(prefix ~ "%02x%02x%02x%02x")(color.tupleof); 771 772 } 773 774 unittest { 775 776 // No relevant alpha 777 assert(color("fff").toHex == "#ffffff"); 778 assert(color("ffff").toHex == "#ffffff"); 779 assert(color("ffffff").toHex == "#ffffff"); 780 assert(color("ffffffff").toHex == "#ffffff"); 781 assert(color("fafbfc").toHex == "#fafbfc"); 782 assert(color("123").toHex == "#112233"); 783 784 // Alpha set 785 assert(color("c0fe").toHex == "#cc00ffee"); 786 assert(color("1234").toHex == "#11223344"); 787 assert(color("0000").toHex == "#00000000"); 788 assert(color("12345678").toHex == "#12345678"); 789 790 } 791 792 /// Create a color from hex code. 793 Color color(string hexCode)() { 794 795 return color(hexCode); 796 797 } 798 799 /// ditto 800 Color color(string hexCode) pure { 801 802 import std.string : chompPrefix; 803 import std.format : format, formattedRead; 804 805 // Remove the # if there is any 806 const hex = hexCode.chompPrefix("#"); 807 808 Color result; 809 result.a = 0xff; 810 811 switch (hex.length) { 812 813 // 4 digit RGBA 814 case 4: 815 formattedRead!"%x"(hex[3..4], result.a); 816 result.a *= 17; 817 818 // Parse the rest like RGB 819 goto case; 820 821 // 3 digit RGB 822 case 3: 823 formattedRead!"%x"(hex[0..1], result.r); 824 formattedRead!"%x"(hex[1..2], result.g); 825 formattedRead!"%x"(hex[2..3], result.b); 826 result.r *= 17; 827 result.g *= 17; 828 result.b *= 17; 829 break; 830 831 // 8 digit RGBA 832 case 8: 833 formattedRead!"%x"(hex[6..8], result.a); 834 goto case; 835 836 // 6 digit RGB 837 case 6: 838 formattedRead!"%x"(hex[0..2], result.r); 839 formattedRead!"%x"(hex[2..4], result.g); 840 formattedRead!"%x"(hex[4..6], result.b); 841 break; 842 843 default: 844 assert(false, "Invalid hex code length"); 845 846 } 847 848 return result; 849 850 } 851 852 unittest { 853 854 import std.exception; 855 856 assert(color!"#123" == Color(0x11, 0x22, 0x33, 0xff)); 857 assert(color!"#1234" == Color(0x11, 0x22, 0x33, 0x44)); 858 assert(color!"1234" == Color(0x11, 0x22, 0x33, 0x44)); 859 assert(color!"123456" == Color(0x12, 0x34, 0x56, 0xff)); 860 assert(color!"2a5592f0" == Color(0x2a, 0x55, 0x92, 0xf0)); 861 862 assertThrown(color!"ag5"); 863 864 } 865 866 /// Set the alpha channel for the given color, as a float. 867 Color setAlpha(Color color, float alpha) { 868 869 import std.algorithm : clamp; 870 871 color.a = cast(ubyte) clamp(ubyte.max * alpha, 0, ubyte.max); 872 return color; 873 874 } 875 876 /// Blend two colors together; apply `top` on top of the `bottom` color. If `top` has maximum alpha, returns `top`. If 877 /// alpha is zero, returns `bottom`. 878 Color alphaBlend(Color bottom, Color top) { 879 880 auto topA = cast(float) top.a / ubyte.max; 881 auto bottomA = (1 - topA) * cast(float) bottom.a / ubyte.max; 882 883 return Color( 884 cast(ubyte) (bottom.r * bottomA + top.r * topA), 885 cast(ubyte) (bottom.g * bottomA + top.g * topA), 886 cast(ubyte) (bottom.b * bottomA + top.b * topA), 887 cast(ubyte) (bottom.a * bottomA + top.a * topA), 888 ); 889 890 } 891 892 version (Have_raylib_d) { 893 894 import raylib; 895 896 debug (Fluid_BuildMessages) { 897 pragma(msg, "Fluid: Using Raylib 5 as the default backend"); 898 } 899 900 static this() { 901 902 defaultFluidBackend = new Raylib5Backend; 903 904 } 905 906 alias Rectangle = raylib.Rectangle; 907 alias Vector2 = raylib.Vector2; 908 alias Color = raylib.Color; 909 910 } 911 912 else { 913 914 debug (Fluid_BuildMessages) { 915 pragma(msg, "Fluid: No built-in backend in use"); 916 } 917 918 struct Vector2 { 919 920 float x = 0; 921 float y = 0; 922 923 mixin Linear; 924 925 } 926 927 struct Rectangle { 928 929 float x, y; 930 float width, height; 931 932 alias w = width; 933 alias h = height; 934 935 } 936 937 struct Color { 938 939 ubyte r, g, b, a; 940 941 } 942 943 /// `mixin Linear` taken from [raylib-d](https://github.com/schveiguy/raylib-d), reformatted and without Rotor3 944 /// support. 945 /// 946 /// Licensed under the [z-lib license](https://github.com/schveiguy/raylib-d/blob/master/LICENSE). 947 private mixin template Linear() { 948 949 private static alias T = typeof(this); 950 private import std.traits : FieldNameTuple; 951 952 static T zero() { 953 954 enum fragment = { 955 string result; 956 static foreach(i; 0 .. T.tupleof.length) 957 result ~= "0,"; 958 return result; 959 }(); 960 961 return mixin("T(", fragment, ")"); 962 } 963 964 static T one() { 965 966 enum fragment = { 967 string result; 968 static foreach(i; 0 .. T.tupleof.length) 969 result ~= "1,"; 970 return result; 971 }(); 972 return mixin("T(", fragment, ")"); 973 974 } 975 976 inout T opUnary(string op)() if (op == "+" || op == "-") { 977 978 enum fragment = { 979 string result; 980 static foreach(fn; FieldNameTuple!T) 981 result ~= op ~ fn ~ ","; 982 return result; 983 }(); 984 return mixin("T(", fragment, ")"); 985 986 } 987 988 inout T opBinary(string op)(inout T rhs) if (op == "+" || op == "-") { 989 990 enum fragment = { 991 string result; 992 foreach(fn; FieldNameTuple!T) 993 result ~= fn ~ op ~ "rhs." ~ fn ~ ","; 994 return result; 995 }(); 996 return mixin("T(", fragment, ")"); 997 998 } 999 1000 ref T opOpAssign(string op)(inout T rhs) if (op == "+" || op == "-") { 1001 1002 foreach (field; FieldNameTuple!T) 1003 mixin(field, op, "= rhs.", field, ";"); 1004 1005 return this; 1006 1007 } 1008 1009 inout T opBinary(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 1010 1011 enum fragment = { 1012 string result; 1013 foreach(fn; FieldNameTuple!T) 1014 result ~= fn ~ op ~ "rhs,"; 1015 return result; 1016 }(); 1017 return mixin("T(", fragment, ")"); 1018 1019 } 1020 1021 inout T opBinaryRight(string op)(inout float lhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 1022 1023 enum fragment = { 1024 string result; 1025 foreach(fn; FieldNameTuple!T) 1026 result ~= "lhs" ~ op ~ fn ~ ","; 1027 return result; 1028 }(); 1029 return mixin("T(", fragment, ")"); 1030 1031 } 1032 1033 ref T opOpAssign(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 1034 1035 foreach (field; FieldNameTuple!T) 1036 mixin(field, op, "= rhs;"); 1037 return this; 1038 1039 } 1040 } 1041 1042 }