X11Window.cpp 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. 
  2. // Avoid cluttering the global namespace by hiding these from the header
  3. #include <X11/Xlib.h>
  4. #include <X11/Xutil.h>
  5. #include <X11/Xos.h>
  6. #include <X11/Xatom.h>
  7. #include "../DFPSR/api/imageAPI.h"
  8. #include "../DFPSR/api/drawAPI.h"
  9. #include "../DFPSR/api/timeAPI.h"
  10. #include "../DFPSR/gui/BackendWindow.h"
  11. // According to this documentation, XInitThreads doesn't have to be used if a mutex is wrapped around all the calls to XLib.
  12. // https://tronche.com/gui/x/xlib/display/XInitThreads.html
  13. #include <mutex>
  14. #include <future>
  15. #include <climits>
  16. static std::mutex windowLock;
  17. // Enable this macro to disable multi-threading
  18. //#define DISABLE_MULTI_THREADING
  19. static const int bufferCount = 2;
  20. class X11Window : public dsr::BackendWindow {
  21. private:
  22. // The display is the connection to the X server
  23. // Each window has it's own connection, because you're only supposed to have one window
  24. // Let sub-windows be visual components for simpler input and deterministic custom decorations
  25. Display *display = nullptr;
  26. // The handle to the X11 window
  27. Window window;
  28. // Holds settings for drawing to the window
  29. GC graphicsContext;
  30. // Invisible cursor for hiding it over the window
  31. Cursor noCursor;
  32. // Double buffering to allow drawing to a canvas while displaying the previous one
  33. // The image which can be drawn to, sharing memory with the X11 image
  34. dsr::AlignedImageRgbaU8 canvas[bufferCount];
  35. // An X11 image wrapped around the canvas pixel data
  36. XImage *canvasX[bufferCount] = {};
  37. int drawIndex = 0 % bufferCount;
  38. int showIndex = 1 % bufferCount;
  39. bool firstFrame = true;
  40. #ifndef DISABLE_MULTI_THREADING
  41. // The background worker for displaying the result using a separate thread protected by a mutex
  42. std::future<void> displayFuture;
  43. #endif
  44. // Remembers the dimensions of the window from creation and resize events
  45. // This allow requesting the size of the window at any time
  46. int windowWidth = 0, windowHeight = 0;
  47. // Called before the application fetches events from the input queue
  48. // Closing the window, moving the mouse, pressing a key, et cetera
  49. void prefetchEvents() override;
  50. // Called to change the cursor visibility and returning true on success
  51. void applyCursorVisibility_locked();
  52. bool setCursorVisibility(bool visible) override;
  53. // Place the cursor within the window
  54. void setCursorPosition(int x, int y) override;
  55. // Color format
  56. dsr::PackOrderIndex packOrderIndex = dsr::PackOrderIndex::RGBA;
  57. dsr::PackOrderIndex getColorFormat_locked();
  58. private:
  59. // Helper methods specific to calling XLib
  60. void updateTitle_locked();
  61. private:
  62. // Canvas methods
  63. dsr::AlignedImageRgbaU8 getCanvas() override { return this->canvas[this->drawIndex]; }
  64. void resizeCanvas(int width, int height) override;
  65. // Window methods
  66. void setTitle(const dsr::String &newTitle) override {
  67. this->title = newTitle;
  68. this->updateTitle_locked();
  69. }
  70. void removeOldWindow_locked();
  71. void createGCWindow_locked(const dsr::String& title, int width, int height);
  72. void createWindowed_locked(const dsr::String& title, int width, int height);
  73. void createFullscreen_locked();
  74. void prepareWindow_locked();
  75. int windowState = 0; // 0=none, 1=windowed, 2=fullscreen
  76. public:
  77. // Constructors
  78. X11Window(const X11Window&) = delete; // Non-copyable because of pointer aliasing.
  79. X11Window(const dsr::String& title, int width, int height);
  80. int getWidth() const override { return this->windowWidth; };
  81. int getHeight() const override { return this->windowHeight; };
  82. // Destructor
  83. ~X11Window();
  84. // Full-screen
  85. void setFullScreen(bool enabled) override;
  86. bool isFullScreen() override { return this->windowState == 2; }
  87. // Showing the content
  88. void showCanvas() override;
  89. // Clipboard access
  90. Atom clipboardAtom, targetsAtom, utf8StringAtom, targetAtom;
  91. bool loadingFromClipboard = false;
  92. dsr::String textFromClipboard;
  93. dsr::String textToClipboard;
  94. void listContentInClipboard();
  95. void initializeClipboard();
  96. void terminateClipboard();
  97. dsr::ReadableString loadFromClipboard(int64_t timeoutInMilliseconds);
  98. void saveToClipboard(const dsr::ReadableString &text);
  99. };
  100. void X11Window::initializeClipboard() {
  101. this->clipboardAtom = XInternAtom(this->display , "CLIPBOARD", False);
  102. this->targetsAtom = XInternAtom(this->display , "TARGETS", False);
  103. this->utf8StringAtom = XInternAtom(this->display, "UTF8_STRING", False);
  104. this->targetAtom = None;
  105. }
  106. void X11Window::terminateClipboard() {
  107. // TODO: Send ownership to the clipboard to allow pasting after terminating the program it was copied from.
  108. }
  109. dsr::ReadableString X11Window::loadFromClipboard(int64_t timeoutInMilliseconds) {
  110. // TODO: Can the old request be aborted if it takes too long?
  111. if (!this->loadingFromClipboard) {
  112. // Request text to paste and wait some time for an application to respond.
  113. // TODO: How can old content be ignored after a timeout if the time is too short?
  114. XConvertSelection(this->display, this->clipboardAtom, this->targetsAtom, this->clipboardAtom, this->window, CurrentTime);
  115. this->loadingFromClipboard = true;
  116. // TODO: Implement timeout without drifting time.
  117. int64_t time = 0;
  118. while (this->loadingFromClipboard && time < timeoutInMilliseconds) {
  119. this->prefetchEvents();
  120. dsr::time_sleepSeconds(0.001);
  121. time++;
  122. }
  123. return this->textFromClipboard;
  124. } else {
  125. return U"";
  126. }
  127. }
  128. void X11Window::listContentInClipboard() {
  129. // Tell other programs sharing the clipboard that something is available to paste.
  130. XSetSelectionOwner(this->display, this->clipboardAtom, this->window, CurrentTime);
  131. }
  132. void X11Window::saveToClipboard(const dsr::ReadableString &text) {
  133. this->textToClipboard = text;
  134. this->listContentInClipboard();
  135. }
  136. void X11Window::setCursorPosition(int x, int y) {
  137. windowLock.lock();
  138. XWarpPointer(this->display, this->window, this->window, 0, 0, this->windowWidth, this->windowHeight, x, y);
  139. windowLock.unlock();
  140. }
  141. void X11Window::applyCursorVisibility_locked() {
  142. windowLock.lock();
  143. if (this->visibleCursor) {
  144. // Reset to parent cursor
  145. XUndefineCursor(this->display, this->window);
  146. } else {
  147. // Let the window display an empty cursor
  148. XDefineCursor(this->display, this->window, this->noCursor);
  149. }
  150. windowLock.unlock();
  151. }
  152. bool X11Window::setCursorVisibility(bool visible) {
  153. // Remember the cursor's visibility for anyone asking
  154. this->visibleCursor = visible;
  155. // Use the stored visibility to update the cursor
  156. this->applyCursorVisibility_locked();
  157. // Indicate success
  158. return true;
  159. }
  160. void X11Window::updateTitle_locked() {
  161. windowLock.lock();
  162. XSetStandardProperties(this->display, this->window, this->title.toStdString().c_str(), "Icon", None, NULL, 0, NULL);
  163. windowLock.unlock();
  164. }
  165. dsr::PackOrderIndex X11Window::getColorFormat_locked() {
  166. windowLock.lock();
  167. XVisualInfo visualRequest;
  168. visualRequest.screen = 0;
  169. visualRequest.depth = 32;
  170. visualRequest.c_class = TrueColor;
  171. int visualCount;
  172. XVisualInfo *formatList = XGetVisualInfo(this->display, VisualScreenMask | VisualDepthMask | VisualClassMask, &visualRequest, &visualCount);
  173. dsr::PackOrderIndex result = dsr::PackOrderIndex::RGBA;
  174. if (formatList == nullptr) {
  175. dsr::throwError(U"Error! The display does not support truecolor formats.\n");
  176. } else {
  177. for (int i = 0; i < visualCount; i++) {
  178. if (formatList[i].bits_per_rgb == 8) {
  179. const uint32_t red = formatList[i].red_mask;
  180. const uint32_t green = formatList[i].green_mask;
  181. const uint32_t blue = formatList[i].blue_mask;
  182. const uint32_t first = 255u;
  183. const uint32_t second = 255u << 8u;
  184. const uint32_t third = 255u << 16u;
  185. const uint32_t fourth = 255u << 24u;
  186. if (red == first && green == second && blue == third) {
  187. result = dsr::PackOrderIndex::RGBA;
  188. } else if (red == second && green == third && blue == fourth) {
  189. result = dsr::PackOrderIndex::ARGB;
  190. } else if (blue == first && green == second && red == third) {
  191. result = dsr::PackOrderIndex::BGRA;
  192. } else if (blue == second && green == third && red == fourth) {
  193. result = dsr::PackOrderIndex::ABGR;
  194. } else {
  195. dsr::throwError(U"Error! Unhandled color format. Only RGBA, ARGB, BGRA and ABGR are currently supported.\n");
  196. }
  197. break;
  198. }
  199. }
  200. XFree(formatList);
  201. }
  202. windowLock.unlock();
  203. return result;
  204. }
  205. struct Hints {
  206. unsigned long flags;
  207. unsigned long functions;
  208. unsigned long decorations;
  209. long inputMode;
  210. unsigned long status;
  211. };
  212. void X11Window::setFullScreen(bool enabled) {
  213. if (this->windowState == 1 && enabled) {
  214. // Clean up any previous X11 window
  215. removeOldWindow_locked();
  216. // Create the new window and graphics context
  217. this->createFullscreen_locked();
  218. } else if (this->windowState == 2 && !enabled) {
  219. // Clean up any previous X11 window
  220. removeOldWindow_locked();
  221. // Create the new window and graphics context
  222. this->createWindowed_locked(this->title, 800, 600); // TODO: Remember the dimensions from last windowed mode
  223. }
  224. this->applyCursorVisibility_locked();
  225. windowLock.lock();
  226. listContentInClipboard();
  227. windowLock.unlock();
  228. }
  229. void X11Window::removeOldWindow_locked() {
  230. windowLock.lock();
  231. if (this->windowState != 0) {
  232. XFreeGC(this->display, this->graphicsContext);
  233. XDestroyWindow(this->display, this->window);
  234. XUngrabPointer(this->display, CurrentTime);
  235. }
  236. this->windowState = 0;
  237. windowLock.unlock();
  238. }
  239. void X11Window::prepareWindow_locked() {
  240. windowLock.lock();
  241. // Set input masks
  242. XSelectInput(this->display, this->window,
  243. ExposureMask | StructureNotifyMask |
  244. PointerMotionMask |
  245. ButtonPressMask | ButtonReleaseMask |
  246. KeyPressMask | KeyReleaseMask
  247. );
  248. // Listen to the window close event.
  249. Atom WM_DELETE_WINDOW = XInternAtom(this->display, "WM_DELETE_WINDOW", False);
  250. XSetWMProtocols(this->display, this->window, &WM_DELETE_WINDOW, 1);
  251. windowLock.unlock();
  252. // Reallocate the canvas
  253. this->resizeCanvas(this->windowWidth, this->windowHeight);
  254. }
  255. void X11Window::createGCWindow_locked(const dsr::String& title, int width, int height) {
  256. windowLock.lock();
  257. // Request to resize the canvas and interface according to the new window
  258. this->windowWidth = width;
  259. this->windowHeight = height;
  260. this->receivedWindowResize(width, height);
  261. int screenIndex = DefaultScreen(this->display);
  262. unsigned long black = BlackPixel(this->display, screenIndex);
  263. unsigned long white = WhitePixel(this->display, screenIndex);
  264. // Create a new window
  265. this->window = XCreateSimpleWindow(this->display, DefaultRootWindow(this->display), 0, 0, width, height, 0, white, black);
  266. windowLock.unlock();
  267. this->updateTitle_locked();
  268. windowLock.lock();
  269. // Create a new graphics context
  270. this->graphicsContext = XCreateGC(this->display, this->window, 0, 0);
  271. XSetBackground(this->display, this->graphicsContext, black);
  272. XSetForeground(this->display, this->graphicsContext, white);
  273. XClearWindow(this->display, this->window);
  274. windowLock.unlock();
  275. }
  276. void X11Window::createWindowed_locked(const dsr::String& title, int width, int height) {
  277. // Create the window
  278. this->createGCWindow_locked(title, width, height);
  279. windowLock.lock();
  280. // Display the window when done placing it
  281. XMapRaised(this->display, this->window);
  282. this->windowState = 1;
  283. this->firstFrame = true;
  284. windowLock.unlock();
  285. this->prepareWindow_locked();
  286. }
  287. void X11Window::createFullscreen_locked() {
  288. windowLock.lock();
  289. // Get the screen resolution
  290. Screen* screenInfo = DefaultScreenOfDisplay(this->display);
  291. windowLock.unlock();
  292. // Create the window
  293. this->createGCWindow_locked(U"", screenInfo->width, screenInfo->height);
  294. windowLock.lock();
  295. // Override redirect
  296. unsigned long valuemask = CWOverrideRedirect;
  297. XSetWindowAttributes setwinattr;
  298. setwinattr.override_redirect = 1;
  299. XChangeWindowAttributes(this->display, this->window, valuemask, &setwinattr);
  300. // Remove decorations
  301. Hints hints;
  302. memset(&hints, 0, sizeof(hints));
  303. hints.flags = 2;
  304. hints.decorations = 0;
  305. Atom property;
  306. property = XInternAtom(this->display, "_MOTIF_WM_HINTS", True); // TODO: Check if this optional XLib feature is supported
  307. XChangeProperty(this->display, this->window, property, property, 32, PropModeReplace, (unsigned char*)&hints, 5);
  308. // Move to absolute origin
  309. XMoveResizeWindow(this->display, this->window, 0, 0, screenInfo->width, screenInfo->height);
  310. // Prevent accessing anything outside of the window until it closes (for multiple displays)
  311. XGrabPointer(this->display, this->window, 1, 0, GrabModeAsync, GrabModeAsync, this->window, 0L, CurrentTime);
  312. XGrabKeyboard(this->display, this->window, 1, GrabModeAsync, GrabModeAsync, CurrentTime);
  313. // Display the window when done placing it
  314. XMapRaised(this->display, this->window);
  315. // Now that the window is visible, it can be focused for keyboard input
  316. XSetInputFocus(this->display, this->window, RevertToNone, CurrentTime);
  317. this->windowState = 2;
  318. this->firstFrame = true;
  319. windowLock.unlock();
  320. this->prepareWindow_locked();
  321. }
  322. X11Window::X11Window(const dsr::String& title, int width, int height) {
  323. bool fullScreen = false;
  324. if (width < 1 || height < 1) {
  325. fullScreen = true;
  326. width = 400;
  327. height = 300;
  328. }
  329. windowLock.lock();
  330. this->display = XOpenDisplay(nullptr);
  331. windowLock.unlock();
  332. if (this->display == nullptr) {
  333. dsr::throwError(U"Error! Failed to open XLib display!\n");
  334. return;
  335. }
  336. // Get the color format
  337. this->packOrderIndex = this->getColorFormat_locked();
  338. // Remember the title
  339. this->title = title;
  340. // Create a window
  341. if (fullScreen) {
  342. this->createFullscreen_locked();
  343. } else {
  344. this->createWindowed_locked(title, width, height);
  345. }
  346. // Create a hidden cursor stored as noCursor
  347. // Create a black color using zero bits, which will not be visible anyway
  348. XColor black; memset(&black, 0, sizeof(XColor));
  349. // Store all 8x8 pixels in a 64-bit unsigned integer
  350. uint64_t zeroBits = 0u;
  351. // Create a temporary image for both 1-bit color selection and a visibility mask
  352. Pixmap zeroBitmap = XCreateBitmapFromData(this->display, this->window, (char*)&zeroBits, 8, 8);
  353. // Create the cursor
  354. this->noCursor = XCreatePixmapCursor(this->display, zeroBitmap, zeroBitmap, &black, &black, 0, 0);
  355. // Free the temporary bitmap used to create the cursor
  356. XFreePixmap(this->display, zeroBitmap);
  357. // Create things needed for copying and pasting text.
  358. this->initializeClipboard();
  359. }
  360. // Convert keycodes from XLib to DSR
  361. static dsr::MouseKeyEnum getMouseKey(int keyCode) {
  362. dsr::MouseKeyEnum result = dsr::MouseKeyEnum::NoKey;
  363. if (keyCode == Button1) {
  364. result = dsr::MouseKeyEnum::Left;
  365. } else if (keyCode == Button2) {
  366. result = dsr::MouseKeyEnum::Middle;
  367. } else if (keyCode == Button3) {
  368. result = dsr::MouseKeyEnum::Right;
  369. } else if (keyCode == Button4) {
  370. result = dsr::MouseKeyEnum::ScrollUp;
  371. } else if (keyCode == Button5) {
  372. result = dsr::MouseKeyEnum::ScrollDown;
  373. }
  374. return result;
  375. }
  376. static bool isVerticalScrollKey(dsr::MouseKeyEnum key) {
  377. return key == dsr::MouseKeyEnum::ScrollDown || key == dsr::MouseKeyEnum::ScrollUp;
  378. }
  379. static dsr::DsrKey getDsrKey(KeySym keyCode) {
  380. dsr::DsrKey result = dsr::DsrKey_Unhandled;
  381. if (keyCode == XK_Escape) {
  382. result = dsr::DsrKey_Escape;
  383. } else if (keyCode == XK_F1) {
  384. result = dsr::DsrKey_F1;
  385. } else if (keyCode == XK_F2) {
  386. result = dsr::DsrKey_F2;
  387. } else if (keyCode == XK_F3) {
  388. result = dsr::DsrKey_F3;
  389. } else if (keyCode == XK_F4) {
  390. result = dsr::DsrKey_F4;
  391. } else if (keyCode == XK_F5) {
  392. result = dsr::DsrKey_F5;
  393. } else if (keyCode == XK_F6) {
  394. result = dsr::DsrKey_F6;
  395. } else if (keyCode == XK_F7) {
  396. result = dsr::DsrKey_F7;
  397. } else if (keyCode == XK_F8) {
  398. result = dsr::DsrKey_F8;
  399. } else if (keyCode == XK_F9) {
  400. result = dsr::DsrKey_F9;
  401. } else if (keyCode == XK_F10) {
  402. result = dsr::DsrKey_F10;
  403. } else if (keyCode == XK_F11) {
  404. result = dsr::DsrKey_F11;
  405. } else if (keyCode == XK_F12) {
  406. result = dsr::DsrKey_F12;
  407. } else if (keyCode == XK_Pause) {
  408. result = dsr::DsrKey_Pause;
  409. } else if (keyCode == XK_space) {
  410. result = dsr::DsrKey_Space;
  411. } else if (keyCode == XK_Tab) {
  412. result = dsr::DsrKey_Tab;
  413. } else if (keyCode == XK_Return) {
  414. result = dsr::DsrKey_Return;
  415. } else if (keyCode == XK_BackSpace) {
  416. result = dsr::DsrKey_BackSpace;
  417. } else if (keyCode == XK_Shift_L) {
  418. result = dsr::DsrKey_LeftShift;
  419. } else if (keyCode == XK_Shift_R) {
  420. result = dsr::DsrKey_RightShift;
  421. } else if (keyCode == XK_Control_L) {
  422. result = dsr::DsrKey_LeftControl;
  423. } else if (keyCode == XK_Control_R) {
  424. result = dsr::DsrKey_RightControl;
  425. } else if (keyCode == XK_Alt_L) {
  426. result = dsr::DsrKey_LeftAlt;
  427. } else if (keyCode == XK_Alt_R) {
  428. result = dsr::DsrKey_RightAlt;
  429. } else if (keyCode == XK_Delete) {
  430. result = dsr::DsrKey_Delete;
  431. } else if (keyCode == XK_Left) {
  432. result = dsr::DsrKey_LeftArrow;
  433. } else if (keyCode == XK_Right) {
  434. result = dsr::DsrKey_RightArrow;
  435. } else if (keyCode == XK_Up) {
  436. result = dsr::DsrKey_UpArrow;
  437. } else if (keyCode == XK_Down) {
  438. result = dsr::DsrKey_DownArrow;
  439. } else if (keyCode == XK_0) {
  440. result = dsr::DsrKey_0;
  441. } else if (keyCode == XK_1) {
  442. result = dsr::DsrKey_1;
  443. } else if (keyCode == XK_2) {
  444. result = dsr::DsrKey_2;
  445. } else if (keyCode == XK_3) {
  446. result = dsr::DsrKey_3;
  447. } else if (keyCode == XK_4) {
  448. result = dsr::DsrKey_4;
  449. } else if (keyCode == XK_5) {
  450. result = dsr::DsrKey_5;
  451. } else if (keyCode == XK_6) {
  452. result = dsr::DsrKey_6;
  453. } else if (keyCode == XK_7) {
  454. result = dsr::DsrKey_7;
  455. } else if (keyCode == XK_8) {
  456. result = dsr::DsrKey_8;
  457. } else if (keyCode == XK_9) {
  458. result = dsr::DsrKey_9;
  459. } else if (keyCode == XK_a || keyCode == XK_A) {
  460. result = dsr::DsrKey_A;
  461. } else if (keyCode == XK_b || keyCode == XK_B) {
  462. result = dsr::DsrKey_B;
  463. } else if (keyCode == XK_c || keyCode == XK_C) {
  464. result = dsr::DsrKey_C;
  465. } else if (keyCode == XK_d || keyCode == XK_D) {
  466. result = dsr::DsrKey_D;
  467. } else if (keyCode == XK_e || keyCode == XK_E) {
  468. result = dsr::DsrKey_E;
  469. } else if (keyCode == XK_f || keyCode == XK_F) {
  470. result = dsr::DsrKey_F;
  471. } else if (keyCode == XK_g || keyCode == XK_G) {
  472. result = dsr::DsrKey_G;
  473. } else if (keyCode == XK_h || keyCode == XK_H) {
  474. result = dsr::DsrKey_H;
  475. } else if (keyCode == XK_i || keyCode == XK_I) {
  476. result = dsr::DsrKey_I;
  477. } else if (keyCode == XK_j || keyCode == XK_J) {
  478. result = dsr::DsrKey_J;
  479. } else if (keyCode == XK_k || keyCode == XK_K) {
  480. result = dsr::DsrKey_K;
  481. } else if (keyCode == XK_l || keyCode == XK_L) {
  482. result = dsr::DsrKey_L;
  483. } else if (keyCode == XK_m || keyCode == XK_M) {
  484. result = dsr::DsrKey_M;
  485. } else if (keyCode == XK_n || keyCode == XK_N) {
  486. result = dsr::DsrKey_N;
  487. } else if (keyCode == XK_o || keyCode == XK_O) {
  488. result = dsr::DsrKey_O;
  489. } else if (keyCode == XK_p || keyCode == XK_P) {
  490. result = dsr::DsrKey_P;
  491. } else if (keyCode == XK_q || keyCode == XK_Q) {
  492. result = dsr::DsrKey_Q;
  493. } else if (keyCode == XK_r || keyCode == XK_R) {
  494. result = dsr::DsrKey_R;
  495. } else if (keyCode == XK_s || keyCode == XK_S) {
  496. result = dsr::DsrKey_S;
  497. } else if (keyCode == XK_t || keyCode == XK_T) {
  498. result = dsr::DsrKey_T;
  499. } else if (keyCode == XK_u || keyCode == XK_U) {
  500. result = dsr::DsrKey_U;
  501. } else if (keyCode == XK_v || keyCode == XK_V) {
  502. result = dsr::DsrKey_V;
  503. } else if (keyCode == XK_w || keyCode == XK_W) {
  504. result = dsr::DsrKey_W;
  505. } else if (keyCode == XK_x || keyCode == XK_X) {
  506. result = dsr::DsrKey_X;
  507. } else if (keyCode == XK_y || keyCode == XK_Y) {
  508. result = dsr::DsrKey_Y;
  509. } else if (keyCode == XK_z || keyCode == XK_Z) {
  510. result = dsr::DsrKey_Z;
  511. } else if (keyCode == XK_Insert) {
  512. result = dsr::DsrKey_Insert;
  513. } else if (keyCode == XK_Home) {
  514. result = dsr::DsrKey_Home;
  515. } else if (keyCode == XK_End) {
  516. result = dsr::DsrKey_End;
  517. } else if (keyCode == XK_Page_Up) {
  518. result = dsr::DsrKey_PageUp;
  519. } else if (keyCode == XK_Page_Down) {
  520. result = dsr::DsrKey_PageDown;
  521. }
  522. return result;
  523. }
  524. static dsr::DsrChar getCharacterCode(XEvent& event) {
  525. const int buffersize = 8;
  526. KeySym key; char codePoints[buffersize]; dsr::DsrChar character = '\0';
  527. if (XLookupString(&event.xkey, codePoints, buffersize, &key, 0) == 1) {
  528. // X11 does not specify any encoding, but BOM_UTF16LE seems to work on Linux.
  529. // TODO: See if there is a list of X11 character encodings for different platforms.
  530. dsr::CharacterEncoding encoding = dsr::CharacterEncoding::BOM_UTF16LE;
  531. dsr::String characterString = string_dangerous_decodeFromData(codePoints, encoding);
  532. character = characterString[0];
  533. }
  534. return character;
  535. }
  536. // Also locked, but cannot change the name when overriding
  537. void X11Window::prefetchEvents() {
  538. // Only prefetch new events if nothing else is using the communication link
  539. if (windowLock.try_lock()) {
  540. if (this->display) {
  541. bool hasScrolled = false;
  542. while (XPending(this->display)) {
  543. // Ensure that full-screen applications have keyboard focus if interacted with in any way
  544. if (this->windowState == 2) {
  545. XSetInputFocus(this->display, this->window, RevertToNone, CurrentTime);
  546. }
  547. // Get the current event
  548. XEvent currentEvent;
  549. XNextEvent(this->display, &currentEvent);
  550. // See if there's another event
  551. XEvent nextEvent;
  552. bool hasNextEvent = XPending(this->display);
  553. if (hasNextEvent) {
  554. XPeekEvent(this->display, &nextEvent);
  555. }
  556. if (currentEvent.type == Expose && currentEvent.xexpose.count == 0) {
  557. // Redraw
  558. this->queueInputEvent(new dsr::WindowEvent(dsr::WindowEventType::Redraw, this->windowWidth, this->windowHeight));
  559. } else if (currentEvent.type == KeyPress || currentEvent.type == KeyRelease) {
  560. // Key down/up
  561. dsr::DsrChar character = getCharacterCode(currentEvent);
  562. KeySym nativeKey = XLookupKeysym(&currentEvent.xkey, 0);
  563. dsr::DsrKey dsrKey = getDsrKey(nativeKey);
  564. KeySym nextNativeKey = hasNextEvent ? XLookupKeysym(&nextEvent.xkey, 0) : 0;
  565. // Distinguish between fake and physical repeats using time stamps
  566. if (hasNextEvent
  567. && currentEvent.type == KeyRelease && nextEvent.type == KeyPress
  568. && currentEvent.xkey.time == nextEvent.xkey.time
  569. && nativeKey == nextNativeKey) {
  570. // Repeated typing
  571. this->queueInputEvent(new dsr::KeyboardEvent(dsr::KeyboardEventType::KeyType, character, dsrKey));
  572. // Skip next event
  573. XNextEvent(this->display, &currentEvent);
  574. } else {
  575. if (currentEvent.type == KeyPress) {
  576. // Physical key down
  577. this->queueInputEvent(new dsr::KeyboardEvent(dsr::KeyboardEventType::KeyDown, character, dsrKey));
  578. // First press typing
  579. this->queueInputEvent(new dsr::KeyboardEvent(dsr::KeyboardEventType::KeyType, character, dsrKey));
  580. } else { // currentEvent.type == KeyRelease
  581. // Physical key up
  582. this->queueInputEvent(new dsr::KeyboardEvent(dsr::KeyboardEventType::KeyUp, character, dsrKey));
  583. }
  584. }
  585. } else if (currentEvent.type == ButtonPress || currentEvent.type == ButtonRelease) {
  586. dsr::MouseKeyEnum key = getMouseKey(currentEvent.xbutton.button);
  587. if (isVerticalScrollKey(key)) {
  588. // Scroll down/up
  589. if (!hasScrolled) {
  590. this->queueInputEvent(new dsr::MouseEvent(dsr::MouseEventType::Scroll, key, dsr::IVector2D(currentEvent.xbutton.x, currentEvent.xbutton.y)));
  591. }
  592. hasScrolled = true;
  593. } else {
  594. // Mouse down/up
  595. this->queueInputEvent(new dsr::MouseEvent(currentEvent.type == ButtonPress ? dsr::MouseEventType::MouseDown : dsr::MouseEventType::MouseUp, key, dsr::IVector2D(currentEvent.xbutton.x, currentEvent.xbutton.y)));
  596. }
  597. } else if (currentEvent.type == MotionNotify) {
  598. // Mouse move
  599. this->queueInputEvent(new dsr::MouseEvent(dsr::MouseEventType::MouseMove, dsr::MouseKeyEnum::NoKey, dsr::IVector2D(currentEvent.xmotion.x, currentEvent.xmotion.y)));
  600. } else if (currentEvent.type == ClientMessage) {
  601. // Close
  602. // Assume WM_DELETE_WINDOW since it is the only registered client message
  603. this->queueInputEvent(new dsr::WindowEvent(dsr::WindowEventType::Close, this->windowWidth, this->windowHeight));
  604. } else if (currentEvent.type == ConfigureNotify) {
  605. XConfigureEvent xce = currentEvent.xconfigure;
  606. if (this->windowWidth != xce.width || this->windowHeight != xce.height) {
  607. this->windowWidth = xce.width;
  608. this->windowHeight = xce.height;
  609. // Make a request to resize the canvas
  610. this->receivedWindowResize(xce.width, xce.height);
  611. }
  612. } else if (currentEvent.type == SelectionRequest) {
  613. // Based on: https://handmade.network/forums/articles/t/8544-implementing_copy_paste_in_x11
  614. // Another program has requested the content that you posted about in the clipboard.
  615. XSelectionRequestEvent request = currentEvent.xselectionrequest;
  616. if (XGetSelectionOwner(this->display, this->clipboardAtom) == this->window && request.selection == this->clipboardAtom) {
  617. if (request.target == this->targetsAtom && request.property != None) {
  618. XChangeProperty(request.display, request.requestor, request.property,
  619. XA_ATOM, 32, PropModeReplace, (unsigned char*)&(this->utf8StringAtom), 1);
  620. } else if (request.target == this->utf8StringAtom && request.property != None) {
  621. // Encode the data as UTF-8 with portable line-breaks, without byte order mark nor null terminator.
  622. dsr::Buffer encodedUTF8 = dsr::string_saveToMemory(this->textToClipboard, dsr::CharacterEncoding::BOM_UTF8, dsr::LineEncoding::CrLf, false, false);
  623. XChangeProperty(request.display, request.requestor, request.property,
  624. request.target, 8, PropModeReplace, dsr::buffer_dangerous_getUnsafeData(encodedUTF8), dsr::buffer_getSize(encodedUTF8));
  625. }
  626. XSelectionEvent sendEvent;
  627. sendEvent.type = SelectionNotify;
  628. sendEvent.serial = request.serial;
  629. sendEvent.send_event = request.send_event;
  630. sendEvent.display = request.display;
  631. sendEvent.requestor = request.requestor;
  632. sendEvent.selection = request.selection;
  633. sendEvent.target = request.target;
  634. sendEvent.property = request.property;
  635. sendEvent.time = request.time;
  636. XSendEvent(display, request.requestor, 0, 0, (XEvent*)&sendEvent);
  637. }
  638. } else if (currentEvent.type == SelectionNotify) {
  639. // You previously requested access to a program's clipboard content and here it is giving the data to you.
  640. // Based on: https://handmade.network/forums/articles/t/8544-implementing_copy_paste_in_x11
  641. XSelectionEvent selection = currentEvent.xselection;
  642. if (selection.property != None) {
  643. Atom actualType;
  644. int actualFormat;
  645. unsigned long bytesAfter;
  646. unsigned char* data;
  647. unsigned long count;
  648. XGetWindowProperty(this->display, this->window, this->clipboardAtom, 0, LONG_MAX, False, AnyPropertyType,
  649. &actualType, &actualFormat, &count, &bytesAfter, &data);
  650. if (selection.target == this->targetsAtom) {
  651. Atom* list = (Atom*)data;
  652. for (int i = 0; i < count; i++) {
  653. if (list[i] == XA_STRING) {
  654. this->targetAtom = XA_STRING;
  655. } else if (list[i] == this->utf8StringAtom) {
  656. this->targetAtom = this->utf8StringAtom;
  657. break;
  658. }
  659. }
  660. if (this->targetAtom != None) {
  661. XConvertSelection(this->display, this->clipboardAtom, this->targetAtom, this->clipboardAtom, this->window, CurrentTime);
  662. }
  663. } else if (selection.target == this->targetAtom) {
  664. dsr::Buffer textBuffer = dsr::buffer_create(count + 4); // Null terminate by adding zero initialized data after the copy.
  665. memcpy(buffer_dangerous_getUnsafeData(textBuffer), data, count);
  666. this->textFromClipboard = dsr::string_dangerous_decodeFromData(buffer_dangerous_getUnsafeData(textBuffer), dsr::CharacterEncoding::BOM_UTF8);
  667. // Stop waiting now that we found the data.
  668. this->loadingFromClipboard = false;
  669. }
  670. if (data) XFree(data);
  671. }
  672. }
  673. }
  674. }
  675. windowLock.unlock();
  676. }
  677. }
  678. // Locked because it overrides
  679. void X11Window::resizeCanvas(int width, int height) {
  680. windowLock.lock();
  681. if (this->display) {
  682. unsigned int defaultDepth = DefaultDepth(this->display, XDefaultScreen(this->display));
  683. // Get the old canvas
  684. dsr::AlignedImageRgbaU8 oldCanvas = this->canvas[this->showIndex];
  685. for (int b = 0; b < bufferCount; b++) {
  686. // Create a new canvas
  687. this->canvas[b] = dsr::image_create_RgbaU8_native(width, height, this->packOrderIndex);
  688. // Copy from any old canvas
  689. if (dsr::image_exists(oldCanvas)) {
  690. dsr::draw_copy(this->canvas[b], oldCanvas);
  691. }
  692. // Get a pointer to the pixels
  693. uint8_t* rawData = dsr::image_dangerous_getData(this->canvas[b]);
  694. // Create an image in XLib using the pointer
  695. // XLib takes ownership of the data
  696. this->canvasX[b] = XCreateImage(
  697. this->display, CopyFromParent, defaultDepth, ZPixmap, 0, (char*)rawData,
  698. dsr::image_getWidth(this->canvas[b]), dsr::image_getHeight(this->canvas[b]), 32, dsr::image_getStride(this->canvas[b])
  699. );
  700. // When the canvas image buffer is garbage collected, the destructor will call XLib to free the memory
  701. XImage *image = this->canvasX[b];
  702. dsr::image_dangerous_replaceDestructor(this->canvas[b], [image](uint8_t *data) { XDestroyImage(image); });
  703. }
  704. }
  705. windowLock.unlock();
  706. }
  707. X11Window::~X11Window() {
  708. #ifndef DISABLE_MULTI_THREADING
  709. // Wait for the last update of the window to finish so that it doesn't try to operate on freed resources
  710. if (this->displayFuture.valid()) {
  711. this->displayFuture.wait();
  712. }
  713. #endif
  714. windowLock.lock();
  715. if (this->display) {
  716. this->terminateClipboard();
  717. XFreeCursor(this->display, this->noCursor);
  718. XFreeGC(this->display, this->graphicsContext);
  719. XDestroyWindow(this->display, this->window);
  720. XCloseDisplay(this->display);
  721. this->display = nullptr;
  722. }
  723. windowLock.unlock();
  724. }
  725. void X11Window::showCanvas() {
  726. if (this->display) {
  727. #ifndef DISABLE_MULTI_THREADING
  728. // Wait for the previous update to finish, to avoid flooding the system with new threads waiting for windowLock
  729. if (this->displayFuture.valid()) {
  730. this->displayFuture.wait();
  731. }
  732. #endif
  733. this->drawIndex = (this->drawIndex + 1) % bufferCount;
  734. this->showIndex = (this->showIndex + 1) % bufferCount;
  735. this->prefetchEvents();
  736. int displayIndex = this->showIndex;
  737. windowLock.lock();
  738. std::function<void()> task = [this, displayIndex]() {
  739. // Clamp canvas dimensions to the target window
  740. int width = std::min(dsr::image_getWidth(this->canvas[displayIndex]), this->windowWidth);
  741. int height = std::min(dsr::image_getHeight(this->canvas[displayIndex]), this->windowHeight);
  742. // Display the result
  743. XPutImage(this->display, this->window, this->graphicsContext, this->canvasX[displayIndex], 0, 0, 0, 0, width, height);
  744. windowLock.unlock();
  745. };
  746. #ifdef DISABLE_MULTI_THREADING
  747. // Perform instantly
  748. task();
  749. #else
  750. if (this->firstFrame) {
  751. // The first frame will be cloned when double buffering.
  752. if (bufferCount == 2) {
  753. dsr::draw_copy(this->canvas[this->drawIndex], this->canvas[this->showIndex]);
  754. }
  755. // Single-thread the first frame to keep it safe.
  756. task();
  757. this->firstFrame = false;
  758. } else {
  759. // Run in the background while doing other things
  760. this->displayFuture = std::async(std::launch::async, task);
  761. }
  762. #endif
  763. }
  764. }
  765. std::shared_ptr<dsr::BackendWindow> createBackendWindow(const dsr::String& title, int width, int height) {
  766. // Check if a display is available for creating a window
  767. if (XOpenDisplay(nullptr) != nullptr) {
  768. auto backend = std::make_shared<X11Window>(title, width, height);
  769. return std::dynamic_pointer_cast<dsr::BackendWindow>(backend);
  770. } else {
  771. dsr::sendWarning("No display detected. Aborting X11 window creation.\n");
  772. return std::shared_ptr<dsr::BackendWindow>();
  773. }
  774. }