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 }