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 }