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