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