X11Window.cpp 33 KB

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