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 }