1 module fluid.backend.simpledisplay; 2 3 version (Have_arsd_official_simpledisplay): 4 5 debug (Fluid_BuildMessages) { 6 pragma(msg, "Fluid: Building with arsd.simpledisplay support"); 7 } 8 9 import arsd.simpledisplay; 10 11 import std.algorithm; 12 import std.datetime.stopwatch; 13 14 import fluid.backend; 15 16 17 @safe: 18 19 20 private { 21 alias Rectangle = fluid.backend.Rectangle; 22 alias Color = fluid.backend.Color; 23 alias Image = fluid.backend.Image; 24 alias MouseButton = fluid.backend.MouseButton; 25 } 26 27 class SimpledisplayBackend : FluidBackend { 28 29 SimpleWindow window; 30 31 private enum InputState { 32 up, 33 pressed, 34 down, 35 released, 36 repeated, 37 } 38 39 private { 40 41 Vector2 _mousePosition; 42 int _dpi; 43 float _scale = 1; 44 bool _hasJustResized; 45 StopWatch _stopWatch; 46 float _deltaTime; 47 Rectangle _scissors; 48 bool _scissorsEnabled; 49 FluidMouseCursor _cursor; 50 51 TextureReaper _reaper; 52 53 /// Recent input events for each keyboard and mouse. 54 InputState[KeyboardKey.max+1] _keyboardState; 55 InputState[MouseButton.max+1] _mouseState; 56 // gamepads? 57 58 /// Characters typed by the user, awaiting consumption. 59 const(dchar)[] _characterQueue; 60 61 // Missing from simpledisplay at the time of writing 62 extern(C) void function(GLint x, GLint y, GLsizei width, GLsizei height) glScissor; 63 64 } 65 66 // TODO non-openGL backend, maybe... 67 68 /// Initialize the backend using the given window. 69 /// 70 /// Make sure to call `SimpledisplayBackend.poll()` *after* the Fluid `draw` call, and only do it once per frame, 71 /// other Fluid might not be able to keep itself up to date with latest events. 72 /// 73 /// Please note Fluid will register its own event handlers, so if you 74 /// intend to use them, you should make sure to call whatever value was set previously. 75 /// 76 /// --- 77 /// auto oldMouseHandler = window.handleMouseEvent; 78 /// window.handleMouseEvent = (MouseEvent event) { 79 /// oldMouseHandler(event); 80 /// // ... do your stuff ... 81 /// }; 82 /// --- 83 /// 84 /// Gamepad input is not supported for simpledisplay. 85 this(SimpleWindow window) { 86 87 this.window = window; 88 89 () @trusted { 90 this.glScissor = cast(typeof(glScissor)) glbindGetProcAddress("glScissor"); 91 }(); 92 93 updateDPI(); 94 _stopWatch.start(); 95 96 auto oldMouseHandler = window.handleMouseEvent; 97 auto oldKeyHandler = window.handleKeyEvent; 98 auto oldCharHandler = window.handleCharEvent; 99 auto oldWindowResized = window.windowResized; 100 auto oldOnDpiChanged = window.onDpiChanged; 101 102 // Register a mouse handler 103 this.window.handleMouseEvent = (MouseEvent event) { 104 105 if (oldMouseHandler) oldMouseHandler(event); 106 107 final switch (event.type) { 108 109 // Update mouse position 110 case event.type.motion: 111 _mousePosition = Vector2(event.x, event.y); 112 return; 113 114 // Update button state 115 case event.type.buttonPressed: 116 _mouseState[event.button.toFluid] = InputState.pressed; 117 return; 118 119 case event.type.buttonReleased: 120 _mouseState[event.button.toFluid] = InputState.released; 121 return; 122 123 } 124 125 }; 126 127 // Register a keyboard handler 128 this.window.handleKeyEvent = (KeyEvent event) { 129 130 if (oldKeyHandler) oldKeyHandler(event); 131 132 const key = event.key.toFluid; 133 134 // Released 135 if (!event.pressed) 136 _keyboardState[key] = InputState.released; 137 138 // Repeat 139 else if (isDown(key)) 140 _keyboardState[key] = InputState.repeated; 141 142 // Pressed 143 else 144 _keyboardState[key] = InputState.pressed; 145 146 }; 147 148 // Register character handler 149 this.window.handleCharEvent = (dchar character) { 150 151 import std.uni; 152 153 if (oldCharHandler) oldCharHandler(character); 154 155 // Ignore control characters 156 if (character.isControl) return; 157 158 // Send new characters 159 _characterQueue ~= character; 160 161 }; 162 163 // Register a resize handler 164 this.window.windowResized = (int width, int height) { 165 166 if (oldWindowResized) oldWindowResized(width, height); 167 168 // Update window size 169 _hasJustResized = true; 170 glViewport(0, 0, width, height); 171 172 }; 173 174 this.window.onDpiChanged = () { 175 176 if (oldOnDpiChanged) oldOnDpiChanged(); 177 178 // Update window size 179 _hasJustResized = true; 180 updateDPI(); 181 182 }; 183 184 } 185 186 bool isPressed(MouseButton button) const { 187 188 return _mouseState[button] == InputState.pressed; 189 190 } 191 192 bool isReleased(MouseButton button) const { 193 194 return _mouseState[button] == InputState.released; 195 196 } 197 198 bool isDown(MouseButton button) const { 199 200 return _mouseState[button].among(InputState.pressed, InputState.down) != 0; 201 202 } 203 204 bool isUp(MouseButton button) const { 205 206 return _mouseState[button].among(InputState.released, InputState.up) != 0; 207 208 } 209 210 bool isPressed(KeyboardKey key) const { 211 212 return _keyboardState[key] == InputState.pressed; 213 214 } 215 216 bool isReleased(KeyboardKey key) const { 217 218 return _keyboardState[key] == InputState.released; 219 220 } 221 222 bool isDown(KeyboardKey key) const { 223 224 return _keyboardState[key].among(InputState.pressed, InputState.repeated, InputState.down) != 0; 225 226 } 227 228 bool isUp(KeyboardKey key) const { 229 230 return _keyboardState[key].among(InputState.released, InputState.up) != 0; 231 232 } 233 234 bool isRepeated(KeyboardKey key) const { 235 236 return _keyboardState[key] == InputState.repeated; 237 238 } 239 240 241 dchar inputCharacter() { 242 243 // No characters in queue 244 if (_characterQueue.length == 0) 245 return '\0'; 246 247 // Pop the first character 248 auto result = _characterQueue[0]; 249 _characterQueue = _characterQueue[1..$]; 250 return result; 251 252 } 253 254 int isPressed(GamepadButton button) const 255 => 0; 256 int isReleased(GamepadButton button) const 257 => 0; 258 int isDown(GamepadButton button) const 259 => 0; 260 int isUp(GamepadButton button) const 261 => 1; 262 int isRepeated(GamepadButton button) const 263 => 0; 264 265 private void updateDPI() @trusted { 266 267 _dpi = either(window.actualDpi, 96); 268 269 } 270 271 /// Update event state. To be called *after* drawing. 272 void poll() { 273 274 // Calculate delta time 275 _deltaTime = _stopWatch.peek.total!"msecs" / 1000f; 276 _stopWatch.reset(); 277 278 // Reset frame state 279 _hasJustResized = false; 280 _characterQueue = null; 281 282 foreach (ref state; _keyboardState) { 283 if (state == state.pressed) state = state.down; 284 if (state == state.repeated) state = state.down; 285 if (state == state.released) state = state.up; 286 } 287 288 foreach (ref state; _mouseState) { 289 if (state == state.pressed) state = state.down; 290 if (state == state.released) state = state.up; 291 } 292 293 } 294 295 Vector2 mousePosition(Vector2 position) @trusted { 296 297 auto positionRay = toSdpyCoords(position); 298 window.warpMouse(cast(int) positionRay.x, cast(int) positionRay.y); 299 return _mousePosition = position; 300 301 } 302 303 Vector2 mousePosition() const @trusted { 304 305 return toFluidCoords(_mousePosition); 306 307 } 308 309 float deltaTime() const @trusted { 310 311 return _deltaTime; 312 313 } 314 315 bool hasJustResized() const @trusted { 316 317 return _hasJustResized; 318 319 } 320 321 Vector2 windowSize(Vector2 size) @trusted { 322 323 auto sizeRay = toSdpyCoords(size); 324 window.resize(cast(int) sizeRay.x, cast(int) sizeRay.y); 325 return size; 326 327 } 328 329 Vector2 windowSize() const @trusted { 330 331 return toFluidCoords(Vector2(window.width, window.height)); 332 333 } 334 335 /// Convert window coordinates to OpenGL coordinates; done *after* toSdpyCoords. 336 Vector2 toGL(Vector2 coords) { 337 338 return Vector2( 339 coords.x / window.width * 2 - 1, 340 1 - coords.y / window.height * 2 341 ); 342 343 } 344 345 /// Create a vertex at given screenspace position 346 void vertex(Vector2 coords) @trusted { 347 348 glVertex2f(toGL(coords).tupleof); 349 350 } 351 352 float scale() const { 353 354 return _scale; 355 356 } 357 358 float scale(float value) { 359 360 return _scale = value; 361 362 } 363 364 Vector2 dpi() const @trusted { 365 366 return Vector2(_dpi, _dpi) * scale; 367 368 } 369 370 Vector2 toSdpyCoords(Vector2 position) const @trusted { 371 372 return Vector2(position.x * hidpiScale.x, position.y * hidpiScale.y); 373 374 } 375 376 Rectangle toSdpyCoords(Rectangle rec) const @trusted { 377 378 return Rectangle( 379 rec.x * hidpiScale.x, 380 rec.y * hidpiScale.y, 381 rec.width * hidpiScale.x, 382 rec.height * hidpiScale.y, 383 ); 384 385 } 386 387 Vector2 toFluidCoords(Vector2 position) const @trusted { 388 389 return Vector2(position.x / hidpiScale.x, position.y / hidpiScale.y); 390 391 } 392 393 Vector2 toFluidCoords(float x, float y) const @trusted { 394 395 return Vector2(x / hidpiScale.x, y / hidpiScale.y); 396 397 } 398 399 Rectangle toFluidCoords(Rectangle rec) const @trusted { 400 401 return Rectangle( 402 rec.x / hidpiScale.x, 403 rec.y / hidpiScale.y, 404 rec.width / hidpiScale.x, 405 rec.height / hidpiScale.y, 406 ); 407 408 } 409 410 Rectangle area(Rectangle rect) @trusted { 411 412 auto rectRay = toSdpyCoords(rect); 413 414 glEnable(GL_SCISSOR_TEST); 415 glScissor( 416 cast(int) rectRay.x, 417 cast(int) (window.height - rectRay.y - rectRay.height), 418 cast(int) rectRay.width, 419 cast(int) rectRay.height, 420 ); 421 _scissorsEnabled = true; 422 423 return _scissors = rect; 424 425 } 426 427 Rectangle area() const { 428 429 if (_scissorsEnabled) 430 return _scissors; 431 else 432 return Rectangle(0, 0, windowSize.tupleof); 433 434 } 435 436 void restoreArea() @trusted { 437 438 glDisable(GL_SCISSOR_TEST); 439 _scissorsEnabled = false; 440 441 } 442 443 FluidMouseCursor mouseCursor(FluidMouseCursor cursor) @trusted { 444 445 // Hide the cursor 446 if (cursor.system == cursor.system.none) { 447 window.hideCursor(); 448 } 449 450 // Show the cursor 451 else { 452 window.showCursor(); 453 window.cursor = cursor.toSimpleDisplay; 454 } 455 456 return _cursor = cursor; 457 458 } 459 460 FluidMouseCursor mouseCursor() const { 461 462 return _cursor; 463 464 } 465 466 TextureReaper* reaper() return scope { 467 468 return &_reaper; 469 470 } 471 472 Texture loadTexture(Image image) @system { 473 474 Texture result; 475 result.width = image.width; 476 result.height = image.height; 477 478 // Create an OpenGL texture 479 glGenTextures(1, &result.id); 480 glBindTexture(GL_TEXTURE_2D, result.id); 481 482 // Prepare the tombstone 483 result.tombstone = reaper.makeTombstone(this, result.id); 484 485 // No filtering 486 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 487 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 488 489 // Repeat on 490 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 491 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 492 493 // Upload the data 494 glTexImage2D( 495 496 // 2D texture, no mipmaps, four channels 497 GL_TEXTURE_2D, 0, GL_RGBA, 498 499 // Size 500 image.width, image.height, 501 502 // No border 503 0, 504 505 // Formatted as R8B8G8A8 506 GL_RGBA, GL_UNSIGNED_BYTE, image.pixels.ptr, 507 508 ); 509 510 // Unbind the texture 511 glBindTexture(GL_TEXTURE_2D, 0); 512 513 return result; 514 515 } 516 517 Image loadImage(string filename) @system { 518 519 version (Have_arsd_official_image_files) { 520 521 import arsd.image; 522 523 // Load the image 524 auto image = loadImageFromFile(filename).getAsTrueColorImage; 525 526 // Convert to a Fluid image 527 Image result; 528 result.pixels = cast(Color[]) image.imageData.bytes; 529 result.width = image.width; 530 result.height = image.height; 531 return result; 532 533 } 534 535 else assert(false, "arsd-official:image_files is required to load images from files"); 536 537 } 538 539 Texture loadTexture(string filename) @system { 540 541 return loadTexture(loadImage(filename)); 542 543 } 544 545 /// Destroy a texture 546 void unloadTexture(uint id) @system { 547 548 if (id == 0) return; 549 550 glDeleteTextures(1, &id); 551 552 } 553 554 private void openglDraw() @trusted { 555 556 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 557 glEnable(GL_BLEND); 558 glLoadIdentity(); 559 560 // This must be present, otherwise the AMD Linux driver will hang when the window is resized. I don't have the 561 // slightest clue why. 562 if (auto error = glGetError()) { 563 import std.stdio; 564 debug writeln(error); 565 } 566 567 } 568 569 void drawLine(Vector2 start, Vector2 end, Color color) @trusted { 570 571 openglDraw(); 572 glBegin(GL_LINES); 573 574 glColor4ub(color.tupleof); 575 vertex(toSdpyCoords(start)); 576 vertex(toSdpyCoords(end)); 577 578 glEnd(); 579 580 } 581 582 void drawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color) @trusted { 583 584 openglDraw(); 585 glBegin(GL_TRIANGLES); 586 glColor4ub(color.tupleof); 587 vertex(toSdpyCoords(a)); 588 vertex(toSdpyCoords(b)); 589 vertex(toSdpyCoords(c)); 590 glEnd(); 591 592 } 593 594 void drawRectangle(Rectangle rectangle, Color color) @trusted { 595 596 drawRectangleImpl(toSdpyCoords(rectangle), color); 597 598 } 599 600 private void drawRectangleImpl(Rectangle rectangle, Color color) @trusted { 601 602 import fluid.utils; 603 604 openglDraw(); 605 glBegin(GL_TRIANGLES); 606 glColor4ub(color.tupleof); 607 608 // d--c 609 // | /| 610 // |/ | 611 // a--b 612 const a = start(rectangle) + Vector2(0, rectangle.height); 613 const b = end(rectangle); 614 const d = start(rectangle); 615 const c = start(rectangle) + Vector2(rectangle.width, 0); 616 617 // First triangle 618 glTexCoord2f(0, 0); 619 vertex(d); 620 glTexCoord2f(0, 1); 621 vertex(a); 622 glTexCoord2f(1, 0); 623 vertex(c); 624 625 // Second triangle 626 glTexCoord2f(1, 0); 627 vertex(c); 628 glTexCoord2f(0, 1); 629 vertex(a); 630 glTexCoord2f(1, 1); 631 vertex(b); 632 633 glEnd(); 634 635 } 636 637 void drawTexture(Texture texture, Rectangle rectangle, Color tint, string altText) @trusted 638 in (false) 639 do { 640 641 // TODO filtering? 642 drawTextureImpl(texture, rectangle, tint, altText, true); 643 644 } 645 646 void drawTextureAlign(Texture texture, Rectangle rectangle, Color tint, string altText) @trusted 647 in (false) 648 do { 649 650 drawTextureImpl(texture, rectangle, tint, altText, true); 651 652 } 653 654 @trusted 655 private void drawTextureImpl(Texture texture, Rectangle rectangle, Color tint, string altText, bool alignPixels) { 656 657 import std.math; 658 659 rectangle = toSdpyCoords(rectangle); 660 661 if (alignPixels) { 662 rectangle.x = floor(rectangle.x); 663 rectangle.y = floor(rectangle.y); 664 } 665 666 glEnable(GL_TEXTURE_2D); 667 glBindTexture(GL_TEXTURE_2D, texture.id); 668 drawRectangleImpl(rectangle, tint); 669 glBindTexture(GL_TEXTURE_2D, 0); 670 671 } 672 673 } 674 675 MouseButton toFluid(arsd.simpledisplay.MouseButton button) { 676 677 switch (button) { 678 679 default: 680 case button.none: return MouseButton.none; 681 case button.left: return MouseButton.left; 682 case button.middle: return MouseButton.middle; 683 case button.right: return MouseButton.right; 684 case button.wheelUp: return MouseButton.scrollUp; 685 case button.wheelDown: return MouseButton.scrollDown; 686 case button.backButton: return MouseButton.back; 687 case button.forwardButton: return MouseButton.forward; 688 689 } 690 691 } 692 693 KeyboardKey toFluid(arsd.simpledisplay.Key key) { 694 695 switch (key) { 696 697 default: return KeyboardKey.none; 698 case key.Escape: return KeyboardKey.escape; 699 case key.Backspace: return KeyboardKey.backspace; 700 case key.F1: return KeyboardKey.f1; 701 case key.F2: return KeyboardKey.f2; 702 case key.F3: return KeyboardKey.f3; 703 case key.F4: return KeyboardKey.f4; 704 case key.F5: return KeyboardKey.f5; 705 case key.F6: return KeyboardKey.f6; 706 case key.F7: return KeyboardKey.f7; 707 case key.F8: return KeyboardKey.f8; 708 case key.F9: return KeyboardKey.f9; 709 case key.F10: return KeyboardKey.f10; 710 case key.F11: return KeyboardKey.f11; 711 case key.F12: return KeyboardKey.f12; 712 case key.PrintScreen: return KeyboardKey.printScreen; 713 case key.ScrollLock: return KeyboardKey.scrollLock; 714 case key.Pause: return KeyboardKey.pause; 715 case key.Grave: return KeyboardKey.grave; 716 case key.N0: return KeyboardKey.digit0; 717 case key.N1: return KeyboardKey.digit1; 718 case key.N2: return KeyboardKey.digit2; 719 case key.N3: return KeyboardKey.digit3; 720 case key.N4: return KeyboardKey.digit4; 721 case key.N5: return KeyboardKey.digit5; 722 case key.N6: return KeyboardKey.digit6; 723 case key.N7: return KeyboardKey.digit7; 724 case key.N8: return KeyboardKey.digit8; 725 case key.N9: return KeyboardKey.digit9; 726 case key.Dash: return KeyboardKey.dash; 727 case key.Equals: return KeyboardKey.equal; 728 case key.Backslash: return KeyboardKey.backslash; 729 case key.Insert: return KeyboardKey.insert; 730 case key.Home: return KeyboardKey.home; 731 case key.PageUp: return KeyboardKey.pageUp; 732 case key.PageDown: return KeyboardKey.pageDown; 733 case key.Delete: return KeyboardKey.del; 734 case key.End: return KeyboardKey.end; 735 case key.Up: return KeyboardKey.up; 736 case key.Down: return KeyboardKey.down; 737 case key.Left: return KeyboardKey.left; 738 case key.Right: return KeyboardKey.right; 739 case key.Tab: return KeyboardKey.tab; 740 case key.Q: return KeyboardKey.q; 741 case key.W: return KeyboardKey.w; 742 case key.E: return KeyboardKey.e; 743 case key.R: return KeyboardKey.r; 744 case key.T: return KeyboardKey.t; 745 case key.Y: return KeyboardKey.y; 746 case key.U: return KeyboardKey.u; 747 case key.I: return KeyboardKey.i; 748 case key.O: return KeyboardKey.o; 749 case key.P: return KeyboardKey.p; 750 case key.LeftBracket: return KeyboardKey.leftBracket; 751 case key.RightBracket: return KeyboardKey.rightBracket; 752 case key.CapsLock: return KeyboardKey.capsLock; 753 case key.A: return KeyboardKey.a; 754 case key.S: return KeyboardKey.s; 755 case key.D: return KeyboardKey.d; 756 case key.F: return KeyboardKey.f; 757 case key.G: return KeyboardKey.g; 758 case key.H: return KeyboardKey.h; 759 case key.J: return KeyboardKey.j; 760 case key.K: return KeyboardKey.k; 761 case key.L: return KeyboardKey.l; 762 case key.Semicolon: return KeyboardKey.semicolon; 763 case key.Apostrophe: return KeyboardKey.apostrophe; 764 case key.Enter: return KeyboardKey.enter; 765 case key.Shift: return KeyboardKey.leftShift; 766 case key.Z: return KeyboardKey.z; 767 case key.X: return KeyboardKey.x; 768 case key.C: return KeyboardKey.c; 769 case key.V: return KeyboardKey.v; 770 case key.B: return KeyboardKey.b; 771 case key.N: return KeyboardKey.n; 772 case key.M: return KeyboardKey.m; 773 case key.Comma: return KeyboardKey.comma; 774 case key.Period: return KeyboardKey.period; 775 case key.Slash: return KeyboardKey.slash; 776 case key.Shift_r: return KeyboardKey.rightShift; 777 case key.Ctrl: return KeyboardKey.leftControl; 778 case key.Windows: return KeyboardKey.leftSuper; 779 case key.Alt: return KeyboardKey.leftAlt; 780 case key.Space: return KeyboardKey.space; 781 case key.Alt_r: return KeyboardKey.rightAlt; 782 case key.Windows_r: return KeyboardKey.rightSuper; 783 case key.Menu: return KeyboardKey.contextMenu; 784 case key.Ctrl_r: return KeyboardKey.rightControl; 785 case key.NumLock: return KeyboardKey.numLock; 786 case key.Divide: return KeyboardKey.keypadDivide; 787 case key.Multiply: return KeyboardKey.keypadMultiply; 788 case key.Minus: return KeyboardKey.keypadSubtract; 789 case key.Plus: return KeyboardKey.keypadSum; 790 case key.PadEnter: return KeyboardKey.keypadEnter; 791 case key.Pad0: return KeyboardKey.keypad0; 792 case key.Pad1: return KeyboardKey.keypad1; 793 case key.Pad2: return KeyboardKey.keypad2; 794 case key.Pad3: return KeyboardKey.keypad3; 795 case key.Pad4: return KeyboardKey.keypad4; 796 case key.Pad5: return KeyboardKey.keypad5; 797 case key.Pad6: return KeyboardKey.keypad6; 798 case key.Pad7: return KeyboardKey.keypad7; 799 case key.Pad8: return KeyboardKey.keypad8; 800 case key.Pad9: return KeyboardKey.keypad9; 801 case key.PadDot: return KeyboardKey.keypadDecimal; 802 803 } 804 805 } 806 807 MouseCursor toSimpleDisplay(FluidMouseCursor cursor) @trusted { 808 809 switch (cursor.system) { 810 811 default: 812 case cursor.system.systemDefault: 813 case cursor.system.none: 814 return GenericCursor.Default; 815 816 case cursor.system.pointer: return GenericCursor.Hand; 817 case cursor.system.crosshair: return GenericCursor.Cross; 818 case cursor.system.text: return GenericCursor.Text; 819 case cursor.system.allScroll: return GenericCursor.Move; 820 case cursor.system.resizeEW: return GenericCursor.SizeWe; 821 case cursor.system.resizeNS: return GenericCursor.SizeNs; 822 case cursor.system.resizeNESW: return GenericCursor.SizeNesw; 823 case cursor.system.resizeNWSE: return GenericCursor.SizeNwse; 824 case cursor.system.notAllowed: return GenericCursor.NotAllowed; 825 826 } 827 828 }