1 /**
2  * X11 window implementation.
3  *
4  * Copyright: Copyright (C) 2017 Richard Andrew Cattermole
5  *            Copyright (C) 2017 Ethan Reker
6  *            Copyright (C) 2017 Lukasz Pelszynski
7  *
8  * Bugs:
9  *     - X11 does not support double clicks, it is sometimes emulated https://github.com/glfw/glfw/issues/462
10  *
11  * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
12  * Authors:   Richard (Rikki) Andrew Cattermole
13  */
14 module dplug.window.x11window;
15 
16 import gfm.math.box;
17 
18 import core.sys.posix.unistd;
19 import core.stdc..string;
20 import core.atomic;
21 
22 import dplug.window.window;
23 
24 import dplug.core.runtime;
25 import dplug.core.nogc;
26 import dplug.core.thread;
27 import dplug.core.sync;
28 
29 import dplug.graphics.image;
30 import dplug.graphics.view;
31 
32 nothrow:
33 @nogc:
34 
35 version(linux):
36 
37 import dplug.core.map;
38 
39 import derelict.x11.X;
40 import derelict.x11.Xlib;
41 import derelict.x11.keysym;
42 import derelict.x11.keysymdef;
43 import derelict.x11.Xutil;
44 import derelict.x11.extensions.Xrandr;
45 import derelict.x11.extensions.randr;
46 
47 // This is an extension to X11, almost always should exist on modern systems
48 // If it becomes a problem, version out its usage, it'll work just won't be as nice event wise
49 extern(C) bool XkbSetDetectableAutoRepeat(Display*, bool, bool*);
50 
51 __gshared XLibInitialized = false;
52 __gshared Display* _display;
53 __gshared size_t _white_pixel, _black_pixel;
54 __gshared int _screen;
55 
56 final class X11Window : IWindow
57 {
58 nothrow:
59 @nogc:
60 
61 private:
62     // Xlib variables
63     Window _windowId, _parentWindowId;
64     Atom _closeAtom;
65     derelict.x11.Xlib.GC _graphicGC;
66     XImage* _graphicImage;
67     int width, height, depth;
68     // Threads
69     Thread _eventLoop, _timerLoop;
70     UncheckedMutex drawMutex;
71     //Other
72     IWindowListener listener;
73 
74     ImageRef!RGBA _wfb; // framebuffer reference
75     ubyte[4][] _bufferData;
76 
77     uint lastTimeGot, creationTime, currentTime;
78     int lastMouseX, lastMouseY;
79 
80     box2i prevMergedDirtyRect, mergedDirtyRect;
81 
82     shared(bool) _terminated = false;
83 
84 public:
85     this(void* parentWindow, IWindowListener listener, int width, int height)
86     {
87         drawMutex = makeMutex();
88 
89         initializeXLib();
90 
91         int x, y;
92         this.listener = listener;
93 
94         if (parentWindow is null)
95         {
96             _parentWindowId = RootWindow(_display, _screen);
97         }
98         else
99         {
100             _parentWindowId = cast(Window)parentWindow;
101         }
102 
103         x = (DisplayWidth(_display, _screen) - width) / 2;
104         y = (DisplayHeight(_display, _screen) - height) / 3;
105         this.width = width;
106         this.height = height;
107         depth = 24;
108 
109         //
110 
111         _windowId = XCreateSimpleWindow(_display, _parentWindowId, x, y, width, height, 0, 0, _black_pixel);
112         XStoreName(_display, _windowId, cast(char*)" ".ptr);
113 
114         XSizeHints sizeHints;
115         sizeHints.flags = PMinSize | PMaxSize;
116         sizeHints.min_width = width;
117         sizeHints.max_width = width;
118         sizeHints.min_height = height;
119         sizeHints.max_height = height;
120 
121         XSetWMNormalHints(_display, _windowId, &sizeHints);
122 
123         //
124 
125         _closeAtom = XInternAtom(_display, cast(char*)("WM_DELETE_WINDOW".ptr), cast(Bool)false);
126         XSetWMProtocols(_display, _windowId, &_closeAtom, 1);
127 
128         if (parentWindow) {
129           // Embed the window in parent (most VST hosts expose some area for embedding a VST client)
130           XReparentWindow(_display, _windowId, _parentWindowId, 0, 0);
131         }
132 
133         XMapWindow(_display, _windowId);
134         XFlush(_display);
135 
136         XSelectInput(_display, _windowId, windowEventMask());
137         _graphicGC = XCreateGC(_display, _windowId, 0, null);
138         XSetBackground(_display, _graphicGC, _white_pixel);
139         XSetForeground(_display, _graphicGC, _black_pixel);
140 
141         _wfb = listener.onResized(width, height);
142 
143         creationTime = getTimeMs();
144         lastTimeGot = creationTime;
145 
146         emptyMergedBoxes();
147 
148         _timerLoop = makeThread(&timerLoop);
149         _timerLoop.start();
150 
151         _eventLoop = makeThread(&eventLoop);
152         _eventLoop.start();
153     }
154 
155     ~this()
156     {
157         XDestroyWindow(_display, _windowId);
158         XFlush(_display);
159         _timerLoop.join();
160         _eventLoop.join();
161     }
162 
163     void initializeXLib() {
164         if (!XLibInitialized) {
165             XInitThreads();
166 
167             _display = XOpenDisplay(null);
168             if(_display == null)
169                 assert(false);
170 
171             _screen = DefaultScreen(_display);
172             _white_pixel = WhitePixel(_display, _screen);
173             _black_pixel = BlackPixel(_display, _screen);
174             XkbSetDetectableAutoRepeat(_display, true, null);
175 
176             XLibInitialized = true;
177         }
178     }
179 
180     long windowEventMask() {
181         return ExposureMask | StructureNotifyMask |
182             KeyReleaseMask | KeyPressMask | ButtonReleaseMask | ButtonPressMask | PointerMotionMask;
183     }
184 
185     void swapBuffers(ImageRef!RGBA wfb, box2i[] areasToRedraw)
186     {
187         if (_bufferData.length != wfb.w * wfb.h)
188         {
189             _bufferData = mallocSlice!(ubyte[4])(wfb.w * wfb.h);
190 
191             if (_graphicImage !is null)
192             {
193                 // X11 deallocates _bufferData for us (ugh...)
194                 XDestroyImage(_graphicImage);
195             }
196 
197             _graphicImage = XCreateImage(_display, cast(Visual*)&_graphicGC, depth, ZPixmap, 0, cast(char*)_bufferData.ptr, width, height, 32, 0);
198 
199             size_t i;
200             foreach(y; 0 .. wfb.h)
201             {
202                 RGBA[] scanLine = wfb.scanline(y);
203                 foreach(x, ref c; scanLine)
204                 {
205                     _bufferData[i][0] = c.b;
206                     _bufferData[i][1] = c.g;
207                     _bufferData[i][2] = c.r;
208                     _bufferData[i][3] = c.a;
209                     i++;
210                 }
211             }
212         }
213         else
214         {
215             foreach(box2i area; areasToRedraw)
216             {
217                 foreach(y; area.min.y .. area.max.y)
218                 {
219                     RGBA[] scanLine = wfb.scanline(y);
220 
221                     size_t i = y * wfb.w;
222                     i += area.min.x;
223 
224                     foreach(x, ref c; scanLine[area.min.x .. area.max.x])
225                     {
226                         _bufferData[i][0] = c.b;
227                         _bufferData[i][1] = c.g;
228                         _bufferData[i][2] = c.r;
229                         _bufferData[i][3] = c.a;
230                         i++;
231                     }
232                 }
233             }
234         }
235 
236         XPutImage(_display, _windowId, _graphicGC, _graphicImage, 0, 0, 0, 0, cast(uint)width, cast(uint)height);
237     }
238 
239     // Implements IWindow
240     override void waitEventAndDispatch() nothrow @nogc
241     {
242         XEvent event;
243         // Wait for events for current window
244         XWindowEvent(_display, _windowId, windowEventMask(), &event);
245         handleEvents(event, this);
246     }
247 
248     void eventLoop() nothrow @nogc
249     {
250         while (!terminated()) {
251             waitEventAndDispatch();
252         }
253     }
254 
255     void emptyMergedBoxes() nothrow @nogc
256     {
257         prevMergedDirtyRect = box2i(0,0,0,0);
258         mergedDirtyRect = box2i(0,0,0,0);
259     }
260 
261     void sendRepaintIfUIDirty() nothrow @nogc
262     {
263         listener.recomputeDirtyAreas();
264         box2i dirtyRect = listener.getDirtyRectangle();
265         if (!dirtyRect.empty())
266         {
267             drawMutex.lock();
268 
269             prevMergedDirtyRect = mergedDirtyRect;
270             mergedDirtyRect = mergedDirtyRect.expand(dirtyRect);
271             // If everything has been drawn by Expose event handler, send Expose event.
272             // Otherwise merge areas to be redrawn and postpone Expose event.
273             if (prevMergedDirtyRect.empty() && !mergedDirtyRect.empty()) {
274                 int x = dirtyRect.min.x;
275                 int y = dirtyRect.min.y;
276                 int width = dirtyRect.max.x - x;
277                 int height = dirtyRect.max.y - y;
278 
279                 XEvent evt;
280                 memset(&evt, 0, XEvent.sizeof);
281                 evt.type = Expose;
282                 evt.xexpose.window = _windowId;
283                 evt.xexpose.display = _display;
284                 evt.xexpose.x = 0;
285                 evt.xexpose.y = 0;
286                 evt.xexpose.width = 0;
287                 evt.xexpose.height = 0;
288 
289                 XSendEvent(_display, _windowId, False, ExposureMask, &evt);
290                 XFlush(_display);
291             }
292 
293             drawMutex.unlock();
294         }
295     }
296 
297     void timerLoop() nothrow @nogc
298     {
299         while(!terminated())
300         {
301             currentTime = getTimeMs();
302             float diff = currentTime - lastTimeGot;
303             double dt = (currentTime - lastTimeGot) * 0.001;
304             double time = (currentTime - creationTime) * 0.001;
305             listener.onAnimate(dt, time);
306             sendRepaintIfUIDirty();
307             lastTimeGot = currentTime;
308             //Sleep for ~16.6 milliseconds (60 frames per second rendering)
309             usleep(16666);
310         }
311     }
312 
313     override bool terminated()
314     {
315         return atomicLoad(_terminated);
316     }
317 
318     override uint getTimeMs()
319     {
320         static uint perform() {
321             import core.sys.posix.sys.time;
322             timeval  tv;
323             gettimeofday(&tv, null);
324             return cast(uint)((tv.tv_sec) * 1000 + (tv.tv_usec) / 1000) ;
325 
326         }
327 
328         return assumeNothrowNoGC(&perform)();
329     }
330 
331     override void* systemHandle()
332     {
333         return cast(void*)_windowId;
334     }
335 }
336 
337 void handleEvents(ref XEvent event, X11Window theWindow) nothrow @nogc
338 {
339     with(theWindow)
340     {
341 
342         switch(event.type)
343         {
344             case KeyPress:
345                 KeySym symbol;
346                 XLookupString(&event.xkey, null, 0, &symbol, null);
347                 listener.onKeyDown(convertKeyFromX11(symbol));
348                 break;
349 
350             case KeyRelease:
351                 KeySym symbol;
352                 XLookupString(&event.xkey, null, 0, &symbol, null);
353                 listener.onKeyUp(convertKeyFromX11(symbol));
354                 break;
355 
356             case MapNotify:
357             case Expose:
358                 // Resize should trigger Expose event, so we don't need to handle it here
359                 drawMutex.lock();
360 
361                 box2i areaToRedraw = mergedDirtyRect;
362                 box2i eventAreaToRedraw = box2i(event.xexpose.x, event.xexpose.y, event.xexpose.x + event.xexpose.width, event.xexpose.y + event.xexpose.height);
363                 areaToRedraw = areaToRedraw.expand(eventAreaToRedraw);
364 
365                 emptyMergedBoxes();
366 
367                 drawMutex.unlock();
368 
369                 if (!areaToRedraw.empty()) {
370                     listener.onDraw(WindowPixelFormat.RGBA8);
371                     box2i[] areasToRedraw = (&areaToRedraw)[0..1];
372                     swapBuffers(_wfb, areasToRedraw);
373                 }
374                 break;
375 
376             case ConfigureNotify:
377                 if (event.xconfigure.width != width || event.xconfigure.height != height)
378                 {
379                     // Handle resize event
380                     width = event.xconfigure.width;
381                     height = event.xconfigure.height;
382 
383                     _wfb = listener.onResized(width, height);
384                     sendRepaintIfUIDirty();
385                 }
386                 break;
387 
388             case MotionNotify:
389                 int newMouseX = event.xmotion.x;
390                 int newMouseY = event.xmotion.y;
391                 int dx = newMouseX - lastMouseX;
392                 int dy = newMouseY - lastMouseY;
393 
394                 listener.onMouseMove(newMouseX, newMouseY, dx, dy, mouseStateFromX11(event.xbutton.state));
395 
396                 lastMouseX = newMouseX;
397                 lastMouseY = newMouseY;
398                 break;
399 
400             case ButtonPress:
401                 int newMouseX = event.xbutton.x;
402                 int newMouseY = event.xbutton.y;
403 
404                 MouseButton button;
405 
406                 if (event.xbutton.button == Button1)
407                     button = MouseButton.left;
408                 else if (event.xbutton.button == Button3)
409                     button = MouseButton.right;
410                 else if (event.xbutton.button == Button2)
411                     button = MouseButton.middle;
412                 else if (event.xbutton.button == Button4)
413                     button = MouseButton.x1;
414                 else if (event.xbutton.button == Button5)
415                     button = MouseButton.x2;
416 
417                 bool isDoubleClick;
418 
419                 lastMouseX = newMouseX;
420                 lastMouseY = newMouseY;
421 
422                 if (event.xbutton.button == Button4 || event.xbutton.button == Button5)
423                 {
424                     listener.onMouseWheel(newMouseX, newMouseY, 0, event.xbutton.button == Button4 ? 1 : -1,
425                         mouseStateFromX11(event.xbutton.state));
426                 }
427                 else
428                 {
429                     listener.onMouseClick(newMouseX, newMouseY, button, isDoubleClick, mouseStateFromX11(event.xbutton.state));
430                 }
431                 break;
432 
433             case ButtonRelease:
434                 int newMouseX = event.xbutton.x;
435                 int newMouseY = event.xbutton.y;
436 
437                 MouseButton button;
438 
439                 lastMouseX = newMouseX;
440                 lastMouseY = newMouseY;
441 
442                 if (event.xbutton.button == Button1)
443                     button = MouseButton.left;
444                 else if (event.xbutton.button == Button3)
445                     button = MouseButton.right;
446                 else if (event.xbutton.button == Button2)
447                     button = MouseButton.middle;
448                 else if (event.xbutton.button == Button4 || event.xbutton.button == Button5)
449                     break;
450 
451                 listener.onMouseRelease(newMouseX, newMouseY, button, mouseStateFromX11(event.xbutton.state));
452                 break;
453 
454             case DestroyNotify:
455                 XDestroyImage(_graphicImage);
456                 XFreeGC(_display, _graphicGC);
457                 atomicStore(_terminated, true);
458                 break;
459 
460             case ClientMessage:
461                 // TODO Possibly not used anymore
462                 if (event.xclient.data.l[0] == _closeAtom)
463                 {
464                     atomicStore(_terminated, true);
465                     XDestroyImage(_graphicImage);
466                     XFreeGC(_display, _graphicGC);
467                     XDestroyWindow(_display, _windowId);
468                     XFlush(_display);
469                 }
470                 break;
471 
472             default:
473                 break;
474         }
475     }
476 }
477 
478 Key convertKeyFromX11(KeySym symbol)
479 {
480     switch(symbol)
481     {
482         case XK_space:
483             return Key.space;
484 
485         case XK_Up:
486             return Key.upArrow;
487 
488         case XK_Down:
489             return Key.downArrow;
490 
491         case XK_Left:
492             return Key.leftArrow;
493 
494         case XK_Right:
495             return Key.rightArrow;
496 
497         case XK_0: .. case XK_9:
498             return cast(Key)(Key.digit0 + (symbol - XK_0));
499 
500         case XK_KP_0: .. case XK_KP_9:
501             return cast(Key)(Key.digit0 + (symbol - XK_KP_0));
502 
503         case XK_A: .. case XK_Z:
504             return cast(Key)(Key.A + (symbol - XK_A));
505 
506         case XK_a: .. case XK_z:
507             return cast(Key)(Key.a + (symbol - XK_a));
508 
509         case XK_Return:
510         case XK_KP_Enter:
511             return Key.enter;
512 
513         case XK_Escape:
514             return Key.escape;
515 
516         case XK_BackSpace:
517             return Key.backspace;
518 
519         // case 0x0041:
520         //     return Key.A;
521         default:
522             return Key.unsupported;
523     }
524 }
525 
526 MouseState mouseStateFromX11(uint state) {
527     return MouseState(
528         (state & Button1Mask) == Button1Mask,
529         (state & Button3Mask) == Button3Mask,
530         (state & Button2Mask) == Button2Mask,
531         false, false,
532         (state & ControlMask) == ControlMask,
533         (state & ShiftMask) == ShiftMask,
534         (state & Mod1Mask) == Mod1Mask);
535 }