1 /// Raylib connection layer for Fluid. This makes it possible to render Fluid apps and user interfaces through Raylib. 2 /// 3 /// Use `raylibStack` for a complete implementation, and `raylibView` for a minimal one. The complete stack 4 /// is recommended for most usages, as it bundles full mouse and keyboard support, while the chain node may 5 /// be preferred for advanced usage and requires manual setup. See `RaylibView`'s documentation for more 6 /// information. 7 /// 8 /// Note that because Raylib introduces breaking changes in every version, the current version of Raylib should 9 /// be specified using `raylibStack.v5_5()`. Raylib 5.5 is currently the oldest version supported, 10 /// and is the default in case no version is chosen explicitly. 11 /// 12 /// Unlike `fluid.backend.Raylib5Backend`, this uses the new I/O system introduced in Fluid 0.8.0. This layer 13 /// is recommended for new apps, but disabled by default. 14 module fluid.raylib_view; 15 16 version (Have_raylib_d): 17 18 debug (Fluid_BuildMessages) { 19 pragma(msg, "Fluid: Building with Raylib 5.5 support (RaylibView)"); 20 } 21 22 // Coordinate scaling will translate Fluid coordinates, where each pixels is 1/96th of an inch, to screen coordinates, 23 // making use of DPI information provided by the system. This flag is only set on macOS, where the system handles this 24 // automatically. 25 version (OSX) { 26 version = Fluid_DisableScaling; 27 28 debug (Fluid_BuildMessages) { 29 pragma(msg, "Fluid: Disabling coordinate scaling on macOS"); 30 } 31 } 32 33 import raylib; 34 import optional; 35 36 import std.meta; 37 import std.traits; 38 import std.array; 39 import std.string; 40 41 import fluid.node; 42 import fluid.utils; 43 import fluid.types; 44 import fluid.node_chain; 45 46 import fluid.future.arena; 47 48 import fluid.backend.raylib5 : Raylib5Backend, toRaylib; 49 import fluid.backend.headless : HeadlessBackend; 50 51 import fluid.io.time; 52 import fluid.io.canvas; 53 import fluid.io.hover; 54 import fluid.io.mouse; 55 import fluid.io.focus; 56 import fluid.io.keyboard; 57 import fluid.io.clipboard; 58 import fluid.io.image_load; 59 import fluid.io.preference; 60 import fluid.io.overlay; 61 62 static if (!__traits(compiles, IsShaderReady)) 63 private alias IsShaderReady = IsShaderValid; 64 65 @safe: 66 67 /// `raylibStack` implements all I/O functionality needed for Fluid to function, using Raylib to read user input 68 /// and present visuals on the screen. 69 /// 70 /// Specify Raylib version by using a member: `raylibStack.v5_5()` will create a stack for Raylib 5.5. 71 /// 72 /// `raylibStack` provides a default implementation for `TimeIO`, `PreferenceIO`, `HoverIO`, `FocusIO`, `ActionIO` 73 /// and `FileIO`, on top of all the systems provided by Raylib itself: `CanvasIO`, `KeyboardIO`, `MouseIO`, 74 /// `ClipboardIO` and `ImageLoadIO`. 75 enum raylibStack = RaylibViewBuilder!RaylibStack.init; 76 77 /// `raylibView` implements some I/O functionality using the Raylib library, namely `CanvasIO`, `KeyboardIO`, 78 /// `MouseIO`, `ClipboardIO` and `ImageLoadIO`. 79 /// 80 /// These systems are not enough for Fluid to function. Use `raylibStack` to also initialize all other necessary 81 /// systems. 82 /// 83 /// Specify Raylib version by using a member: `raylibView.v5_5()` will create a stack for Raylib 5.5. 84 enum raylibView = RaylibViewBuilder!RaylibView.init; 85 86 /// Use this enum to pick version of Raylib to use. 87 enum RaylibViewVersion { 88 v5_5, 89 } 90 91 /// Wrapper over `NodeBuilder` which enables specifying Raylib version. 92 struct RaylibViewBuilder(alias T) { 93 94 alias v5_5 this; 95 enum v5_5 = nodeBuilder!(T!(RaylibViewVersion.v5_5)); 96 97 } 98 99 /// Implements Raylib support through Fluid's I/O system. Use `raylibStack` or `raylibView` to construct. 100 /// 101 /// `RaylibView` relies on a number of I/O systems that it does not implement, but must be provided for it 102 /// to function. Use `RaylibStack` to initialize the chain along with default choices for these systems, 103 /// suitable for most uses, or provide these systems as parent nodes: 104 /// 105 /// * `HoverIO` for mouse support. Fluid does not presently support mobile devices through Raylib, and Raylib's 106 /// desktop version does not fully support touchscreens (as GLFW does not). 107 /// * `FocusIO` for keyboard and gamepad support. Gamepad support may currently be limited. 108 /// * `TimeIO` for measuring time between mouse clicks. 109 /// * `PreferenceIO` for user preferences from the system. 110 /// 111 /// There is a few systems that `RaylibView` does not require, but are included in `RaylibStack` to support 112 /// commonly needed functionality: 113 /// 114 /// * `ActionIO` for translating user input into a format Fluid nodes can understand. 115 /// * `FileIO` for loading and writing files. 116 /// 117 /// `RaylibView` itself provides a number of I/O systems using functionality from the Raylib library: 118 /// 119 /// * `CanvasIO` for drawing nodes and providing visual output. 120 /// * `MouseIO` to provide mouse support. 121 /// * `KeyboardIO` to provide keyboard support. 122 /// * `ClipboardIO` to access system keyboard. 123 /// * `ImageLoadIO` to load images using codecs available in Raylib. 124 class RaylibView(RaylibViewVersion raylibVersion) : Node, CanvasIO, MouseIO, KeyboardIO, ClipboardIO, ImageLoadIO { 125 126 HoverIO hoverIO; 127 FocusIO focusIO; 128 TimeIO timeIO; 129 PreferenceIO preferenceIO; 130 131 public { 132 133 /// Node drawn by this view. 134 Node next; 135 136 /// Scale set for this view. It can be controlled through the `FLUID_SCALE` environment 137 /// variable (expected to be a float, e.g. `1.5` or a pair of floats `1.5x1.25`) 138 /// 139 /// Changing the scale requires an `updateSize` call. 140 auto scale = Vector2(1, 1); 141 142 } 143 144 private struct RaylibImage { 145 fluid.Image image; 146 raylib.Texture texture; 147 } 148 149 private { 150 151 // Window state 152 Vector2 _dpi; 153 Vector2 _dpiScale; 154 Vector2 _windowSize; 155 Rectangle _cropArea; 156 157 // Resources 158 ResourceArena!RaylibImage _images; 159 raylib.Texture _paletteTexture; 160 Shader _alphaImageShader; 161 Shader _palettedAlphaImageShader; 162 int _palettedAlphaImageShader_palette; 163 164 /// Map of image pointers (image.data.ptr) to indices in the resource arena (_images) 165 int[size_t] _imageIndices; 166 167 // I/O 168 HoverPointer _mousePointer; 169 Appender!(KeyboardKey[]) _heldKeys; 170 MultipleClickSensor _multiClickSensor; 171 172 } 173 174 this(Node next = null) { 175 176 this.next = next; 177 this.scale = getGlobalScale(); 178 179 // Initialize the mouse 180 _mousePointer.device = this; 181 _mousePointer.number = 0; 182 183 } 184 185 override void resizeImpl(Vector2) @trusted { 186 187 require(focusIO); 188 require(hoverIO); 189 require(timeIO); 190 require(preferenceIO); 191 hoverIO.loadTo(_mousePointer); 192 193 // Fetch data from Raylib 194 _dpiScale = GetWindowScaleDPI; 195 _dpi = Vector2(_dpiScale.x * scale.x * 96, _dpiScale.y * scale.y * 96); 196 _windowSize = toFluid(GetScreenWidth, GetScreenHeight); 197 resetCropArea(); 198 199 // Load shaders 200 if (!IsShaderReady(_alphaImageShader)) { 201 _alphaImageShader = LoadShaderFromMemory(null, Raylib5Backend.alphaImageShaderCode.ptr); 202 } 203 if (!IsShaderReady(_palettedAlphaImageShader)) { 204 _palettedAlphaImageShader = LoadShaderFromMemory(null, Raylib5Backend.palettedAlphaImageShaderCode.ptr); 205 _palettedAlphaImageShader_palette = GetShaderLocation(_palettedAlphaImageShader, "palette"); 206 } 207 208 // Free resources 209 _images.startCycle((newIndex, ref resource) @trusted { 210 211 const id = cast(size_t) resource.image.data.ptr; 212 213 // Resource freed 214 if (newIndex == -1) { 215 _imageIndices.remove(id); 216 UnloadTexture(resource.texture); 217 } 218 219 // Moved 220 else { 221 _imageIndices[id] = newIndex; 222 } 223 224 }); 225 226 // Enable the system 227 auto io = this.implementIO(); 228 229 // Resize the node 230 if (next) { 231 resizeChild(next, _windowSize); 232 } 233 234 // RaylibView does not take space in whatever it is placed in 235 minSize = Vector2(); 236 237 } 238 239 override void drawImpl(Rectangle, Rectangle) @trusted { 240 241 updateMouse(); 242 updateKeyboard(); 243 244 if (next) { 245 resetCropArea(); 246 drawChild(next, _cropArea); 247 } 248 249 if (IsWindowResized) { 250 updateSize(); 251 } 252 253 } 254 255 protected void updateMouse() @trusted { 256 257 import fluid.backend : FluidMouseCursor; 258 259 // Update mouse status 260 _mousePointer.position = toFluid(GetMousePosition); 261 _mousePointer.scroll = scroll(); 262 _mousePointer.isScrollHeld = false; 263 _mousePointer.clickCount = 0; 264 265 // Detect multiple mouse clicks 266 if (IsMouseButtonDown(MouseButton.MOUSE_BUTTON_LEFT)) { 267 _multiClickSensor.hold(timeIO, preferenceIO, _mousePointer); 268 _mousePointer.clickCount = _multiClickSensor.clicks; 269 } 270 else if (IsMouseButtonReleased(MouseButton.MOUSE_BUTTON_LEFT)) { 271 _multiClickSensor.activate(); 272 _mousePointer.clickCount = _multiClickSensor.clicks; 273 } 274 275 hoverIO.loadTo(_mousePointer); 276 277 // Set cursor icon 278 if (auto node = cast(Node) hoverIO.hoverOf(_mousePointer)) { 279 280 const cursor = node.pickStyle().mouseCursor; 281 282 // Hide the cursor if requested 283 if (cursor.system == cursor.system.none) { 284 HideCursor(); 285 } 286 // Show the cursor 287 else { 288 SetMouseCursor(cursor.system.toRaylib); 289 ShowCursor(); 290 } 291 292 } 293 else { 294 SetMouseCursor(FluidMouseCursor.systemDefault.system.toRaylib); 295 ShowCursor(); 296 } 297 298 // Send buttons 299 foreach (button; NoDuplicates!(EnumMembers!(MouseIO.Button))) { 300 301 const buttonRay = button.toRaylibEx; 302 303 if (buttonRay == -1) continue; 304 305 // Active event 306 if (IsMouseButtonReleased(buttonRay)) { 307 hoverIO.emitEvent(_mousePointer, MouseIO.createEvent(button, true)); 308 } 309 310 else if (IsMouseButtonDown(buttonRay)) { 311 hoverIO.emitEvent(_mousePointer, MouseIO.createEvent(button, false)); 312 } 313 314 } 315 316 } 317 318 protected void updateKeyboard() @trusted { 319 320 import std.utf; 321 322 // Take text input character by character 323 while (true) { 324 325 // TODO take more at once 326 char[4] buffer; 327 328 const ch = cast(dchar) GetCharPressed(); 329 if (ch == 0) break; 330 331 const size = buffer.encode(ch); 332 focusIO.typeText(buffer[0 .. size]); 333 334 } 335 336 // Find all newly pressed keyboard keys 337 while (true) { 338 339 const keyRay = cast(KeyboardKey) GetKeyPressed(); 340 if (keyRay == 0) break; 341 342 _heldKeys ~= keyRay; 343 344 } 345 346 size_t newIndex; 347 foreach (keyRay; _heldKeys[]) { 348 349 const key = keyRay.toFluid; 350 351 // Pressed 352 if (IsKeyPressed(keyRay) || IsKeyPressedRepeat(keyRay)) { 353 focusIO.emitEvent(KeyboardIO.createEvent(key, true)); 354 _heldKeys[][newIndex++] = keyRay; 355 } 356 357 // Held 358 else if (IsKeyDown(keyRay)) { 359 focusIO.emitEvent(KeyboardIO.createEvent(key, false)); 360 _heldKeys[][newIndex++] = keyRay; 361 } 362 363 } 364 _heldKeys.shrinkTo(newIndex); 365 366 } 367 368 /// Returns: 369 /// Distance travelled by the mouse in Fluid coordinates. 370 private Vector2 scroll() @trusted { 371 372 const move = -GetMouseWheelMoveV; 373 const speed = preferenceIO.scrollSpeed; 374 375 return Vector2(move.x * speed.x, move.y * speed.y); 376 377 } 378 379 override Vector2 dpi() const nothrow { 380 return _dpi; 381 } 382 383 override Optional!Rectangle cropArea() const nothrow { 384 return typeof(return)(_cropArea); 385 } 386 387 override void cropArea(Rectangle area) nothrow @trusted { 388 _cropArea = area; 389 const rectRay = toRaylib(area); 390 BeginScissorMode( 391 cast(int) rectRay.x, 392 cast(int) rectRay.y, 393 cast(int) rectRay.width, 394 cast(int) rectRay.height, 395 ); 396 } 397 398 override void resetCropArea() nothrow { 399 cropArea(Rectangle(0, 0, _windowSize.tupleof)); 400 } 401 402 /// Convert position of a point or rectangle in Fluid space to Raylib space. 403 /// Params: 404 /// position = Fluid position, where each coordinate is specified in pixels (1/96th of an inch). 405 /// rectangle = Rectangle in Fluid space. 406 /// Returns: 407 /// Raylib position or rectangle, where each coordinate is specified in screen dots. 408 Vector2 toRaylib(Vector2 position) const nothrow { 409 410 version (Fluid_DisableScaling) 411 return position; 412 else 413 return toDots(position); 414 415 } 416 417 /// ditto 418 Rectangle toRaylib(Rectangle rectangle) const nothrow { 419 420 version (Fluid_DisableScaling) 421 return rectangle; 422 else 423 return Rectangle( 424 toDots(rectangle.start).tupleof, 425 toDots(rectangle.size).tupleof, 426 ); 427 428 } 429 430 /// Convert position of a point or rectangle in Raylib space to Fluid space. 431 /// Params: 432 /// position = Raylib position, where each coordinate is specified in screen dots. 433 /// rectangle = Rectangle in Raylib space. 434 /// Returns: 435 /// Position in Fluid space 436 Vector2 toFluid(Vector2 position) const nothrow { 437 438 version (Fluid_DisableScaling) 439 return position; 440 else 441 return fromDots(position); 442 443 } 444 445 /// ditto 446 Vector2 toFluid(float x, float y) const nothrow { 447 448 version (Fluid_DisableScaling) 449 return Vector2(x, y); 450 else 451 return fromDots(Vector2(x, y)); 452 453 } 454 455 /// ditto 456 Rectangle toFluid(Rectangle rectangle) const nothrow { 457 458 version (Fluid_DisableScaling) 459 return rectangle; 460 else 461 return Rectangle( 462 fromDots(rectangle.start).tupleof, 463 fromDots(rectangle.size).tupleof, 464 ); 465 466 } 467 468 /// Get a Raylib texture for the corresponding drawable image. The image MUST be loaded. 469 raylib.Texture textureFor(DrawableImage image) nothrow @trusted { 470 return _images[image.id].texture; 471 } 472 473 /// Get the shader used for `alpha` images. This shader is loaded on the first resize, 474 /// and is not accessible before. 475 /// Returns: 476 /// Shader used for images with the `alpha` format set. 477 Shader alphaImageShader() nothrow @trusted { 478 assert(IsShaderReady(_alphaImageShader), "alphaImageShader is not accessible before resize"); 479 return _alphaImageShader; 480 } 481 482 /// Get the shader used for `palettedAlpha` images. This shader is loaded on the first resize, 483 /// and is not accessible before. 484 /// Params: 485 /// palette = Palette to use with the shader. 486 /// Returns: 487 /// Shader used for images with the `palettedAlpha` format set. 488 Shader palettedAlphaImageShader(Color[] palette) nothrow @trusted { 489 assert(IsShaderReady(_palettedAlphaImageShader), "palettedAlphaImageShader is not accessible before resize"); 490 491 auto paletteTexture = this.paletteTexture(palette); 492 493 // Load the palette 494 SetShaderValueTexture(_palettedAlphaImageShader, _palettedAlphaImageShader_palette, paletteTexture); 495 496 return _palettedAlphaImageShader; 497 } 498 499 /// Create a palette texture. 500 private raylib.Texture paletteTexture(scope Color[] colors) nothrow @trusted 501 in (colors.length <= 256, "There can only be at most 256 colors in a palette.") 502 do { 503 504 // Fill empty slots in the palette with white 505 Color[256] allColors = Color(0xff, 0xff, 0xff, 0xff); 506 allColors[0 .. colors.length] = colors; 507 508 // Prepare an image for the texture 509 scope image = fluid.Image(allColors[], 256, 1); 510 511 // Create the texture if it doesn't exist 512 if (_paletteTexture is _paletteTexture.init) 513 _paletteTexture = LoadTextureFromImage(image.toRaylib); 514 515 // Or, update existing palette image 516 else 517 UpdateTexture(_paletteTexture, image.data.ptr); 518 519 return _paletteTexture; 520 521 } 522 523 override void drawTriangleImpl(Vector2 a, Vector2 b, Vector2 c, Color color) nothrow @trusted { 524 DrawTriangle( 525 toRaylib(a), 526 toRaylib(b), 527 toRaylib(c), 528 color); 529 } 530 531 override void drawCircleImpl(Vector2 center, float radius, Color color) nothrow @trusted { 532 const centerRay = toRaylib(center); 533 const radiusRay = toRaylib(Vector2(radius, radius)); 534 DrawEllipse( 535 cast(int) centerRay.x, 536 cast(int) centerRay.y, 537 radiusRay.tupleof, 538 color); 539 } 540 541 override void drawCircleOutlineImpl(Vector2 center, float radius, float width, Color color) nothrow @trusted { 542 const centerRay = toRaylib(center); 543 const radiusRay = toRaylib(Vector2(radius, radius)); 544 const previousLineWidth = rlGetLineWidth(); 545 // Note: This isn't very accurate at greater widths 546 rlSetLineWidth(width); 547 DrawEllipseLines( 548 cast(int) centerRay.x, 549 cast(int) centerRay.y, 550 radiusRay.tupleof, 551 color); 552 rlDrawRenderBatchActive(); 553 rlSetLineWidth(previousLineWidth); 554 } 555 556 override void drawRectangleImpl(Rectangle rectangle, Color color) nothrow @trusted { 557 DrawRectangleRec( 558 toRaylib(rectangle), 559 color); 560 } 561 562 override void drawLineImpl(Vector2 start, Vector2 end, float width, Color color) nothrow @trusted { 563 DrawLineEx( 564 toRaylib(start), 565 toRaylib(end), 566 width, 567 color); 568 } 569 570 override void drawImageImpl(DrawableImage image, Rectangle destination, Color tint) nothrow { 571 drawImageImpl(image, destination, tint, false); 572 } 573 574 override void drawHintedImageImpl(DrawableImage image, Rectangle destination, Color tint) nothrow { 575 drawImageImpl(image, destination, tint, true); 576 } 577 578 private void drawImageImpl(DrawableImage image, Rectangle destination, Color tint, bool hinted) nothrow @trusted { 579 580 import std.math; 581 582 // Perform hinting if enabled 583 auto start = destination.start; 584 if (hinted) { 585 start = toDots(destination.start); 586 start.x = floor(start.x); 587 start.y = floor(start.y); 588 start = fromDots(start); 589 } 590 591 const destinationRay = Rectangle( 592 toRaylib(start).tupleof, 593 toRaylib(destination.size).tupleof 594 ); 595 596 const source = Rectangle(0, 0, image.width, image.height); 597 Shader shader; 598 599 // Enable shaders relevant to given format 600 switch (image.format) { 601 602 case fluid.Image.Format.alpha: 603 shader = alphaImageShader; 604 break; 605 606 case fluid.Image.Format.palettedAlpha: 607 shader = palettedAlphaImageShader(image.palette); 608 break; 609 610 default: break; 611 612 } 613 614 // Start shaders, if applicable 615 if (IsShaderReady(shader)) 616 BeginShaderMode(shader); 617 618 auto texture = textureFor(image); 619 620 DrawTexturePro(texture, source, destinationRay, Vector2(0, 0), 0, tint); 621 622 // End shaders 623 if (IsShaderReady(shader)) 624 EndShaderMode(); 625 626 } 627 628 override int load(fluid.Image image) nothrow @trusted { 629 630 const empty = image.width * image.height == 0; 631 const id = empty 632 ? 0 633 : cast(size_t) image.data.ptr; 634 635 // Image already loaded, reuse 636 if (auto indexPtr = id in _imageIndices) { 637 638 auto resource = _images[*indexPtr]; 639 640 // Image was updated, mirror the changes 641 if (image.revisionNumber > resource.image.revisionNumber) { 642 643 const sameFormat = resource.image.width == image.width 644 && resource.image.height == image.height 645 && resource.image.format == image.format; 646 647 resource.image = image; 648 649 if (empty) { } 650 651 // Update the texture in place if the format is the same 652 if (sameFormat) { 653 UpdateTexture(resource.texture, image.data.ptr); 654 } 655 656 // Reupload the image if not 657 else { 658 UnloadTexture(resource.texture); 659 resource.texture = LoadTextureFromImage(image.toRaylib); 660 } 661 662 } 663 664 _images.reload(*indexPtr, resource); 665 return *indexPtr; 666 } 667 668 // Empty image; do not upload 669 else if (empty) { 670 auto internalImage = RaylibImage(image, raylib.Texture.init); 671 return _imageIndices[id] = _images.load(internalImage); 672 } 673 674 // Load the image 675 else { 676 auto texture = LoadTextureFromImage(image.toRaylib); 677 auto internalImage = RaylibImage(image, texture); 678 679 return _imageIndices[id] = _images.load(internalImage); 680 } 681 682 } 683 684 override bool writeClipboard(string text) nothrow @trusted { 685 686 SetClipboardText(text.toStringz); 687 return true; 688 689 } 690 691 override char[] readClipboard(return scope char[] buffer, ref int offset) nothrow @trusted { 692 693 import std.algorithm : min; 694 695 // This is horrible but this API will change https://git.samerion.com/Samerion/Fluid/issues/276 696 const scope clipboard = GetClipboardText().fromStringz; 697 698 // Read the entire text, nothing remains to be read 699 if (offset >= clipboard.length) return null; 700 701 // Get remaining text 702 const text = clipboard[offset .. $]; 703 const length = min(text.length, buffer.length); 704 705 offset += length; 706 return buffer[0 .. length] = text[0 .. length]; 707 708 } 709 710 override fluid.Image loadImage(const ubyte[] image) @trusted { 711 712 assert(image.length < int.max, "Image is too big to load"); 713 714 const fileType = identifyImageType(image); 715 716 auto imageRay = LoadImageFromMemory(fileType.ptr, image.ptr, cast(int) image.length); 717 auto colors = LoadImageColors(imageRay); 718 scope (exit) UnloadImageColors(colors); 719 720 const size = imageRay.width * imageRay.height; 721 722 return fluid.Image(colors[0 .. size].dup, imageRay.width, imageRay.height); 723 724 } 725 726 } 727 728 /// A complete implementation of all systems Fluid needs to function, using Raylib as the base for communicating with 729 /// the operating system. Use `raylibStack` to construct. 730 /// 731 /// For a minimal installation that only includes systems provided by Raylib use `RaylibView`. 732 /// Note that `RaylibView` does not provide all the systems Fluid needs to function. See its documentation for more 733 /// information. 734 /// 735 /// On top of systems already provided by `RaylibView`, `RaylibStack` also includes `HoverIO`, `FocusIO`, `ActionIO`, 736 /// `PreferenceIO`, `TimeIO` and `FileIO`. You can access them through fields named `hoverIO`, `focusIO`, `actionIO`, 737 /// `preferenceIO`, `timeIO` and `fileIO` respectively. 738 class RaylibStack(RaylibViewVersion raylibVersion) : Node { 739 740 import fluid.hover_chain; 741 import fluid.focus_chain; 742 import fluid.input_map_chain; 743 import fluid.preference_chain; 744 import fluid.time_chain; 745 import fluid.file_chain; 746 import fluid.overlay_chain; 747 748 public { 749 750 /// I/O implementations provided by the stack. 751 FocusChain focusIO; 752 753 /// ditto 754 HoverChain hoverIO; 755 756 /// ditto 757 InputMapChain actionIO; 758 759 /// ditto 760 PreferenceChain preferenceIO; 761 762 /// ditto 763 TimeChain timeIO; 764 765 /// ditto 766 FileChain fileIO; 767 768 /// ditto 769 RaylibView!raylibVersion raylibIO; 770 771 /// ditto 772 OverlayChain overlayIO; 773 774 } 775 776 private { 777 778 HeadlessBackend _headlessBackend; 779 780 } 781 782 /// Initialize the stack. 783 /// Params: 784 /// next = Node to draw using the stack. 785 this(Node next = null) { 786 787 import fluid.structs : layout; 788 789 _headlessBackend = new HeadlessBackend; 790 chain( 791 preferenceIO = preferenceChain(), 792 timeIO = timeChain(), 793 actionIO = inputMapChain(), 794 focusIO = focusChain(), 795 hoverIO = hoverChain(), 796 fileIO = fileChain(), 797 raylibIO = raylibView( 798 chain( 799 overlayIO = overlayChain( 800 layout!(1, "fill") 801 ), 802 next, 803 ), 804 ), 805 ); 806 807 } 808 809 /// Returns: 810 /// The first node in the stack. 811 inout(NodeChain) root() inout { 812 return preferenceIO; 813 } 814 815 /// Returns: 816 /// Top node of the stack, before `next` 817 inout(NodeChain) top() inout { 818 return overlayIO; 819 } 820 821 /// Returns: 822 /// The node contained by the stack, child node of the `top`. 823 inout(Node) next() inout { 824 return top.next; 825 } 826 827 /// Change the node contained by the stack. 828 /// Params: 829 /// value = Value to set. 830 /// Returns: 831 /// Newly set node. 832 Node next(Node value) { 833 return top.next = value; 834 } 835 836 override void resizeImpl(Vector2 space) { 837 resizeChild(root, space); 838 minSize = root.minSize; 839 840 // If RaylibStack is used as the root, disable the legacy backend 841 if (tree.root == this) { 842 this.backend = _headlessBackend; 843 } 844 } 845 846 override void drawImpl(Rectangle, Rectangle inner) { 847 drawChild(root, inner); 848 } 849 850 } 851 852 /// Convert a `MouseIO.Button` to a `raylib.MouseButton`. 853 /// 854 /// Temporarily named `toRaylibEx` instead of `toRaylib` to avoid conflicts with legacy Raylib functionality. 855 /// 856 /// Note: 857 /// `raylib.MouseButton` does not have a dedicated invalid value so this function will instead 858 /// return `-1`. 859 /// Params: 860 /// button = A Fluid `MouseIO` button code. 861 /// Returns: 862 /// A corresponding `raylib.MouseButton` value, `-1` if there isn't one. 863 int toRaylibEx(MouseIO.Button button) { 864 865 with (MouseButton) 866 with (button) 867 final switch (button) { 868 869 case none: return -1; 870 case left: return MOUSE_BUTTON_LEFT; 871 case right: return MOUSE_BUTTON_RIGHT; 872 case middle: return MOUSE_BUTTON_MIDDLE; 873 case extra1: return MOUSE_BUTTON_SIDE; 874 case extra2: return MOUSE_BUTTON_EXTRA; 875 case forward: return MOUSE_BUTTON_FORWARD; 876 case back: return MOUSE_BUTTON_BACK; 877 878 } 879 880 } 881 882 /// Convert a Raylib keyboard key to a `KeyboardIO.Key` code. 883 /// 884 /// Params: 885 /// button = A Raylib `KeyboardKey` key code. 886 /// Returns: 887 /// A corresponding `KeyboardIO.Key` value. `KeyboardIO.Key.none` if the key is not recognized. 888 KeyboardIO.Key toFluid(KeyboardKey key) { 889 890 with (KeyboardIO.Key) 891 with (KeyboardKey) 892 final switch (key) { 893 894 case KEY_NULL: return none; 895 case KEY_APOSTROPHE: return apostrophe; 896 case KEY_COMMA: return comma; 897 case KEY_MINUS: return minus; 898 case KEY_PERIOD: return period; 899 case KEY_SLASH: return slash; 900 case KEY_ZERO: return digit0; 901 case KEY_ONE: return digit1; 902 case KEY_TWO: return digit2; 903 case KEY_THREE: return digit3; 904 case KEY_FOUR: return digit4; 905 case KEY_FIVE: return digit5; 906 case KEY_SIX: return digit6; 907 case KEY_SEVEN: return digit7; 908 case KEY_EIGHT: return digit8; 909 case KEY_NINE: return digit9; 910 case KEY_SEMICOLON: return semicolon; 911 case KEY_EQUAL: return equal; 912 case KEY_A: return a; 913 case KEY_B: return b; 914 case KEY_C: return c; 915 case KEY_D: return d; 916 case KEY_E: return e; 917 case KEY_F: return f; 918 case KEY_G: return g; 919 case KEY_H: return h; 920 case KEY_I: return i; 921 case KEY_J: return j; 922 case KEY_K: return k; 923 case KEY_L: return l; 924 case KEY_M: return m; 925 case KEY_N: return n; 926 case KEY_O: return o; 927 case KEY_P: return p; 928 case KEY_Q: return q; 929 case KEY_R: return r; 930 case KEY_S: return s; 931 case KEY_T: return t; 932 case KEY_U: return u; 933 case KEY_V: return v; 934 case KEY_W: return w; 935 case KEY_X: return x; 936 case KEY_Y: return y; 937 case KEY_Z: return z; 938 case KEY_LEFT_BRACKET: return leftBracket; 939 case KEY_BACKSLASH: return backslash; 940 case KEY_RIGHT_BRACKET: return rightBracket; 941 case KEY_GRAVE: return grave; 942 case KEY_SPACE: return space; 943 case KEY_ESCAPE: return escape; 944 case KEY_ENTER: return enter; 945 case KEY_TAB: return tab; 946 case KEY_BACKSPACE: return backspace; 947 case KEY_INSERT: return insert; 948 case KEY_DELETE: return delete_; 949 case KEY_RIGHT: return right; 950 case KEY_LEFT: return left; 951 case KEY_DOWN: return down; 952 case KEY_UP: return up; 953 case KEY_PAGE_UP: return pageUp; 954 case KEY_PAGE_DOWN: return pageDown; 955 case KEY_HOME: return home; 956 case KEY_END: return end; 957 case KEY_CAPS_LOCK: return capsLock; 958 case KEY_SCROLL_LOCK: return scrollLock; 959 case KEY_NUM_LOCK: return numLock; 960 case KEY_PRINT_SCREEN: return printScreen; 961 case KEY_PAUSE: return pause; 962 case KEY_F1: return f1; 963 case KEY_F2: return f2; 964 case KEY_F3: return f3; 965 case KEY_F4: return f4; 966 case KEY_F5: return f5; 967 case KEY_F6: return f6; 968 case KEY_F7: return f7; 969 case KEY_F8: return f8; 970 case KEY_F9: return f9; 971 case KEY_F10: return f10; 972 case KEY_F11: return f11; 973 case KEY_F12: return f12; 974 case KEY_LEFT_SHIFT: return leftShift; 975 case KEY_LEFT_CONTROL: return leftControl; 976 case KEY_LEFT_ALT: return leftAlt; 977 case KEY_LEFT_SUPER: return leftSuper; 978 case KEY_RIGHT_SHIFT: return rightShift; 979 case KEY_RIGHT_CONTROL: return rightControl; 980 case KEY_RIGHT_ALT: return rightAlt; 981 case KEY_RIGHT_SUPER: return rightSuper; 982 case KEY_KB_MENU: return contextMenu; 983 case KEY_KP_0: return keypad0; 984 case KEY_KP_1: return keypad1; 985 case KEY_KP_2: return keypad2; 986 case KEY_KP_3: return keypad3; 987 case KEY_KP_4: return keypad4; 988 case KEY_KP_5: return keypad5; 989 case KEY_KP_6: return keypad6; 990 case KEY_KP_7: return keypad7; 991 case KEY_KP_8: return keypad8; 992 case KEY_KP_9: return keypad9; 993 case KEY_KP_DECIMAL: return keypadDecimal; 994 case KEY_KP_DIVIDE: return keypadDivide; 995 case KEY_KP_MULTIPLY: return keypadMultiply; 996 case KEY_KP_SUBTRACT: return keypadSubtract; 997 case KEY_KP_ADD: return keypadSum; 998 case KEY_KP_ENTER: return keypadEnter; 999 case KEY_KP_EQUAL: return keypadEqual; 1000 case KEY_BACK: return androidBack; 1001 case KEY_MENU: return androidMenu; 1002 case KEY_VOLUME_UP: return volumeUp; 1003 case KEY_VOLUME_DOWN: return volumeDown; 1004 1005 } 1006 1007 } 1008 1009 enum ImageType : string { 1010 1011 none = "", 1012 png = ".png", 1013 bmp = ".bmp", 1014 tga = ".tga", 1015 jpg = ".jpg", 1016 gif = ".gif", 1017 qoi = ".qoi", 1018 psd = ".psd", 1019 dds = ".dds", 1020 hdr = ".hdr", 1021 pic = ".pic", 1022 ktx = ".ktx", 1023 astc = ".astc", 1024 pkm = ".pkm", 1025 pvr = ".pvr", 1026 1027 } 1028 1029 /// Identify image type by contents. 1030 /// Params: 1031 /// image = File data of the image to identify. 1032 /// Returns: 1033 /// String containing the image extension, or an empty string indicating unknown file. 1034 ImageType identifyImageType(const ubyte[] data) { 1035 1036 import std.algorithm : predSwitch; 1037 import std.conv : hexString; 1038 1039 1040 return data.predSwitch!"a.startsWith(cast(const ubyte[]) b)"( 1041 // Source: https://en.wikipedia.org/wiki/List_of_file_signatures 1042 hexString!"89 50 4E 47 0D 0A 1A 0A", ImageType.png, 1043 hexString!"42 4D", ImageType.bmp, 1044 hexString!"FF D8 FF E0 00 10 4A 46 49 46 00 01", ImageType.jpg, 1045 hexString!"FF D8 FF EE", ImageType.jpg, 1046 hexString!"FF D8 FF E1", ImageType.jpg, 1047 hexString!"FF D8 FF E0", ImageType.jpg, 1048 hexString!"00 00 00 0C 6A 50 20 20 0D 0A 87 0A", ImageType.jpg, 1049 hexString!"FF 4F FF 51", ImageType.jpg, 1050 hexString!"47 49 46 38 37 61", ImageType.gif, 1051 hexString!"47 49 46 38 39 61", ImageType.gif, 1052 hexString!"71 6f 69 66", ImageType.qoi, 1053 hexString!"38 42 50 53", ImageType.psd, 1054 hexString!"23 3F 52 41 44 49 41 4E 43 45 0A", ImageType.hdr, 1055 hexString!"6E 69 31 00", ImageType.hdr, 1056 hexString!"00", ImageType.pic, 1057 // Source: https://en.wikipedia.org/wiki/DirectDraw_Surface 1058 hexString!"44 44 53 20", ImageType.dds, 1059 // Source: https://paulbourke.net/dataformats/ktx/ 1060 hexString!"AB 4B 54 58 20 31 31 BB 0D 0A 1A 0A", ImageType.ktx, 1061 // Source: https://github.com/ARM-software/astc-encoder/blob/main/Docs/FileFormat.md 1062 hexString!"13 AB A1 5C", ImageType.astc, 1063 // Source: https://stackoverflow.com/questions/35881537/how-to-decode-this-image 1064 hexString!"50 4B 4D 20", ImageType.pkm, 1065 // Source: http://powervr-graphics.github.io/WebGL_SDK/WebGL_SDK/Documentation/Specifications/PVR%20File%20Format.Specification.pdf 1066 hexString!"03 52 56 50", ImageType.pvr, 1067 hexString!"50 56 52 03", ImageType.pvr, 1068 // Source: https://en.wikipedia.org/wiki/Truevision_TGA 1069 data.endsWith("TRUEVISION-XFILE.\0") 1070 ? ImageType.tga 1071 : ImageType.none, 1072 ); 1073 1074 }