/* Copyright (c) 2017 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #define _GNU_SOURCE #include "glxshell/GLXShell.h" #include "glxshell/GLXShellCallbacks.h" #include "glxshell/GLXTarget.h" #include "shell/ShellBatteryInfo.h" #include "shell/ShellCallbacks.h" #include "shell/ShellKeyCodes.h" #include "shell/ShellThreads.h" #include "shell/Shell.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#include #include #include #include #include #define WINDOW_TITLE_BAR_HEIGHT_ESTIMATE 36 // TODO: Query window system for this (_NET_REQUEST_FRAME_EXTENTS: https://stackoverflow.com/questions/35908676/gtk-window-how-to-get-window-decoration-sizes) struct GLXShellTimer { double interval; double nextFireTime; bool repeat; bool canceled; unsigned int id; void (* callback)(unsigned int timerID, void * context); void * context; }; #if defined(__linux) #define VSYNC_DEFAULT_WINDOW false #define VSYNC_DEFAULT_FULLSCREEN true #else #define VSYNC_DEFAULT_WINDOW true #define VSYNC_DEFAULT_FULLSCREEN true #endif static Display * display; static int screen; static GLXContext context; static Window window; static Window rootWindow; static Atom deleteWindowAtom, dialogClosedAtom; static bool windowCreated; static int initialWindowX, initialWindowY; static bool redisplayNeeded; static unsigned int buttonMask = 0; static unsigned int modifiers = 0, lastModifiers = 0; static int lastMouseX, lastMouseY; static double mainLoopTargetInterval; static bool inFullScreenMode = false; static bool vsyncWindow = VSYNC_DEFAULT_WINDOW, vsyncFullscreen = VSYNC_DEFAULT_FULLSCREEN; static struct GLXShellConfiguration configuration; static unsigned int nextTimerID; static size_t timerCount; static struct GLXShellTimer * timers; static int lastWidth, lastHeight; static bool backgrounded; static bool coalesceMouseMotionEvents; static bool sendActivationClicks = false; static bool cursorHiddenByHide; static bool showCursorOnNextMouseMove; static ShellCursorID lastUnhiddenCursor = ShellCursor_arrow; static bool mouseDeltaMode; static int restoreMouseX, restoreMouseY; static int lastMouseX, lastMouseY; static int ignoreX = INT_MAX, ignoreY = INT_MAX; static bool nextKeyDownIsRepeat; static char * clipboardText; static double lastDrawTime; static char * windowTitle; static bool documentEdited; static bool runningDialog; static unsigned int screensaverCookie; static ShellMutex callbackQueueHeadMutex; static volatile unsigned int queuedCallbackHead, queuedCallbackTail; #define QUEUED_CALLBACK_MAX 500 static struct { void (* callback)(void * context); void * context; } queuedCallbacks[QUEUED_CALLBACK_MAX]; static int pselectPipe[2]; #define CUSTOM_CURSOR_ID_OFFSET 1000 static unsigned int customCursorCount; static struct { unsigned int representationCount; struct { float scaleFactor; Cursor cursor; } * representations; } * customCursors; // From dbus/dbus.h (don't want to include) typedef struct DBusConnection DBusConnection; typedef struct DBusError DBusError; typedef struct DBusMessage DBusMessage; typedef uint32_t dbus_uint32_t; typedef dbus_uint32_t dbus_bool_t; typedef enum { DBUS_BUS_SESSION, /**< The login session bus */ DBUS_BUS_SYSTEM, /**< The systemwide bus */ DBUS_BUS_STARTER /**< The bus that started us, if any */ } DBusBusType; struct DBusError { const char *name; /**< public error name field */ const char *message; /**< public error message field */ unsigned int dummy1 : 1; /**< placeholder */ unsigned int dummy2 : 1; /**< placeholder */ unsigned int dummy3 : 1; /**< placeholder */ unsigned int dummy4 : 1; /**< placeholder */ unsigned int dummy5 : 1; /**< placeholder */ void *padding1; /**< placeholder */ }; #define DBUS_TYPE_INVALID ((int) '\0') #define DBUS_TYPE_ARRAY ((int) 'a') #define DBUS_TYPE_UINT32 ((int) 'u') #define DBUS_TYPE_STRING ((int) 's') #define DBUS_LOAD_STATUS_UNINITIALIZED 0 #define DBUS_LOAD_STATUS_INITIALIZING 1 #define DBUS_LOAD_STATUS_SUCCEEDED 2 #define DBUS_LOAD_STATUS_FAILED 3 static unsigned int dbusLoadStatus = DBUS_LOAD_STATUS_UNINITIALIZED; static struct { DBusConnection * connection; DBusConnection * (* bus_get_private)(DBusBusType, DBusError *); DBusMessage * (* message_new_method_call)(const char *, const char *, const char *, const char *); dbus_bool_t (* message_append_args_valist)(DBusMessage *, int, va_list); void (* connection_set_exit_on_disconnect)(DBusConnection *, dbus_bool_t); dbus_bool_t (* connection_send)(DBusConnection *, DBusMessage *, dbus_uint32_t *); DBusMessage * (* connection_send_with_reply_and_block)(DBusConnection *, DBusMessage *, int, DBusError *); void (* connection_close)(DBusConnection *); void (* connection_unref)(DBusConnection *); void (* connection_flush)(DBusConnection *); dbus_bool_t (* message_get_args_valist)(DBusMessage *, DBusError *, int, va_list); void (* message_unref)(DBusMessage *); dbus_bool_t (* threads_init_default)(void); void (* error_init)(DBusError *); dbus_bool_t (* error_is_set)(const DBusError *); void (* error_free)(DBusError *); } dbusInterface; static void setEmptyCursor() { static Cursor emptyCursor = None; if (emptyCursor == None) { char emptyBits[32]; XColor color; Pixmap pixmap; memset(emptyBits, 0, sizeof(emptyBits)); memset(&color, 0, sizeof(color)); pixmap = XCreateBitmapFromData(display, window, emptyBits, 16, 16); if (pixmap != None) { emptyCursor = XCreatePixmapCursor(display, pixmap, pixmap, &color, &color, 0, 0); XFreePixmap(display, pixmap); } } XDefineCursor(display, window, emptyCursor); } static void setShellCursor(ShellCursorID cursor) { static struct {unsigned int name; Cursor cursor;} cursors[] = { {XC_top_left_arrow, None}, // ShellCursor_arrow {XC_xterm, None}, // ShellCursor_iBeam {XC_crosshair, None}, // ShellCursor_crosshair {XC_hand2, None}, // ShellCursor_hand {XC_watch, None} // ShellCursor_wait }; if (cursor >= CUSTOM_CURSOR_ID_OFFSET) { cursor -= CUSTOM_CURSOR_ID_OFFSET; if ((unsigned int) cursor >= customCursorCount) { return; } float scaleFactor = Shell_getDisplayScaleFactor(Shell_getDisplayIndexFromWindow()); unsigned int bestRepresentationIndex = 0; float bestScaleFactor = 0.0f; for (unsigned int representationIndex = 0; representationIndex < customCursors[cursor].representationCount; representationIndex++) { if (customCursors[cursor].representations[representationIndex].scaleFactor > bestScaleFactor && customCursors[cursor].representations[representationIndex].scaleFactor <= scaleFactor) { bestRepresentationIndex = representationIndex; bestScaleFactor = customCursors[cursor].representations[representationIndex].scaleFactor; } } XDefineCursor(display, window, customCursors[cursor].representations[bestRepresentationIndex].cursor); } else { if ((unsigned int) cursor > ShellCursor_wait) { return; } // XC_top_left_arrow doesn't match the default arrow cursor, so use None instead (which is equivalent to calling XUndefineCursor) if (cursors[cursor].cursor == None && cursor != ShellCursor_arrow) { cursors[cursor].cursor = XCreateFontCursor(display, cursors[cursor].name); } XDefineCursor(display, window, cursors[cursor].cursor); } } static void warpPointerAndIgnoreEvent(int x, int y) { ignoreX = x; ignoreY = y; XWarpPointer(display, None, window, 0, 0, 0, 0, x, y); XSync(display, False); } static unsigned int xkeyCodeToShellKeyCode(unsigned int keyCode) { switch (keyCode) { case 9: return KEY_CODE_ESCAPE; case 67: return KEY_CODE_F1; case 68: return KEY_CODE_F2; case 69: return KEY_CODE_F3; case 70: return KEY_CODE_F4; case 71: return KEY_CODE_F5; case 72: return KEY_CODE_F6; case 73: return KEY_CODE_F7; case 74: return KEY_CODE_F8; case 75: return KEY_CODE_F9; case 76: return KEY_CODE_F10; case 95: return KEY_CODE_F11; case 96: return KEY_CODE_F12; case 107: return KEY_CODE_PRINT_SCREEN; case 78: return KEY_CODE_SCROLL_LOCK; case 127: return KEY_CODE_PAUSE; case 49: return KEY_CODE_BACKTICK; case 10: return KEY_CODE_1; case 11: return KEY_CODE_2; case 12: return KEY_CODE_3; case 13: return KEY_CODE_4; case 14: return KEY_CODE_5; case 15: return KEY_CODE_6; case 16: return KEY_CODE_7; case 17: return KEY_CODE_8; case 18: return KEY_CODE_9; case 19: return KEY_CODE_0; case 20: return KEY_CODE_MINUS; case 21: return KEY_CODE_EQUAL; case 22: return KEY_CODE_BACKSPACE; case 23: return KEY_CODE_TAB; case 24: return KEY_CODE_Q; case 25: return KEY_CODE_W; case 26: return KEY_CODE_E; case 27: return KEY_CODE_R; case 28: return KEY_CODE_T; case 29: return KEY_CODE_Y; case 30: return KEY_CODE_U; case 31: return KEY_CODE_I; case 32: return KEY_CODE_O; case 33: return KEY_CODE_P; case 34: return KEY_CODE_OPEN_BRACKET; case 35: return KEY_CODE_CLOSE_BRACKET; case 51: return KEY_CODE_BACKSLASH; case 66: return KEY_CODE_CAPS_LOCK; case 38: return KEY_CODE_A; case 39: return KEY_CODE_S; case 40: return KEY_CODE_D; case 41: return KEY_CODE_F; case 42: return KEY_CODE_G; case 43: return KEY_CODE_H; case 44: return KEY_CODE_J; case 45: return KEY_CODE_K; case 46: return KEY_CODE_L; case 47: return KEY_CODE_SEMICOLON; case 48: return KEY_CODE_SINGLE_QUOTE; case 36: return KEY_CODE_ENTER; case 50: return KEY_CODE_LEFT_SHIFT; case 52: return KEY_CODE_Z; case 53: return KEY_CODE_X; case 54: return KEY_CODE_C; case 55: return KEY_CODE_V; case 56: return KEY_CODE_B; case 57: return KEY_CODE_N; case 58: return KEY_CODE_M; case 59: return KEY_CODE_COMMA; case 60: return KEY_CODE_PERIOD; case 61: return KEY_CODE_SLASH; case 62: return KEY_CODE_RIGHT_SHIFT; case 37: return KEY_CODE_LEFT_CONTROL; case 133: return KEY_CODE_LEFT_GUI; case 64: return KEY_CODE_LEFT_ALT; case 65: return KEY_CODE_SPACE; case 108: return KEY_CODE_RIGHT_ALT; case 134: return KEY_CODE_RIGHT_GUI; case 135: return KEY_CODE_MENU; case 105: return KEY_CODE_RIGHT_CONTROL; case 113: return KEY_CODE_LEFT_ARROW; case 114: return KEY_CODE_RIGHT_ARROW; case 111: return KEY_CODE_UP_ARROW; case 116: return KEY_CODE_DOWN_ARROW; case 118: return KEY_CODE_INSERT; case 110: return KEY_CODE_HOME; case 112: return KEY_CODE_PAGE_UP; case 119: return KEY_CODE_FORWARD_DELETE; case 115: return KEY_CODE_END; case 117: return KEY_CODE_PAGE_DOWN; case 77: return KEY_CODE_NUM_LOCK; case 125: return KEY_CODE_NUMPAD_EQUAL; case 106: return KEY_CODE_NUMPAD_SLASH; case 63: return KEY_CODE_NUMPAD_ASTERISK; case 82: return KEY_CODE_NUMPAD_MINUS; case 79: return KEY_CODE_NUMPAD_7; case 80: return KEY_CODE_NUMPAD_8; case 81: return KEY_CODE_NUMPAD_9; case 86: return KEY_CODE_NUMPAD_PLUS; case 83: return KEY_CODE_NUMPAD_4; case 84: return KEY_CODE_NUMPAD_5; case 85: return KEY_CODE_NUMPAD_6; case 87: return KEY_CODE_NUMPAD_1; case 88: return KEY_CODE_NUMPAD_2; case 89: return KEY_CODE_NUMPAD_3; case 90: return KEY_CODE_NUMPAD_0; case 91: return KEY_CODE_NUMPAD_PERIOD; case 104: return KEY_CODE_NUMPAD_ENTER; #ifdef DEBUG default: fprintf(stderr, "Warning: Unknown key code %u\n", keyCode); #endif } return 0; } /* static unsigned int xkeyStateToShellModifiers(unsigned int xkeyState) { unsigned int state = 0; if (xkeyState & ShiftMask) { state |= MODIFIER_SHIFT_BIT; } if (xkeyState & ControlMask) { state |= MODIFIER_CONTROL_BIT; } if (xkeyState & LockMask) { state |= MODIFIER_CAPS_LOCK_BIT; } if (xkeyState & Mod1Mask) { state |= MODIFIER_ALT_BIT; } return state; } */ static void updateKeyModifierState(void) { char keys[32]; XQueryKeymap(display, keys); bool leftShift = keys[6] & 0x4; bool rightShift = keys[7] & 0x40; bool leftCtrl = keys[4] & 0x20; bool rightCtrl = keys[13] & 0x2; bool leftAlt = keys[8] & 0x1; bool rightAlt = keys[13] & 0x10; if (leftShift || rightShift) { modifiers |= MODIFIER_SHIFT_BIT; } else { modifiers &= ~MODIFIER_SHIFT_BIT; } if (leftCtrl || rightCtrl) { modifiers |= MODIFIER_CONTROL_BIT; } else { modifiers &= ~MODIFIER_CONTROL_BIT; } if (leftAlt || rightAlt) { modifiers |= MODIFIER_ALT_BIT; } else { modifiers &= ~MODIFIER_ALT_BIT; } if (lastModifiers != modifiers) { if (keyModifiersChangedCallback != NULL) { keyModifiersChangedCallback(modifiers, lastModifiers, Shell_getCurrentTime()); } lastModifiers = modifiers; } } static void sleepEventLoop(double currentTime, bool coordinateWithTimers) { fd_set fdset; int socket; struct timespec wait; socket = ConnectionNumber(display); FD_ZERO(&fdset); FD_SET(socket, &fdset); FD_SET(pselectPipe[0], &fdset); if (coordinateWithTimers) { double closestNextFireTime = 1000.0; for (unsigned int timerIndex = 0; timerIndex < timerCount; timerIndex++) { if (timers[timerIndex].nextFireTime - currentTime < closestNextFireTime) { closestNextFireTime = timers[timerIndex].nextFireTime - currentTime; } } if (closestNextFireTime < 0.0) { closestNextFireTime = 0.0; } wait.tv_sec = closestNextFireTime; wait.tv_nsec = fmod(closestNextFireTime, 1.0) * 1000000000; } else { wait.tv_sec = INT_MAX; wait.tv_nsec = 0; } sigset_t emptyset; sigemptyset(&emptyset); int nfds = socket + 1; if (pselectPipe[0] + 1 > nfds) { nfds = pselectPipe[0] + 1; } pselect(nfds, &fdset, NULL, NULL, &wait, &emptyset); } #define MOUSE_BUTTON_NUMBER_SCROLL_WHEEL_UP 3 #define MOUSE_BUTTON_NUMBER_SCROLL_WHEEL_DOWN 4 static unsigned int translateXButtonToShellButtonIndex(unsigned int xbutton) { if (xbutton == 2) { return MOUSE_BUTTON_NUMBER_MIDDLE; } if (xbutton == 3) { return MOUSE_BUTTON_NUMBER_RIGHT; } return xbutton - 1; } static void processMouseMoveEvent(XEvent event) { int reportedX, reportedY, reportedDeltaX, reportedDeltaY; if (event.xbutton.x == lastMouseX && event.xbutton.y == lastMouseY) { return; } if (event.xbutton.x == ignoreX && event.xbutton.y == ignoreY) { lastMouseX = ignoreX; lastMouseY = ignoreY; return; } if (showCursorOnNextMouseMove) { setShellCursor(lastUnhiddenCursor); } if (mouseDeltaMode) { reportedX = restoreMouseX; reportedY = restoreMouseY; reportedDeltaX = event.xbutton.x - lastMouseX; reportedDeltaY = event.xbutton.y - lastMouseY; } else { reportedX = event.xbutton.x; reportedY = event.xbutton.y; reportedDeltaX = reportedX - lastMouseX; reportedDeltaY = reportedY - lastMouseY; lastMouseX = event.xbutton.x; lastMouseY = event.xbutton.y; } ignoreX = ignoreY = INT_MAX; if (buttonMask != 0) { if (mouseDraggedCallback != NULL) { mouseDraggedCallback(buttonMask, reportedX, reportedY, reportedDeltaX, reportedDeltaY, modifiers, event.xbutton.time / 1000.0); } } else { if (mouseMovedCallback != NULL) { mouseMovedCallback(reportedX, reportedY, reportedDeltaX, reportedDeltaY, modifiers, event.xbutton.time / 1000.0); } } if (mouseDeltaMode) { XWindowAttributes windowAttributes; XGetWindowAttributes(display, window, &windowAttributes); warpPointerAndIgnoreEvent(windowAttributes.width / 2, windowAttributes.height / 2); } } void Shell_mainLoop(void) { XEvent event, lastMouseMoveEvent = {.type = 0}; XWindowAttributes windowAttributes; char charCode; unsigned int keyCode; KeySym keySym; char keys[32]; //Time lastKeyDownEventTime = 0, lastKeyUpEventTime = 0; //unsigned int lastKeyDownEventKeyCode = 0, lastKeyUpEventKeyCode = 0; bool frameExtentsSet = true;//false; bool extraRedisplayPosted = false; double loopStartTime = Shell_getCurrentTime(); double lastFocusInTime = 0.0; for (;;) { while (XPending(display)) { XNextEvent(display, &event); if (coalesceMouseMotionEvents && event.type == MotionNotify) { lastMouseMoveEvent = event; continue; } if (lastMouseMoveEvent.type == MotionNotify) { processMouseMoveEvent(lastMouseMoveEvent); lastMouseMoveEvent.type = 0; } switch (event.type) { case Expose: Shell_redisplay(); /* fall through */ case ConfigureNotify: if (!frameExtentsSet) { // Some sources claim XCreateWindow specifies content rect rather than frame rect, // but observed behavior on Ubuntu 20.10 Unity was that it specifies frame rect. // Have to wait for window manager to asynchronously set _NET_FRAME_EXTENTS, then // adjust by that amount to get content rect sizing. Atom actualType; int actualFormat; unsigned long itemCountReturned = 0, itemCountUnread = 0; unsigned long frameExtentsArray[4] = {0, 0, 0, 0}; unsigned long * frameExtents = frameExtentsArray; if (XGetWindowProperty(display, window, XInternAtom(display, "_NET_FRAME_EXTENTS", False), 0, 4, False, AnyPropertyType, &actualType, &actualFormat, &itemCountReturned, &itemCountUnread, (unsigned char **) &frameExtents) == Success) { XMoveWindow(display, window, initialWindowX - frameExtents[0], initialWindowY - frameExtents[2]); frameExtentsSet = true; } } XGetWindowAttributes(display, window, &windowAttributes); if (windowAttributes.width != lastWidth || windowAttributes.height != lastHeight) { lastWidth = windowAttributes.width; lastHeight = windowAttributes.height; if (resizeCallback != NULL) { resizeCallback(windowAttributes.width, windowAttributes.height, Shell_getCurrentTime()); } Shell_redisplay(); } break; case KeyPress: { if (runningDialog) { break; } //if (event.xkey.time == lastKeyDownEventTime && event.xkey.keycode == lastKeyDownEventKeyCode) { // Occasionally X11 seems to send duplicate key events; debounce //printf("would have debounced key press (%lu, %u)\n", event.xkey.time, xkeyCodeToShellKeyCode(event.xkey.keycode)); //lastDebounceKeyDownTime = event.xkey.time; //break; //} if (!XLookupString(&event.xkey, &charCode, 1, &keySym, NULL)) { charCode = 0; } keyCode = xkeyCodeToShellKeyCode(event.xkey.keycode); bool capsWasOn = !!(modifiers & MODIFIER_CAPS_LOCK_BIT); switch (keyCode) { case KEY_CODE_CAPS_LOCK: if (!capsWasOn) { modifiers |= MODIFIER_CAPS_LOCK_BIT; } break; case KEY_CODE_LEFT_SHIFT: case KEY_CODE_RIGHT_SHIFT: modifiers |= MODIFIER_SHIFT_BIT; break; case KEY_CODE_LEFT_CONTROL: case KEY_CODE_RIGHT_CONTROL: modifiers |= MODIFIER_CONTROL_BIT; break; case KEY_CODE_LEFT_ALT: case KEY_CODE_RIGHT_ALT: modifiers |= MODIFIER_ALT_BIT; break; } if (keyCode != 0) { if (keyDownCallback != NULL) { keyDownCallback(charCode, keyCode, modifiers, nextKeyDownIsRepeat, event.xkey.time / 1000.0); } nextKeyDownIsRepeat = false; } if (lastModifiers != modifiers) { if (keyModifiersChangedCallback != NULL) { keyModifiersChangedCallback(modifiers, lastModifiers, event.xkey.time / 1000.0); } lastModifiers = modifiers; } //lastKeyDownEventTime = event.xkey.time; //lastKeyDownEventKeyCode = event.xkey.keycode; break; } case KeyRelease: { if (runningDialog) { break; } //if (event.xkey.time == lastKeyUpEventTime && event.xkey.keycode == lastKeyUpEventKeyCode) { // Occasionally X11 seems to send duplicate key events; debounce //printf("would have debounced key release (%lu, %u)\n", event.xkey.time, xkeyCodeToShellKeyCode(event.xkey.keycode)); //break; //} if (event.xkey.keycode > 255) { break; } XQueryKeymap(display, keys); if (keys[event.xkey.keycode >> 3] & 1 << (event.xkey.keycode % 8)) { // Xlib sends key ups for key repeats; this one is a repeat, so skip it and mark the next keyDown as a repeat nextKeyDownIsRepeat = true; break; } keyCode = xkeyCodeToShellKeyCode(event.xkey.keycode); bool capsWasOn = !!(modifiers & MODIFIER_CAPS_LOCK_BIT); switch (keyCode) { case KEY_CODE_CAPS_LOCK: if (capsWasOn) { modifiers &= ~MODIFIER_CAPS_LOCK_BIT; } break; case KEY_CODE_LEFT_SHIFT: case KEY_CODE_RIGHT_SHIFT: modifiers &= ~MODIFIER_SHIFT_BIT; break; case KEY_CODE_LEFT_CONTROL: case KEY_CODE_RIGHT_CONTROL: modifiers &= ~MODIFIER_CONTROL_BIT; break; case KEY_CODE_LEFT_ALT: case KEY_CODE_RIGHT_ALT: modifiers &= ~MODIFIER_ALT_BIT; break; } if (keyCode != 0) { if (keyUpCallback != NULL) { keyUpCallback(keyCode, modifiers, event.xkey.time / 1000.0); } } if (lastModifiers != modifiers) { if (keyModifiersChangedCallback != NULL) { keyModifiersChangedCallback(modifiers, lastModifiers, event.xkey.time / 1000.0); } lastModifiers = modifiers; } //lastKeyUpEventTime = event.xkey.time; //lastKeyUpEventKeyCode = event.xkey.keycode; break; } case ButtonPress: if (event.xbutton.button - 1 == MOUSE_BUTTON_NUMBER_SCROLL_WHEEL_UP) { if (scrollWheelCallback != NULL) { scrollWheelCallback(event.xbutton.x, event.xbutton.y, 0, -1, modifiers, event.xbutton.time / 1000.0); } } else if (event.xbutton.button - 1 == MOUSE_BUTTON_NUMBER_SCROLL_WHEEL_DOWN) { if (scrollWheelCallback != NULL) { scrollWheelCallback(event.xbutton.x, event.xbutton.y, 0, 1, modifiers, event.xbutton.time / 1000.0); } } else { double timestamp = event.xbutton.time / 1000.0; if (!sendActivationClicks && timestamp <= lastFocusInTime) { break; } unsigned int shellButtonIndex = translateXButtonToShellButtonIndex(event.xbutton.button); buttonMask |= 1 << shellButtonIndex; if (mouseDownCallback != NULL) { mouseDownCallback(shellButtonIndex, buttonMask, event.xbutton.x, event.xbutton.y, modifiers, timestamp); } } break; case ButtonRelease: if (event.xbutton.button - 1 != MOUSE_BUTTON_NUMBER_SCROLL_WHEEL_UP && event.xbutton.button - 1 != MOUSE_BUTTON_NUMBER_SCROLL_WHEEL_DOWN) { unsigned int shellButtonIndex = translateXButtonToShellButtonIndex(event.xbutton.button); buttonMask &= ~(1 << shellButtonIndex); if (mouseUpCallback != NULL) { mouseUpCallback(shellButtonIndex, buttonMask, event.xbutton.x, event.xbutton.y, modifiers, event.xbutton.time / 1000.0); } } break; case MotionNotify: processMouseMoveEvent(event); break; case LeaveNotify: if (buttonMask == 0 && mouseLeaveCallback != NULL) { mouseLeaveCallback(modifiers, event.xcrossing.time / 1000.0); } break; case FocusIn: updateKeyModifierState(); if (backgrounded && (event.xfocus.mode != NotifyGrab || event.xfocus.detail != NotifyPointer)) { backgrounded = false; double timestamp = Shell_getCurrentTime(); lastFocusInTime = timestamp; if (foregroundedCallback != NULL) { foregroundedCallback(timestamp); } } break; case FocusOut: if (event.xfocus.mode != NotifyGrab && event.xfocus.mode != NotifyUngrab) { backgrounded = true; if (backgroundedCallback != NULL) { backgroundedCallback(Shell_getCurrentTime()); } } break; case SelectionClear: free(clipboardText); clipboardText = NULL; break; case SelectionRequest: { XSelectionEvent selectionEvent = { .type = SelectionNotify, .requestor = event.xselectionrequest.requestor, .selection = event.xselectionrequest.selection, .target = event.xselectionrequest.target, .time = event.xselectionrequest.time }; Atom utf8Atom = XInternAtom(display, "UTF8_STRING", False); if (event.xselectionrequest.target != utf8Atom || event.xselectionrequest.property == None) { selectionEvent.property = None; } else { selectionEvent.property = event.xselectionrequest.property, XChangeProperty(display, event.xselectionrequest.requestor, event.xselectionrequest.property, utf8Atom, 8, PropModeReplace, (unsigned char *) clipboardText, strlen(clipboardText)); } XSendEvent(display, event.xselectionrequest.requestor, True, NoEventMask, (XEvent *) &selectionEvent); break; } case ClientMessage: if (event.xclient.message_type == dialogClosedAtom) { runningDialog = false; } else if ((Atom) event.xclient.data.l[0] == deleteWindowAtom) { if (confirmQuitCallback != NULL && !confirmQuitCallback(Shell_getCurrentTime())) { XRaiseWindow(display, window); break; } glXMakeCurrent(display, None, NULL); glXDestroyContext(display, context); XDestroyWindow(display, window); XCloseDisplay(display); exit(EXIT_SUCCESS); } break; } } if (lastMouseMoveEvent.type == MotionNotify) { processMouseMoveEvent(lastMouseMoveEvent); lastMouseMoveEvent.type = 0; } bool neededRedisplayBeforeTimers = redisplayNeeded; double currentTime = Shell_getCurrentTime(); for (unsigned int timerIndex = 0; timerIndex < timerCount; timerIndex++) { if (!timers[timerIndex].canceled && currentTime >= timers[timerIndex].nextFireTime) { timers[timerIndex].callback(timers[timerIndex].id, timers[timerIndex].context); if (timers[timerIndex].repeat) { timers[timerIndex].nextFireTime += timers[timerIndex].interval; } else { timers[timerIndex].canceled = true; } } if (timers[timerIndex].canceled) { timerCount--; for (unsigned int timerIndex2 = timerIndex; timerIndex2 < timerCount; timerIndex2++) { timers[timerIndex2] = timers[timerIndex2 + 1]; } timerIndex--; } } while (queuedCallbackTail != queuedCallbackHead) { queuedCallbacks[queuedCallbackTail].callback(queuedCallbacks[queuedCallbackTail].context); queuedCallbackTail = (queuedCallbackTail + 1) % QUEUED_CALLBACK_MAX; unsigned char byte; read(pselectPipe[0], &byte, 1); } currentTime = Shell_getCurrentTime(); if (redisplayNeeded) { if (mainLoopTargetInterval > 0.0) { double timeAllocationUsed = fmod(currentTime - loopStartTime, mainLoopTargetInterval); struct timespec sleepTimeSpec = {.tv_sec = 0, .tv_nsec = (mainLoopTargetInterval - timeAllocationUsed) * 1000000000}; struct timespec elapsedTimeSpec = sleepTimeSpec; nanosleep(&sleepTimeSpec, &elapsedTimeSpec); } if (lastDrawTime == 0.0 || !neededRedisplayBeforeTimers) { lastDrawTime = currentTime; } redisplayNeeded = false; if (drawCallback != NULL && drawCallback(currentTime, currentTime - lastDrawTime)) { glXSwapBuffers(display, window); if (redisplayNeeded) { extraRedisplayPosted = false; } else if (!extraRedisplayPosted) { Shell_redisplay(); extraRedisplayPosted = true; } else { extraRedisplayPosted = false; } } lastDrawTime = currentTime; #ifdef DEBUG GLenum error = glGetError(); while (error != GL_NO_ERROR) { fprintf(stderr, "GL error: %s\n", gluErrorString(error)); error = glGetError(); } #endif } currentTime = Shell_getCurrentTime(); if (!redisplayNeeded && !XPending(display)) { sleepEventLoop(currentTime, timerCount > 0); } } } void Shell_redisplay(void) { if (!redisplayNeeded) { lastDrawTime = Shell_getCurrentTime(); } redisplayNeeded = true; } bool Shell_needsDisplay(void) { return redisplayNeeded; } unsigned int Shell_getDisplayCount(void) { return ScreenCount(display); } unsigned int Shell_getDisplayIndexFromWindow(void) { return 0; } void Shell_getDisplayBounds(unsigned int displayIndex, int * outOffsetX, int * outOffsetY, unsigned int * outWidth, unsigned int * outHeight) { if (outOffsetX != NULL) { *outOffsetX = 0; } if (outOffsetY != NULL) { *outOffsetY = 0; } if (outWidth != NULL) { *outWidth = DisplayWidth(display, screen); } if (outHeight != NULL) { *outHeight = DisplayHeight(display, screen); } } double Shell_getDisplayRefreshRate(unsigned int displayIndex) { // Note: XRRGetScreenInfo is extremely slow! Cache the result of this function or otherwise avoid calling it if possible XRRScreenConfiguration * configuration = XRRGetScreenInfo(display, rootWindow); double refreshRate = XRRConfigCurrentRate(configuration); XRRFreeScreenConfigInfo(configuration); return refreshRate; } float Shell_getDisplayScaleFactor(unsigned int displayIndex) { return gdk_screen_get_monitor_scale_factor(gdk_display_get_screen(gdk_display_get_default(), displayIndex), 0); } #define INCHES_PER_MILLIMETER 0.03937007874 float Shell_getDisplayDPI(unsigned int displayIndex) { float monitorHeightInches = gdk_screen_get_monitor_height_mm(gdk_display_get_screen(gdk_display_get_default(), displayIndex), 0) * INCHES_PER_MILLIMETER; if (monitorHeightInches <= 0.0f) { return 72.0f; } return DisplayHeight(display, screen) / monitorHeightInches; } static bool getWindowCardinalProperty(Display * display, Window window, const char * propertyName, unsigned long * outValue) { Atom actualType; int actualFormat; unsigned long dataSize, dataSizeUnread; unsigned char * data; int result = XGetWindowProperty(display, window, XInternAtom(display, propertyName, False), 0, LONG_MAX, False, XA_CARDINAL, &actualType, &actualFormat, &dataSize, &dataSizeUnread, &data); if (result == Success && actualType == XA_CARDINAL) { *outValue = *(unsigned long *) data; XFree(data); return true; } return false; } void Shell_getSafeWindowRect(unsigned int displayIndex, int * outOffsetX, int * outOffsetY, unsigned int * outWidth, unsigned int * outHeight) { Atom workareaAtom = XInternAtom(display, "_NET_WORKAREA", False); Window rootWindow = XRootWindow(display, displayIndex); Atom actualType; int actualFormat; unsigned long dataSize, dataSizeUnread; unsigned char * data; int result = XGetWindowProperty(display, rootWindow, workareaAtom, 0, LONG_MAX, False, AnyPropertyType, &actualType, &actualFormat, &dataSize, &dataSizeUnread, &data); int x = 0; int y = 0; unsigned int width = DisplayWidth(display, screen); unsigned int height = DisplayHeight(display, screen); if (result == Success && actualType != None && actualFormat != 0 && dataSizeUnread == 0 && dataSize % 4 == 0) { unsigned long currentDesktop = 0; getWindowCardinalProperty(display, rootWindow, "_NET_CURRENT_DESKTOP", ¤tDesktop); if (currentDesktop < dataSize / 4) { long * workareas = (long *) data; x = workareas[currentDesktop * 4]; y = workareas[currentDesktop * 4 + 1]; width = workareas[currentDesktop * 4 + 2]; height = workareas[currentDesktop * 4 + 3]; } } height -= WINDOW_TITLE_BAR_HEIGHT_ESTIMATE * Shell_getDisplayScaleFactor(0); if (outOffsetX != NULL) { *outOffsetX = x; } if (outOffsetY != NULL) { *outOffsetY = y; } if (outWidth != NULL) { *outWidth = width; } if (outHeight != NULL) { *outHeight = height; } } bool Shell_isFullScreen(void) { return inFullScreenMode; } #define loadDBusSymbol2(symbol, dbus_symbol) \ dbusInterface.symbol = dlsym(dbusLibrary, #dbus_symbol); \ if (dbusInterface.symbol == NULL) { \ dbusLoadStatus = DBUS_LOAD_STATUS_FAILED; \ return; \ } #define loadDBusSymbol(symbol) loadDBusSymbol2(symbol, dbus_##symbol) static void initDBus(void) { if (dbusLoadStatus == DBUS_LOAD_STATUS_UNINITIALIZED) { dbusLoadStatus = DBUS_LOAD_STATUS_INITIALIZING; void * dbusLibrary = dlopen("libdbus-1.so.3", RTLD_NOW | RTLD_LOCAL); if (dbusLibrary == NULL) { dbusLoadStatus = DBUS_LOAD_STATUS_FAILED; return; } loadDBusSymbol(bus_get_private); loadDBusSymbol(message_new_method_call); loadDBusSymbol(message_append_args_valist); loadDBusSymbol(connection_set_exit_on_disconnect); loadDBusSymbol(connection_send); loadDBusSymbol(connection_send_with_reply_and_block); loadDBusSymbol(connection_close); loadDBusSymbol(connection_unref); loadDBusSymbol(connection_flush); loadDBusSymbol(message_get_args_valist); loadDBusSymbol(message_unref); loadDBusSymbol(threads_init_default); loadDBusSymbol(error_init); loadDBusSymbol(error_is_set); loadDBusSymbol(error_free); if (!dbusInterface.threads_init_default()) { dbusLoadStatus = DBUS_LOAD_STATUS_FAILED; return; } DBusError error; dbusInterface.error_init(&error); dbusInterface.connection = dbusInterface.bus_get_private(DBUS_BUS_SESSION, &error); if (dbusInterface.error_is_set(&error)) { dbusInterface.error_free(&error); dbusLoadStatus = DBUS_LOAD_STATUS_FAILED; return; } dbusInterface.connection_set_exit_on_disconnect(dbusInterface.connection, 0); dbusInterface.error_free(&error); dbusLoadStatus = DBUS_LOAD_STATUS_SUCCEEDED; } } static bool callDBusMethodInternal(DBusConnection * connection, const char * node, const char * path, const char * interface, const char * method, va_list argsMessage) { bool success = false; if (connection != NULL) { DBusMessage * message = dbusInterface.message_new_method_call(node, path, interface, method); if (message != NULL) { va_list argsReply; va_copy(argsReply, argsMessage); int argument = va_arg(argsMessage, int); if (argument == DBUS_TYPE_INVALID || dbusInterface.message_append_args_valist(message, argument, argsMessage)) { DBusMessage * reply = dbusInterface.connection_send_with_reply_and_block(connection, message, 300, NULL); if (reply != NULL) { while ((argument = va_arg(argsReply, int)) != DBUS_TYPE_INVALID) { void * discard = va_arg(argsReply, void *); (void) discard; if (argument == DBUS_TYPE_ARRAY) { int discard = va_arg(argsReply, int); (void) discard; } } argument = va_arg(argsReply, int); if (argument == DBUS_TYPE_INVALID || dbusInterface.message_get_args_valist(reply, NULL, argument, argsReply)) { success = true; } dbusInterface.message_unref(reply); } } va_end(argsReply); dbusInterface.message_unref(message); } } return success; } static bool callDBusMethod(const char * node, const char * path, const char * interface, const char * method, ...) { va_list args; va_start(args, method); bool success = callDBusMethodInternal(dbusInterface.connection, node, path, interface, method, args); va_end(args); return success; } static bool callDBusVoidMethodInternal(DBusConnection * connection, const char * node, const char * path, const char * interface, const char * method, va_list argsMessage) { bool success = false; if (connection != NULL) { DBusMessage * message = dbusInterface.message_new_method_call(node, path, interface, method); if (message != NULL) { int argument = va_arg(argsMessage, int); if (argument == DBUS_TYPE_INVALID || dbusInterface.message_append_args_valist(message, argument, argsMessage)) { if (dbusInterface.connection_send(connection, message, NULL)) { dbusInterface.connection_flush(connection); success = true; } } dbusInterface.message_unref(message); } } return success; } static bool callDBusVoidMethod(const char * node, const char * path, const char * interface, const char * method, ...) { va_list args; va_start(args, method); bool success = callDBusVoidMethodInternal(dbusInterface.connection, node, path, interface, method, args); va_end(args); return success; } void Shell_setDisplaySleepEnabled(bool enabled) { // This appears not to work; have to use dbus instead /*if (enabled && screenSaverSuspended) { XScreenSaverSuspend(display, False); XResetScreenSaver(display); screenSaverSuspended = false; } else if (!enabled && !screenSaverSuspended) { XScreenSaverSuspend(display, True); XResetScreenSaver(display); screenSaverSuspended = true; }*/ initDBus(); if (dbusLoadStatus != DBUS_LOAD_STATUS_SUCCEEDED) { #ifdef DEBUG if (!enabled) { fprintf(stderr, "Warning: Couldn't init dbus interface; display sleep cannot be disabled\n"); } #endif return; } bool alreadyEnabled = screensaverCookie == 0; if (alreadyEnabled == enabled) { return; } const char * node = "org.freedesktop.ScreenSaver"; const char * path = "/org/freedesktop/ScreenSaver"; const char * interface = "org.freedesktop.ScreenSaver"; if (enabled) { callDBusVoidMethod(node, path, interface, "UnInhibit", DBUS_TYPE_UINT32, &screensaverCookie, DBUS_TYPE_INVALID); screensaverCookie = 0; } else { const char * applicationString = configuration.applicationName; const char * reasonString = "Screensaver disabled while running"; callDBusMethod(node, path, interface, "Inhibit", DBUS_TYPE_STRING, &applicationString, DBUS_TYPE_STRING, &reasonString, DBUS_TYPE_INVALID, DBUS_TYPE_UINT32, &screensaverCookie, DBUS_TYPE_INVALID); } } void Shell_setDrawThrottleRate(double targetInterval) { mainLoopTargetInterval = targetInterval; } static void setVSync(bool sync) { const char * extensions = glXQueryExtensionsString(glXGetCurrentDisplay(), 0); if (strstr(extensions, "GLX_EXT_swap_control")) { PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXT = (PFNGLXSWAPINTERVALEXTPROC) glXGetProcAddress((const GLubyte *) "glXSwapIntervalEXT"); glXSwapIntervalEXT(glXGetCurrentDisplay(), glXGetCurrentDrawable(), sync); } else if (strstr(extensions, "GLX_SGI_swap_control")) { PFNGLXSWAPINTERVALSGIPROC glxSwapIntervalSGI = (PFNGLXSWAPINTERVALSGIPROC) glXGetProcAddress((const GLubyte *) "glXSwapIntervalSGI"); glxSwapIntervalSGI(sync); } } #define _NET_WM_STATE_TOGGLE 2 bool toggleFullScreenEWMH() { XEvent event; event.type = ClientMessage; event.xclient.window = window; event.xclient.message_type = XInternAtom(display, "_NET_WM_STATE", False); // TODO: Cache atom? event.xclient.format = 32; event.xclient.data.l[0] = _NET_WM_STATE_TOGGLE; event.xclient.data.l[1] = XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", False); // TODO: Cache atom? event.xclient.data.l[2] = 0; event.xclient.data.l[3] = 1; event.xclient.data.l[4] = 0; return !!XSendEvent(display, rootWindow, 0, SubstructureRedirectMask | SubstructureNotifyMask, &event); } bool Shell_enterFullScreen(unsigned int displayIndex, bool captureDisplay) { if (!inFullScreenMode) { if (!toggleFullScreenEWMH()) { return false; } setVSync(vsyncFullscreen); inFullScreenMode = true; } return true; } void Shell_exitFullScreen(void) { if (inFullScreenMode) { if (!toggleFullScreenEWMH()) { return; } inFullScreenMode = false; setVSync(vsyncWindow); } } double Shell_getCurrentTime(void) { struct timespec currentTime; clock_gettime(CLOCK_MONOTONIC, ¤tTime); return currentTime.tv_sec + currentTime.tv_nsec * 0.000000001; } #define RESOURCE_PATH_MAX 2048 const char * Shell_getResourcePath(void) { static char * resourcePath; if (resourcePath == NULL) { resourcePath = malloc(RESOURCE_PATH_MAX); ssize_t bytesRead = readlink("/proc/self/exe", resourcePath, RESOURCE_PATH_MAX - 1); int charIndex; if (bytesRead == -1) { charIndex = 0; } else { for (charIndex = bytesRead - 1; charIndex > 0; charIndex--) { if (resourcePath[charIndex] == '/') {\ charIndex++; break; } } } strncpy(resourcePath + charIndex, "Resources", RESOURCE_PATH_MAX - charIndex); } return resourcePath; } const char * Shell_getSupportPath(const char * subdirectory) { static char supportPath[PATH_MAX]; if (subdirectory == NULL) { snprintf(supportPath, PATH_MAX, "%s/.local/share", getenv("HOME")); } else { snprintf(supportPath, PATH_MAX, "%s/.local/share/%s", getenv("HOME"), subdirectory); } mkdir(supportPath, 0777); return supportPath; } void Shell_openURL(const char * url) { if (!fork()) { execl("/usr/bin/x-www-browser", "/usr/bin/x-www-browser", url, NULL); exit(EXIT_SUCCESS); } } struct batteryInfo { enum ShellBatteryState state; float level; }; static bool readNextKeyValuePair(size_t * ioPosition, const char * contents, size_t length, char * outKey, size_t keySize, char * outValue, size_t valueSize) { size_t charIndex, lastCharIndex, tokenLength; lastCharIndex = *ioPosition; while (lastCharIndex < length && isspace(contents[lastCharIndex])) { lastCharIndex++; } if (lastCharIndex >= length) { return false; } charIndex = lastCharIndex; while (charIndex < length && contents[charIndex] != ':') { charIndex++; } if (charIndex >= length) { return false; } tokenLength = charIndex - lastCharIndex; if (tokenLength > keySize) { tokenLength = keySize - 1; } memcpy(outKey, contents + lastCharIndex, tokenLength); outKey[tokenLength] = '\x00'; lastCharIndex = charIndex + 1; while (lastCharIndex < length && isspace(contents[lastCharIndex])) { lastCharIndex++; } if (lastCharIndex >= length) { return false; } charIndex = lastCharIndex; while (charIndex < length && contents[charIndex] != '\n') { charIndex++; } if (charIndex >= length) { return false; } tokenLength = charIndex - lastCharIndex; if (tokenLength > valueSize) { tokenLength = valueSize - 1; } memcpy(outValue, contents + lastCharIndex, tokenLength); outValue[tokenLength] = '\x00'; *ioPosition = charIndex + 1; return true; } static char * readVirtualFile(const char * path, size_t * outLength) { FILE * file; int byte; size_t allocatedSize = 16; char * data; size_t length = 0; file = fopen(path, "rb"); if (file == NULL) { return NULL; } data = malloc(allocatedSize); for (;;) { byte = fgetc(file); if (feof(file)) { break; } if (length >= allocatedSize) { allocatedSize *= 2; data = realloc(data, allocatedSize); } data[length++] = byte; } fclose(file); *outLength = length; return data; } #define KEY_SIZE 24 #define VALUE_SIZE 16 static struct batteryInfo getBatteryInfo(void) { struct batteryInfo info = {ShellBatteryState_unknown, -1.0f}; DIR * dir; struct dirent * dirent; char path[PATH_MAX]; unsigned int batteryNumber; size_t length, position; char * contents; char key[KEY_SIZE], value[VALUE_SIZE]; bool present = true; unsigned int lastFullCapacity_mWh = 0; unsigned int remainingCapacity_mWh = 0; // TODO: /proc/acpi is deprecated; use /sys/class/power_supply (see https://askubuntu.com/questions/69556/how-do-i-check-the-batterys-status-via-the-terminal) dir = opendir("/proc/acpi"); if (dir == NULL) { return info; } closedir(dir); dir = opendir("/proc/acpi/battery"); if (dir == NULL) { info.state = ShellBatteryState_notBatteryPowered; return info; } while ((dirent = readdir(dir)) != NULL) { if (sscanf(dirent->d_name, "BAT%u", &batteryNumber)) { snprintf(path, PATH_MAX, "/proc/acpi/battery/BAT%u/info", batteryNumber); contents = readVirtualFile(path, &length); if (contents == NULL) { continue; } position = 0; for (;;) { if (!readNextKeyValuePair(&position, contents, length, key, KEY_SIZE, value, VALUE_SIZE)) { break; } if (!strcmp(key, "present")) { if (!strcmp(value, "no")) { present = false; break; } } else if (!strcmp(key, "last full capacity")) { sscanf(value, "%u mWh", &lastFullCapacity_mWh); } } free(contents); if (!present) { info.state = ShellBatteryState_batteryMissing; break; } snprintf(path, PATH_MAX, "/proc/acpi/battery/BAT%u/state", batteryNumber); contents = readVirtualFile(path, &length); if (contents == NULL) { continue; } position = 0; for (;;) { if (!readNextKeyValuePair(&position, contents, length, key, KEY_SIZE, value, VALUE_SIZE)) { break; } if (!strcmp(key, "present")) { if (!strcmp(value, "no")) { present = false; break; } } else if (!strcmp(key, "remaining capacity")) { sscanf(value, "%u mWh", &remainingCapacity_mWh); } else if (!strcmp(key, "charging state")) { if (!strcmp(value, "discharging")) { info.state = ShellBatteryState_unplugged; } else if (!strcmp(value, "charging")) { info.state = ShellBatteryState_charging; } else if (!strcmp(value, "charged")) { info.state = ShellBatteryState_full; } else { info.state = ShellBatteryState_unknown; } } } free(contents); if (!present) { info.state = ShellBatteryState_batteryMissing; break; } if (lastFullCapacity_mWh > 0) { if (remainingCapacity_mWh >= lastFullCapacity_mWh) { info.level = 1.0f; } else { info.level = remainingCapacity_mWh / (float) lastFullCapacity_mWh; } } // Only support for one battery break; } } closedir(dir); return info; } enum ShellBatteryState Shell_getBatteryState(void) { return getBatteryInfo().state; } float Shell_getBatteryLevel(void) { return getBatteryInfo().level; } unsigned int Shell_setTimer(double interval, bool repeat, void (* callback)(unsigned int timerID, void * context), void * context) { timers = realloc(timers, (timerCount + 1) * sizeof(struct GLXShellTimer)); timers[timerCount].interval = interval; timers[timerCount].nextFireTime = Shell_getCurrentTime() + interval; timers[timerCount].repeat = repeat; timers[timerCount].canceled = false; timers[timerCount].id = nextTimerID++; timers[timerCount].callback = callback; timers[timerCount].context = context; return timers[timerCount++].id; } void Shell_cancelTimer(unsigned int timerID) { for (unsigned int timerIndex = 0; timerIndex < timerCount; timerIndex++) { if (timers[timerIndex].id == timerID) { timers[timerIndex].canceled = true; break; } } } void Shell_setCursorVisible(bool visible) { if (visible && cursorHiddenByHide) { cursorHiddenByHide = false; if (!mouseDeltaMode) { setShellCursor(lastUnhiddenCursor); } } else if (!visible && !cursorHiddenByHide) { cursorHiddenByHide = true; setEmptyCursor(); } } void Shell_hideCursorUntilMouseMoves(void) { if (!cursorHiddenByHide) { showCursorOnNextMouseMove = true; setEmptyCursor(); } } void Shell_setCursor(int cursor) { lastUnhiddenCursor = cursor; if (cursorHiddenByHide || mouseDeltaMode) { return; } setShellCursor(cursor); } ShellCursorID Shell_registerCustomCursor(unsigned int representationCount, struct ShellCustomCursorImage * representations, unsigned int hotspotX, unsigned int hotspotY) { customCursors = realloc(customCursors, sizeof(*customCursors) * (customCursorCount + 1)); customCursors[customCursorCount].representationCount = representationCount; customCursors[customCursorCount].representations = malloc(sizeof(*customCursors[customCursorCount].representations) * customCursors[customCursorCount].representationCount); for (unsigned int representationIndex = 0; representationIndex < representationCount; representationIndex++) { customCursors[customCursorCount].representations[representationIndex].scaleFactor = representations[representationIndex].scaleFactor; XcursorImage * image = XcursorImageCreate(representations[representationIndex].width, representations[representationIndex].height); image->xhot = hotspotX * representations[representationIndex].scaleFactor; image->yhot = hotspotY * representations[representationIndex].scaleFactor; image->delay = 0; unsigned char * pixels = representations[representationIndex].pixels; for (unsigned int rowIndex = 0; rowIndex < representations[representationIndex].height; rowIndex++) { for (unsigned int columnIndex = 0; columnIndex < representations[representationIndex].width; columnIndex++) { unsigned int pixelIndex = rowIndex * representations[representationIndex].width + columnIndex; unsigned int alpha = pixels[pixelIndex * 4 + 3]; unsigned int red = pixels[pixelIndex * 4 + 0]; unsigned int green = pixels[pixelIndex * 4 + 1]; unsigned int blue = pixels[pixelIndex * 4 + 2]; XcursorPixel argbPixel = alpha << 24 | (red * alpha / 0xFF) << 16 | (green * alpha / 0xFF) << 8 | (blue * alpha / 0xFF); image->pixels[pixelIndex] = argbPixel; } } customCursors[customCursorCount].representations[representationIndex].cursor = XcursorImageLoadCursor(display, image); XcursorImageDestroy(image); } return CUSTOM_CURSOR_ID_OFFSET + customCursorCount++; } void Shell_getMousePosition(float * outX, float * outY) { if (outX != NULL) { *outX = lastMouseX; } if (outY != NULL) { *outY = lastMouseY; } } void Shell_setMouseDeltaMode(bool deltaMode) { if (!mouseDeltaMode && deltaMode) { XWindowAttributes windowAttributes; setEmptyCursor(); restoreMouseX = lastMouseX; restoreMouseY = lastMouseY; XGetWindowAttributes(display, window, &windowAttributes); warpPointerAndIgnoreEvent(windowAttributes.width / 2, windowAttributes.height / 2); mouseDeltaMode = true; } else if (mouseDeltaMode && !deltaMode) { warpPointerAndIgnoreEvent(restoreMouseX, restoreMouseY); lastMouseX = restoreMouseX; lastMouseY = restoreMouseY; mouseDeltaMode = false; setShellCursor(lastUnhiddenCursor); } } bool Shell_getMouseDeltaMode(void) { return mouseDeltaMode; } void Shell_setVSync(bool sync, bool fullscreen) { if (fullscreen) { vsyncFullscreen = sync; if (inFullScreenMode) { setVSync(sync); } } else { vsyncWindow = sync; if (!inFullScreenMode) { setVSync(sync); } } } static void addFileDialogFilters(GtkFileChooser * chooser, unsigned int filterCount, struct ShellFileDialogFilter * filters) { if (filterCount > 0) { for (unsigned int filterIndex = 0; filterIndex < filterCount; filterIndex++) { GtkFileFilter * fileFilter = gtk_file_filter_new(); for (unsigned int extensionIndex = 0; extensionIndex < filters[filterIndex].fileExtensionCount; extensionIndex++) { char filterWildcardString[256]; snprintf(filterWildcardString, sizeof(filterWildcardString), "*.%s", filters[filterIndex].fileExtensions[extensionIndex]); gtk_file_filter_add_pattern(fileFilter, filterWildcardString); } gtk_file_filter_set_name(fileFilter, filters[filterIndex].filterName); gtk_file_chooser_add_filter(chooser, fileFilter); } GtkFileFilter * fileFilter = gtk_file_filter_new(); gtk_file_filter_add_pattern(fileFilter, "*"); gtk_file_filter_set_name(fileFilter, "All files"); gtk_file_chooser_add_filter(chooser, fileFilter); } } static gint runGTKDialog(GtkWidget * dialog) { GdkWindow * dialogWindow = gtk_widget_get_window(dialog); if (dialogWindow == NULL) { gtk_widget_show(dialog); dialogWindow = gtk_widget_get_window(dialog); } if (dialogWindow != NULL) { XSetTransientForHint(display, GDK_WINDOW_XID(dialogWindow), window); } showCursorOnNextMouseMove = false; setShellCursor(lastUnhiddenCursor); XFlush(display); gint result = gtk_dialog_run(GTK_DIALOG(dialog)); if (cursorHiddenByHide || mouseDeltaMode) { setEmptyCursor(); } return result; } static void postDialogClosedEvent() { XEvent event; event.type = ClientMessage; event.xclient.window = window; event.xclient.message_type = dialogClosedAtom; event.xclient.format = 32; event.xclient.data.l[0] = 0; event.xclient.data.l[1] = 0; event.xclient.data.l[2] = 0; event.xclient.data.l[3] = 0; event.xclient.data.l[4] = 0; XSendEvent(display, window, 0, 0, &event); } bool Shell_openFileDialog(const char * title, const char * defaultDirectory, const char * defaultFileName, const char ** outFilePath, unsigned int filterCount, struct ShellFileDialogFilter * filters) { GtkWidget * dialog; GtkFileChooser * chooser; bool success = false; static char * returnedFilePath; dialog = gtk_file_chooser_dialog_new(title, NULL, GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); chooser = GTK_FILE_CHOOSER(dialog); addFileDialogFilters(chooser, filterCount, filters); if (defaultDirectory != NULL) { if (defaultFileName != NULL) { char defaultFilePath[4096]; snprintf(defaultFilePath, sizeof(defaultFilePath), "%s/%s", defaultDirectory, defaultFileName); GFile * gfile = g_file_new_for_path(defaultFilePath); if (!gtk_file_chooser_set_file(chooser, gfile, NULL)) { gtk_file_chooser_set_current_folder(chooser, defaultDirectory); } g_object_unref(gfile); } else { gtk_file_chooser_set_current_folder(chooser, defaultDirectory); } } runningDialog = true; if (runGTKDialog(dialog) == GTK_RESPONSE_ACCEPT) { char * filename = gtk_file_chooser_get_filename(chooser); free(returnedFilePath); returnedFilePath = strdup(filename); *outFilePath = returnedFilePath; g_free(filename); success = true; } gtk_widget_destroy(dialog); while (gtk_events_pending()) { gtk_main_iteration(); } postDialogClosedEvent(); updateKeyModifierState(); return success; } bool Shell_openFileDialogMultiple(const char * title, const char * defaultDirectory, unsigned int defaultFileNameCount, const char ** defaultFileNames, unsigned int * outFilePathCount, const char *** outFilePaths, unsigned int filterCount, struct ShellFileDialogFilter * filters) { GtkWidget * dialog; GtkFileChooser * chooser; bool success = false; static unsigned int returnedFilePathCount; static char ** returnedFilePaths; dialog = gtk_file_chooser_dialog_new(title, NULL, GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); chooser = GTK_FILE_CHOOSER(dialog); gtk_file_chooser_set_select_multiple(chooser, TRUE); addFileDialogFilters(chooser, filterCount, filters); if (defaultDirectory != NULL) { gtk_file_chooser_set_current_folder(chooser, defaultDirectory); for (unsigned int fileNameIndex = 0; fileNameIndex < defaultFileNameCount; fileNameIndex++) { gtk_file_chooser_select_filename(chooser, defaultFileNames[fileNameIndex]); } } runningDialog = true; if (runGTKDialog(dialog) == GTK_RESPONSE_ACCEPT) { GSList * fileList = gtk_file_chooser_get_filenames(chooser); for (unsigned int filePathIndex = 0; filePathIndex < returnedFilePathCount; filePathIndex++) { free(returnedFilePaths[filePathIndex]); } free(returnedFilePaths); returnedFilePathCount = g_slist_length(fileList); returnedFilePaths = malloc(returnedFilePathCount * sizeof(*returnedFilePaths)); returnedFilePathCount = 0; for (GSList * listNode = fileList; listNode != NULL; listNode = listNode->next) { returnedFilePaths[returnedFilePathCount++] = strdup(listNode->data); g_free(listNode->data); } g_slist_free(fileList); *outFilePathCount = returnedFilePathCount; *outFilePaths = (const char **) returnedFilePaths; success = true; } gtk_widget_destroy(dialog); while (gtk_events_pending()) { gtk_main_iteration(); } postDialogClosedEvent(); updateKeyModifierState(); return success; } bool Shell_chooseFolderDialog(const char * title, const char * defaultDirectory, const char ** outFolderPath) { GtkWidget * dialog; GtkFileChooser * chooser; bool success = false; static char * returnedFolderPath; dialog = gtk_file_chooser_dialog_new(title, NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); chooser = GTK_FILE_CHOOSER(dialog); gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE); if (defaultDirectory != NULL) { gtk_file_chooser_set_current_folder(chooser, defaultDirectory); } runningDialog = true; if (runGTKDialog(dialog) == GTK_RESPONSE_ACCEPT) { char * filename = gtk_file_chooser_get_filename(chooser); free(returnedFolderPath); returnedFolderPath = strdup(filename); *outFolderPath = returnedFolderPath; g_free(filename); success = true; } gtk_widget_destroy(dialog); while (gtk_events_pending()) { gtk_main_iteration(); } postDialogClosedEvent(); updateKeyModifierState(); return success; } bool Shell_saveFileDialog(const char * title, const char * defaultDirectory, const char * defaultFileName, const char ** outFilePath, unsigned int filterCount, struct ShellFileDialogFilter * filters) { GtkWidget * dialog; GtkFileChooser * chooser; bool success = false; static char * returnedFilePath; dialog = gtk_file_chooser_dialog_new(title, NULL, GTK_FILE_CHOOSER_ACTION_SAVE, "Cancel", GTK_RESPONSE_CANCEL, "Save", GTK_RESPONSE_ACCEPT, NULL); chooser = GTK_FILE_CHOOSER(dialog); addFileDialogFilters(chooser, filterCount, filters); gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE); if (defaultDirectory != NULL) { gtk_file_chooser_set_current_folder(chooser, defaultDirectory); } if (defaultFileName != NULL) { gtk_file_chooser_set_current_name(chooser, defaultFileName); } runningDialog = true; if (runGTKDialog(dialog) == GTK_RESPONSE_ACCEPT) { char * filename; filename = gtk_file_chooser_get_filename(chooser); free(returnedFilePath); returnedFilePath = strdup(filename); *outFilePath = returnedFilePath; g_free(filename); success = true; } while (gtk_events_pending()) { gtk_main_iteration(); } gtk_widget_destroy(dialog); while (gtk_events_pending()) { gtk_main_iteration(); } postDialogClosedEvent(); updateKeyModifierState(); return success; } bool Shell_copyTextToClipboard(const char * text) { free(clipboardText); clipboardText = strdup(text); XSetSelectionOwner(display, XInternAtom(display, "CLIPBOARD", False), window, CurrentTime); return true; } #define PASTE_SIZE_MAX 0x1000000 const char * Shell_getClipboardText(void) { Atom selectionAtom, utf8Atom, targetPropertyAtom; Window owner; bool success = false; XEvent * delayedEvents = NULL; unsigned int delayedEventCount = 0; static char * lastPaste; selectionAtom = XInternAtom(display, "CLIPBOARD", False); owner = XGetSelectionOwner(display, selectionAtom); if (owner == None) { return NULL; } if (owner == window) { return clipboardText; } targetPropertyAtom = XInternAtom(display, "PASTE_BUFFER", False); utf8Atom = XInternAtom(display, "UTF8_STRING", False); XConvertSelection(display, selectionAtom, utf8Atom, targetPropertyAtom, window, CurrentTime); double timeoutTime = Shell_getCurrentTime() + 1.0; do { XEvent event; if (XPending(display)) { XNextEvent(display, &event); if (event.type == SelectionNotify) { if (event.xselection.property != None) { success = true; } break; } delayedEvents = realloc(delayedEvents, (delayedEventCount + 1) * sizeof(*delayedEvents)); delayedEvents[delayedEventCount++] = event; } if (!XPending(display)) { sleepEventLoop(0.0, false); } } while (Shell_getCurrentTime() < timeoutTime); for (unsigned int eventIndex = 0; eventIndex < delayedEventCount; eventIndex++) { XPutBackEvent(display, &delayedEvents[eventIndex]); } free(delayedEvents); if (!success) { return NULL; } Atom actualType; int actualFormat; unsigned long dataSize, dataSizeUnread; unsigned char * data; if (XGetWindowProperty(display, window, targetPropertyAtom, 0, PASTE_SIZE_MAX, False, AnyPropertyType, &actualType, &actualFormat, &dataSize, &dataSizeUnread, &data) != Success) { return NULL; } if (actualType != utf8Atom) { XFree(data); return NULL; } free(lastPaste); lastPaste = malloc(dataSize + 1); memcpy(lastPaste, data, dataSize); lastPaste[dataSize] = '\0'; XFree(data); return lastPaste; } unsigned int Shell_getModifierKeys(void) { return modifiers; } unsigned int Shell_getButtonMask(void) { return buttonMask; } const char * Shell_getKeyLabel(unsigned int keyCode) { return NULL; // TODO } void Shell_systemBeep(void) { XBell(display, 100); } static void updateWindowTitle(void) { if (windowCreated) { if (documentEdited) { char adornedTitle[256]; snprintf(adornedTitle, sizeof(adornedTitle), "*%s", windowTitle); XStoreName(display, window, adornedTitle); } else { XStoreName(display, window, windowTitle); } } } void Shell_setWindowTitle(const char * title) { free(windowTitle); windowTitle = strdup(title); updateWindowTitle(); } void Shell_setWindowDocumentEdited(bool edited) { documentEdited = edited; updateWindowTitle(); } void Shell_moveWindow(int x, int y) { if (windowCreated) { XMoveWindow(display, window, x, y); } } void Shell_resizeWindow(unsigned int width, unsigned int height) { if (windowCreated) { XResizeWindow(display, window, width, height); } } void Shell_moveResizeWindow(int x, int y, unsigned int width, unsigned int height) { if (windowCreated) { XMoveWindow(display, window, x, y); XResizeWindow(display, window, width, height); } } #ifndef SEM_VALUE_MAX #define SEM_VALUE_MAX 32767 #endif struct threadFuncInvocation { int (* threadFunction)(void * context); void * context; }; static void * callThreadFunc(void * context) { struct threadFuncInvocation * invocation = context; int (* threadFunction)(void * context); void * threadContext; threadFunction = invocation->threadFunction; threadContext = invocation->context; free(invocation); return (void *) threadFunction(threadContext); } ShellThread Shell_createThread(int (* threadFunction)(void * context), void * context) { pthread_t thread; struct threadFuncInvocation * invocation; invocation = malloc(sizeof(struct threadFuncInvocation)); invocation->threadFunction = threadFunction; invocation->context = context; pthread_create(&thread, NULL, callThreadFunc, invocation); return (ShellThread) thread; } void Shell_detachThread(ShellThread thread) { pthread_detach((pthread_t) thread); } int Shell_joinThread(ShellThread thread) { int status; void * returnValue; status = pthread_join((pthread_t) thread, &returnValue); if (status == 0) { return (int) returnValue; } return status; } void Shell_exitThread(int statusCode) { pthread_exit((void *) statusCode); } ShellThread Shell_getCurrentThread(void) { return (ShellThread) pthread_self(); } ShellMutex Shell_createMutex(void) { pthread_mutex_t * mutex; pthread_mutexattr_t recursiveLock; pthread_mutexattr_init(&recursiveLock); pthread_mutexattr_settype(&recursiveLock, PTHREAD_MUTEX_RECURSIVE); mutex = malloc(sizeof(pthread_mutex_t)); pthread_mutex_init(mutex, &recursiveLock); pthread_mutexattr_destroy(&recursiveLock); return (ShellMutex) mutex; } void Shell_disposeMutex(ShellMutex mutex) { pthread_mutex_destroy((pthread_mutex_t *) mutex); free(mutex); } void Shell_lockMutex(ShellMutex mutex) { pthread_mutex_lock((pthread_mutex_t *) mutex); } bool Shell_tryLockMutex(ShellMutex mutex) { return !pthread_mutex_trylock((pthread_mutex_t *) mutex); } void Shell_unlockMutex(ShellMutex mutex) { pthread_mutex_unlock((pthread_mutex_t *) mutex); } ShellSemaphore Shell_createSemaphore(unsigned int value) { sem_t * semaphore; semaphore = malloc(sizeof(sem_t)); sem_init(semaphore, 0, value > SEM_VALUE_MAX ? SEM_VALUE_MAX : value); return (ShellSemaphore) semaphore; } void Shell_disposeSemaphore(ShellSemaphore semaphore) { sem_destroy((sem_t *) semaphore); free(semaphore); } void Shell_postSemaphore(ShellSemaphore semaphore) { sem_post((sem_t *) semaphore); } void Shell_waitSemaphore(ShellSemaphore semaphore) { sem_wait((sem_t *) semaphore); } bool Shell_tryWaitSemaphore(ShellSemaphore semaphore) { return !sem_trywait((sem_t *) semaphore); } void Shell_executeOnMainThread(void (* callback)(void * context), void * context) { Shell_lockMutex(callbackQueueHeadMutex); if ((queuedCallbackHead + 1) % QUEUED_CALLBACK_MAX == queuedCallbackTail) { #ifdef DEBUG fprintf(stderr, "Error: Shell_executeOnMainThread() called when the maximum number of callbacks (%u) is already queued. Couldn't add to queue.\n", QUEUED_CALLBACK_MAX); #endif Shell_unlockMutex(callbackQueueHeadMutex); return; } queuedCallbacks[queuedCallbackHead].callback = callback; queuedCallbacks[queuedCallbackHead].context = context; queuedCallbackHead = (queuedCallbackHead + 1) % QUEUED_CALLBACK_MAX; Shell_unlockMutex(callbackQueueHeadMutex); unsigned char byte = 0; write(pselectPipe[1], &byte, 1); } void GLXShell_coalesceMouseMotionEvents(bool coalesce) { coalesceMouseMotionEvents = coalesce; } void GLXShell_sendActivationClicks(bool send) { sendActivationClicks = send; } unsigned int getGLXAttributes(struct GLXShellConfiguration configuration, int * outAttributes) { unsigned int attributeCount = 0; outAttributes[attributeCount++] = GLX_RGBA; if (configuration.displayMode.doubleBuffer) { outAttributes[attributeCount++] = GLX_DOUBLEBUFFER; } if (configuration.displayMode.depthBuffer) { outAttributes[attributeCount++] = GLX_DEPTH_SIZE; outAttributes[attributeCount++] = configuration.displayMode.depthSize; } if (configuration.displayMode.multisample) { outAttributes[attributeCount++] = GLX_SAMPLE_BUFFERS; outAttributes[attributeCount++] = configuration.displayMode.sampleBuffers; outAttributes[attributeCount++] = GLX_SAMPLES; outAttributes[attributeCount++] = configuration.displayMode.samples; } outAttributes[attributeCount++] = None; return attributeCount; } int main(int argc, char ** argv) { XVisualInfo * visualInfo = NULL; XSetWindowAttributes windowAttributes; int attributes[7]; Colormap colormap; gtk_init(&argc, &argv); display = XOpenDisplay(NULL); if (display == NULL) { #ifdef DEBUG fprintf(stderr, "XOpenDisplay failed\n"); #endif return EXIT_FAILURE; } screen = DefaultScreen(display); rootWindow = RootWindow(display, screen); float scaleFactor = Shell_getDisplayScaleFactor(0); configuration.windowX = 50; configuration.windowY = 80; configuration.windowWidth = 800 * scaleFactor; configuration.windowHeight = 600 * scaleFactor; configuration.windowTitle = "GLXShell"; configuration.applicationName = "GLXShell Application"; configuration.icon.data = NULL; configuration.icon.size = 0; configuration.displayMode.doubleBuffer = true; configuration.displayMode.depthBuffer = false; configuration.displayMode.depthSize = 24; configuration.displayMode.stencilBuffer = false; configuration.displayMode.stencilSize = 16; configuration.displayMode.multisample = false; configuration.displayMode.sampleBuffers = 1; configuration.displayMode.samples = 4; GLXTarget_configure(argc, (const char **) argv, &configuration); getGLXAttributes(configuration, attributes); visualInfo = glXChooseVisual(display, 0, attributes); if (visualInfo == NULL && configuration.displayMode.multisample) { #ifdef DEBUG fprintf(stderr, "Requested pixel format unavailable; trying fallback non-multisampled format\n"); #endif configuration.displayMode.multisample = false; getGLXAttributes(configuration, attributes); visualInfo = glXChooseVisual(display, 0, attributes); } if (visualInfo == NULL) { #ifdef DEBUG fprintf(stderr, "Requested pixel format unavailable\n"); #endif return EXIT_FAILURE; } colormap = XCreateColormap(display, rootWindow, visualInfo->visual, AllocNone); windowAttributes.colormap = colormap; windowAttributes.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | ButtonMotionMask | LeaveWindowMask | FocusChangeMask | StructureNotifyMask; windowAttributes.background_pixmap = None; windowAttributes.background_pixel = 0; windowAttributes.border_pixel = 0; window = XCreateWindow(display, rootWindow, configuration.windowX, configuration.windowY - WINDOW_TITLE_BAR_HEIGHT_ESTIMATE * scaleFactor, configuration.windowWidth, configuration.windowHeight, 0, visualInfo->depth, InputOutput, visualInfo->visual, CWColormap | CWEventMask | CWBackPixmap | CWBorderPixel, &windowAttributes); deleteWindowAtom = XInternAtom(display, "WM_DELETE_WINDOW", False); dialogClosedAtom = XInternAtom(display, "glxshell_dialogClosed", False); XSetWMProtocols(display, window, &deleteWindowAtom, 1); windowTitle = strdup(configuration.windowTitle); XStoreName(display, window, windowTitle); XClassHint * classHint = XAllocClassHint(); classHint->res_name = configuration.applicationName; classHint->res_class = configuration.applicationName; XSetClassHint(display, window, classHint); free(classHint); XMapWindow(display, window); initialWindowX = configuration.windowX; initialWindowY = configuration.windowY; XMoveWindow(display, window, initialWindowX, initialWindowY); if (configuration.icon.data != NULL) { Atom iconAtom = XInternAtom(display, "_NET_WM_ICON", False); XChangeProperty(display, window, iconAtom, XA_CARDINAL, 32, PropModeReplace, (const unsigned char *) configuration.icon.data, configuration.icon.size); XFlush(display); } windowCreated = true; context = glXCreateContext(display, visualInfo, NULL, GL_TRUE); glXMakeCurrent(display, window, context); setVSync(vsyncWindow); callbackQueueHeadMutex = Shell_createMutex(); pipe2(pselectPipe, O_DIRECT); Target_init(); return EXIT_SUCCESS; } void Shell_msleep(unsigned int milliseconds) { struct timespec sleepTimeSpec = {.tv_sec = milliseconds / 1000, .tv_nsec = (milliseconds % 1000) * 1000000}; struct timespec elapsedTimeSpec = sleepTimeSpec; nanosleep(&sleepTimeSpec, &elapsedTimeSpec); } void Shell_preciseSleep(double seconds) { struct timespec sleepTimeSpec = {.tv_sec = seconds, .tv_nsec = fmod(seconds, 1.0) * 1000000000.0}; struct timespec elapsedTimeSpec = sleepTimeSpec; nanosleep(&sleepTimeSpec, &elapsedTimeSpec); }