1 /// wayland window impl 2 module gfx.window.wayland; 3 4 version(linux): 5 6 import gfx.core.log : LogTag; 7 import gfx.graal : Backend, Instance; 8 import gfx.graal.presentation; 9 import gfx.vulkan.wsi; 10 import gfx.window; 11 import gfx.window.keys; 12 import gfx.window.xkeyboard; 13 import gfx.window.wayland.xdg_shell; 14 15 import wayland.client; 16 import wayland.cursor; 17 import wayland.native.util; 18 import wayland.util; 19 20 enum gfxWlLogMask = 0x0800_0000; 21 package immutable gfxWlLog = LogTag("GFX-WL", gfxWlLogMask); 22 23 class WaylandDisplay : Display 24 { 25 import gfx.core.rc : atomicRcCode, Rc; 26 mixin(atomicRcCode); 27 28 private WlDisplay display; 29 private WlCompositor compositor; 30 private WlShm shm; 31 private WlSeat seat; 32 private WlPointer pointer; 33 private WlKeyboard kbd; 34 private XKeyboard xkb; 35 36 private WlCursorTheme cursorTheme; 37 private WlCursor[string] cursors; 38 private WlSurface cursorSurf; 39 40 private WlShell wlShell; 41 private XdgWmBase xdgShell; 42 43 private Rc!Instance _instance; 44 45 private WaylandWindowBase[] wldWindows; 46 private WaylandWindowBase pointedWindow; 47 private WaylandWindowBase kbdFocus; 48 private Window[] _windows; 49 50 this(DisplayCreateInfo createInfo) 51 { 52 import std.exception : enforce; 53 54 gfxWlLog.info("Opening Wayland display"); 55 56 display = enforce(WlDisplay.connect(), "Wayland not available"); 57 58 { 59 // Only vulkan is supported. 60 import gfx.vulkan : createVulkanInstance, debugReportInstanceExtensions, 61 lunarGValidationLayers, VulkanCreateInfo, vulkanInit; 62 import gfx.vulkan.wsi : waylandSurfaceInstanceExtensions; 63 64 foreach (b; createInfo.backendCreateOrder) { 65 if (b != Backend.vulkan) { 66 gfxWlLog.warningf("Backend %s is not supported with Wayland."); 67 continue; 68 } 69 vulkanInit(); 70 VulkanCreateInfo vci; 71 vci.mandatoryExtensions = waylandSurfaceInstanceExtensions; 72 vci.optionalExtensions = createInfo.debugCallbackEnabled ? 73 debugReportInstanceExtensions : []; 74 vci.optionalLayers = createInfo.validationEnabled ? 75 lunarGValidationLayers : []; 76 _instance = createVulkanInstance(vci); 77 break; 78 } 79 } 80 81 auto reg = display.getRegistry(); 82 reg.onGlobal = (WlRegistry reg, uint name, string iface, uint ver) { 83 import std.algorithm : min; 84 if(iface == WlCompositor.iface.name) 85 { 86 compositor = cast(WlCompositor)reg.bind( 87 name, WlCompositor.iface, min(ver, 4) 88 ); 89 } 90 else if (iface == WlShm.iface.name) 91 { 92 shm = cast(WlShm)reg.bind( 93 name, WlShm.iface, min(ver, 1) 94 ); 95 cursorTheme = enforce( 96 WlCursorTheme.load(null, 24, shm), 97 "Unable to load default cursor theme" 98 ); 99 const cursorIds = [ 100 "default", "n-resize", "ne-resize", "e-resize", "se-resize", 101 "s-resize", "sw-resize", "w-resize", "nw-resize" 102 ]; 103 foreach (cid; cursorIds) { 104 cursors[cid] = enforce( 105 cursorTheme.cursor(cid), "Unable to load "~cid~" from the default cursor theme" 106 ); 107 } 108 } 109 else if(iface == WlSeat.iface.name) 110 { 111 seat = cast(WlSeat)reg.bind( 112 name, WlSeat.iface, min(ver, 2) 113 ); 114 seat.onCapabilities = &seatCapChanged; 115 } 116 else if(iface == WlShell.iface.name) 117 { 118 wlShell = cast(WlShell)reg.bind( 119 name, WlShell.iface, min(ver, 1) 120 ); 121 } 122 else if (iface == XdgWmBase.iface.name) 123 { 124 xdgShell = cast(XdgWmBase)reg.bind( 125 name, XdgWmBase.iface, min(ver, 1) 126 ); 127 xdgShell.onPing = (XdgWmBase shell, uint serial) { 128 shell.pong(serial); 129 }; 130 } 131 }; 132 display.roundtrip(); 133 reg.destroy(); 134 cursorSurf = compositor.createSurface(); 135 } 136 137 override @property Instance instance() 138 { 139 return _instance; 140 } 141 142 override @property Window[] windows() 143 { 144 return _windows; 145 } 146 147 override Window createWindow(in string title) 148 { 149 if (xdgShell) { 150 auto w = new XdgWaylandWindow(this, _instance, xdgShell, title); 151 wldWindows ~= w; 152 _windows ~= w; 153 return w; 154 } 155 else if (wlShell) { 156 auto w = new WaylandWindow(this, _instance, wlShell, title); 157 wldWindows ~= w; 158 _windows ~= w; 159 return w; 160 } 161 throw new Exception("No shell available. Can't create any Wayland window."); 162 } 163 164 override void pollAndDispatch() 165 { 166 while (display.prepareRead() != 0) { 167 display.dispatchPending(); 168 } 169 display.flush(); 170 display.readEvents(); 171 display.dispatchPending(); 172 } 173 174 package void unrefWindow(WaylandWindowBase window) 175 { 176 import std.algorithm : remove; 177 wldWindows = wldWindows.remove!(w => w is window); 178 _windows = _windows.remove!(w => w is window); 179 if (window is pointedWindow) pointedWindow = null; 180 if (window is kbdFocus) kbdFocus = null; 181 } 182 183 private void seatCapChanged (WlSeat seat, WlSeat.Capability cap) 184 { 185 if ((cap & WlSeat.Capability.pointer) && !pointer) 186 { 187 pointer = seat.getPointer(); 188 pointer.onEnter = &pointerEnter; 189 pointer.onButton = &pointerButton; 190 pointer.onMotion = &pointerMotion; 191 pointer.onLeave = &pointerLeave; 192 } 193 else if (!(cap & WlSeat.Capability.pointer) && pointer) 194 { 195 pointer.destroy(); 196 pointer = null; 197 } 198 199 if ((cap & WlSeat.Capability.keyboard) && !kbd) 200 { 201 kbd = seat.getKeyboard(); 202 kbd.onKeymap = &kbdKeymap; 203 kbd.onEnter = &kbdEnter; 204 kbd.onLeave = &kbdLeave; 205 kbd.onKey = &kbdKey; 206 kbd.onModifiers = &kbdModifiers; 207 } 208 else if (!(cap & WlSeat.Capability.keyboard) && kbd) 209 { 210 kbd.destroy(); 211 kbd = null; 212 } 213 } 214 215 private void pointerEnter(WlPointer pointer, uint serial, WlSurface surface, 216 WlFixed surfaceX, WlFixed surfaceY) 217 { 218 foreach (w; wldWindows) { 219 if (w.wlSurface is surface) { 220 pointedWindow = w; 221 w.pointerEnter(surfaceX, surfaceY, serial); 222 break; 223 } 224 } 225 } 226 227 private void pointerButton(WlPointer, uint serial, uint time, uint button, 228 WlPointer.ButtonState state) 229 { 230 if (pointedWindow) { 231 pointedWindow.pointerButton(state, serial, xkb ? xkb.mods : KeyMods.init); 232 } 233 } 234 235 private void pointerMotion(WlPointer, uint serial, WlFixed surfaceX, WlFixed surfaceY) 236 { 237 if (pointedWindow) { 238 pointedWindow.pointerMotion(surfaceX, surfaceY, serial, xkb ? xkb.mods : KeyMods.init); 239 } 240 } 241 242 private void pointerLeave(WlPointer pointer, uint serial, WlSurface surface) 243 { 244 if (pointedWindow && pointedWindow.wlSurface is surface) { 245 pointedWindow.pointerLeave(serial); 246 pointedWindow = null; 247 } 248 else { 249 foreach (w; wldWindows) { 250 if (w.wlSurface is surface) { 251 w.pointerLeave(serial); 252 break; 253 } 254 } 255 } 256 } 257 258 private void kbdKeymap(WlKeyboard, WlKeyboard.KeymapFormat format, int fd, uint size) 259 { 260 import std.exception : enforce; 261 262 enforce(format == WlKeyboard.KeymapFormat.xkbV1, "Unsupported wayland keymap format"); 263 264 if (xkb) xkb.dispose(); 265 xkb = new WaylandKeyboard(fd, size); 266 } 267 268 private void kbdEnter(WlKeyboard, uint serial, WlSurface surf, wl_array* keys) 269 { 270 foreach (w; wldWindows) { 271 if (w.wlSurface is surf) { 272 kbdFocus = w; 273 break; 274 } 275 } 276 } 277 278 private void kbdLeave(WlKeyboard, uint serial, WlSurface surf) 279 { 280 if (kbdFocus && kbdFocus.wlSurface !is surf) { 281 gfxWlLog.warningf("Leaving window that was not entered"); 282 } 283 kbdFocus = null; 284 } 285 286 private void kbdModifiers(WlKeyboard, uint serial, uint modsDepressed, 287 uint modsLatched, uint modsLocked, uint group) 288 { 289 if (xkb) { 290 xkb.updateState(modsDepressed, modsLatched, modsLocked, 0, 0, group); 291 } 292 } 293 294 private void kbdKey(WlKeyboard, uint serial, uint time, uint key, 295 WlKeyboard.KeyState state) 296 { 297 if (xkb) { 298 WaylandWindowBase w = kbdFocus; 299 if (!w && wldWindows.length) w = wldWindows[0]; 300 301 switch (state) { 302 case WlKeyboard.KeyState.pressed: 303 xkb.processKeyDown(key+8, kbdFocus ? kbdFocus.onKeyOnHandler : null); 304 break; 305 case WlKeyboard.KeyState.released: 306 xkb.processKeyUp(key+8, kbdFocus ? kbdFocus.onKeyOffHandler : null); 307 break; 308 default: 309 break; 310 } 311 } 312 } 313 314 private void setCursor(string cursorId, uint serial) 315 { 316 auto cursor = cursors[cursorId]; 317 if (cursor.images.length > 1) { 318 gfxWlLog.warning("animated cursors are not supported, only showing first frame"); 319 } 320 auto img = cursor.images[0]; 321 auto buf = img.buffer; 322 if (!buf) return; 323 pointer.setCursor(serial, cursorSurf, img.hotspotX, img.hotspotY); 324 cursorSurf.attach(buf, 0, 0); 325 cursorSurf.damage(0, 0, img.width, img.height); 326 cursorSurf.commit(); 327 } 328 329 override void dispose() 330 { 331 if (wldWindows.length) { 332 auto ws = wldWindows.dup; 333 foreach (w; ws) w.close(); 334 } 335 assert(!wldWindows.length); 336 assert(!_windows.length); 337 338 cursors = null; 339 if (cursorTheme) { 340 cursorTheme.destroy(); 341 cursorTheme = null; 342 } 343 if (cursorSurf) { 344 cursorSurf.destroy(); 345 cursorSurf = null; 346 } 347 if (xkb) { 348 xkb.dispose(); 349 xkb = null; 350 } 351 if (kbd) { 352 kbd.destroy(); 353 kbd = null; 354 } 355 if (pointer) { 356 pointer.destroy(); 357 pointer = null; 358 } 359 if (seat) { 360 seat.destroy(); 361 seat = null; 362 } 363 if (wlShell) { 364 wlShell.destroy(); 365 wlShell = null; 366 } 367 if (compositor) { 368 compositor.destroy(); 369 compositor = null; 370 } 371 _instance.unload(); 372 display.disconnect(); 373 display = null; 374 } 375 } 376 377 private class WaylandKeyboard : XKeyboard 378 { 379 this (int fd, uint size) 380 { 381 import core.sys.posix.sys.mman; 382 import core.sys.posix.unistd : close; 383 import std.exception : enforce; 384 import xkbcommon.xkbcommon; 385 386 void* buf = mmap(null, size, PROT_READ, MAP_SHARED, fd, 0); 387 enforce(buf != MAP_FAILED, "Could not mmap the wayland keymap"); 388 scope(exit) { 389 munmap(buf, size); 390 close(fd); 391 } 392 393 auto ctx = enforce( 394 xkb_context_new(XKB_CONTEXT_NO_FLAGS), "Could not alloc XKB context" 395 ); 396 scope(failure) xkb_context_unref(ctx); 397 398 auto keymap = enforce( 399 xkb_keymap_new_from_buffer( 400 ctx, cast(char*)buf, size-1, 401 XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS 402 ), 403 "Could not read keymap from mmapped file" 404 ); 405 scope(failure) xkb_keymap_unref(keymap); 406 407 auto state = xkb_state_new(keymap); 408 409 super(ctx, keymap, state); 410 } 411 412 } 413 414 private alias Side = XdgToplevel.ResizeEdge; 415 416 private string sideToCursor(Side side) 417 { 418 final switch (side) 419 { 420 case Side.none: return "default"; 421 case Side.top: return "n-resize"; 422 case Side.bottom: return "s-resize"; 423 case Side.left: return "w-resize"; 424 case Side.right: return "e-resize"; 425 case Side.topLeft: return "nw-resize"; 426 case Side.topRight: return "ne-resize"; 427 case Side.bottomLeft: return "sw-resize"; 428 case Side.bottomRight: return "se-resize"; 429 } 430 } 431 432 433 private abstract class WaylandWindowBase : Window 434 { 435 this(WaylandDisplay display, Instance instance, string title) 436 { 437 this.dpy = display; 438 this.instance = instance; 439 this._title = title; 440 } 441 442 override @property string title() 443 { 444 return _title; 445 } 446 447 override void setTitle(in string title) 448 { 449 _title = title; 450 } 451 452 override void close() { 453 closeShell(); 454 wlSurface.destroy(); 455 wlSurface = null; 456 dpy.unrefWindow(this); 457 } 458 459 abstract protected void prepareShell(WlSurface wlSurf); 460 461 override void show (uint width, uint height) 462 { 463 import std.exception : enforce; 464 465 wlSurface = dpy.compositor.createSurface(); 466 prepareShell(wlSurface); 467 gfxSurface = enforce( 468 createVulkanWaylandSurface(instance, dpy.display, wlSurface), 469 "Could ont create a Vulkan surface" 470 ); 471 wlSurface.commit(); 472 this.width = width; 473 this.height = height; 474 } 475 476 abstract protected void closeShell(); 477 478 override @property void onResize(ResizeHandler handler) { 479 resizeHandler = handler; 480 } 481 override @property void onMouseMove(MouseHandler handler) { 482 moveHandler = handler; 483 } 484 override @property void onMouseOn(MouseHandler handler) { 485 onHandler = handler; 486 } 487 override @property void onMouseOff(MouseHandler handler) { 488 offHandler = handler; 489 } 490 override @property void onKeyOn(KeyHandler handler) { 491 onKeyOnHandler = handler; 492 } 493 override @property void onKeyOff(KeyHandler handler) { 494 onKeyOffHandler = handler; 495 } 496 override @property void onClose(CloseHandler handler) { 497 onCloseHandler = handler; 498 } 499 500 override @property Surface surface() { 501 return gfxSurface; 502 } 503 504 override @property bool closeFlag() const { 505 return _closeFlag; 506 } 507 508 override @property void closeFlag(in bool flag) { 509 _closeFlag = flag; 510 } 511 512 private void pointerButton(WlPointer.ButtonState state, uint serial, KeyMods mods) 513 { 514 const ev = MouseEvent (cast(int)curX, cast(int)curY, mods); 515 516 switch (state) { 517 case WlPointer.ButtonState.pressed: 518 const side = checkResizeArea(); 519 if (side != Side.none) { 520 startResize(side, serial); 521 } 522 else { 523 if (onHandler) onHandler(ev); 524 } 525 break; 526 case WlPointer.ButtonState.released: 527 if (offHandler) offHandler(ev); 528 break; 529 default: 530 break; 531 } 532 } 533 534 private void pointerMotion(WlFixed x, WlFixed y, uint serial, KeyMods mods) 535 { 536 curX = x; curY = y; 537 538 const side = checkResizeArea(); 539 if (side != currentSide) { 540 dpy.setCursor(side.sideToCursor(), serial); 541 currentSide = side; 542 } 543 if (moveHandler) { 544 auto ev = MouseEvent(cast(int)x, cast(int)y, mods); 545 moveHandler(ev); 546 } 547 } 548 549 550 private void pointerEnter(WlFixed x, WlFixed y, uint serial) 551 { 552 curX = x; curY = y; 553 const side = checkResizeArea(); 554 dpy.setCursor(side.sideToCursor(), serial); 555 currentSide = side; 556 } 557 558 private void pointerLeave(uint serial) 559 {} 560 561 protected abstract void startResize(Side side, uint serial); 562 563 private Side checkResizeArea() 564 { 565 const x = cast(int)curX; 566 const y = cast(int)curY; 567 568 Side side = Side.none; 569 570 if (x <= resizeMargin) side |= Side.left; 571 else if (x >= width - resizeMargin) side |= Side.right; 572 573 if (y <= resizeMargin) side |= Side.top; 574 else if (y >= height - resizeMargin) side |= Side.bottom; 575 576 return side; 577 } 578 579 private WaylandDisplay dpy; 580 private Instance instance; 581 private WlSurface wlSurface; 582 private Surface gfxSurface; 583 584 // event handlers 585 private ResizeHandler resizeHandler; 586 private MouseHandler moveHandler; 587 private MouseHandler onHandler; 588 private MouseHandler offHandler; 589 private KeyHandler onKeyOnHandler; 590 private KeyHandler onKeyOffHandler; 591 private CloseHandler onCloseHandler; 592 593 // state handling 594 private bool _closeFlag; 595 private string _title; 596 private WlFixed curX; 597 private WlFixed curY; 598 private uint width; 599 private uint height; 600 private Side currentSide; 601 602 // parameters 603 private enum resizeMargin = 5; 604 } 605 606 private class WaylandWindow : WaylandWindowBase 607 { 608 this (WaylandDisplay display, Instance instance, WlShell wlShell, string title) 609 { 610 super(display, instance, title); 611 this.wlShell = wlShell; 612 } 613 614 override protected void prepareShell(WlSurface wlSurf) 615 { 616 wlShellSurf = wlShell.getShellSurface(wlSurf); 617 wlShellSurf.onPing = (WlShellSurface ss, uint serial) 618 { 619 ss.pong(serial); 620 }; 621 622 wlShellSurf.setToplevel(); 623 wlShellSurf.onConfigure = &onConfigure; 624 } 625 626 override protected void closeShell() 627 { 628 wlShellSurf.destroy(); 629 } 630 631 override protected void startResize(Side side, uint serial) 632 { 633 wlShellSurf.resize(dpy.seat, serial, cast(WlShellSurface.Resize)side); 634 } 635 636 private void onConfigure(WlShellSurface, WlShellSurface.Resize, int width, int height) 637 { 638 if (resizeHandler) resizeHandler(width, height); 639 } 640 641 private WlShell wlShell; 642 private WlShellSurface wlShellSurf; 643 } 644 645 private class XdgWaylandWindow : WaylandWindowBase 646 { 647 this (WaylandDisplay display, Instance instance, XdgWmBase xdgShell, string title) 648 { 649 super(display, instance, title); 650 this.xdgShell = xdgShell; 651 } 652 653 override protected void prepareShell(WlSurface wlSurf) 654 { 655 xdgSurf = xdgShell.getXdgSurface(wlSurf); 656 xdgTopLevel = xdgSurf.getToplevel(); 657 658 xdgTopLevel.onConfigure = &onTLConfigure; 659 xdgTopLevel.onClose = &onTLClose; 660 xdgTopLevel.setTitle(title); 661 662 xdgSurf.onConfigure = (XdgSurface xdgSurf, uint serial) 663 { 664 xdgSurf.ackConfigure(serial); 665 }; 666 } 667 668 override void setTitle(in string title) 669 { 670 _title = title; 671 if (xdgTopLevel) xdgTopLevel.setTitle(title); 672 } 673 674 void onTLConfigure(XdgToplevel, int width, int height, wl_array* states) 675 { 676 if (width != 0) { 677 this.width = width; 678 } 679 if (height != 0) { 680 this.height = height; 681 } 682 if (resizeHandler) resizeHandler(this.width, this.height); 683 } 684 685 void onTLClose(XdgToplevel) 686 { 687 if (onCloseHandler) { 688 _closeFlag = onCloseHandler(); 689 } 690 else { 691 _closeFlag = true; 692 } 693 } 694 695 override protected void closeShell() 696 { 697 xdgTopLevel.destroy(); 698 xdgSurf.destroy(); 699 } 700 701 override protected void startResize(Side side, uint serial) 702 { 703 xdgTopLevel.resize(dpy.seat, serial, cast(uint)side); 704 } 705 706 private bool configured; 707 private bool geometrySet; 708 private XdgWmBase xdgShell; 709 private XdgSurface xdgSurf; 710 private XdgToplevel xdgTopLevel; 711 }