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 }