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