aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--TODO4
m---------depends/mglpp0
-rw-r--r--include/Overlay.hpp15
-rw-r--r--meson.build12
-rw-r--r--project.conf4
-rw-r--r--src/Overlay.cpp382
7 files changed, 370 insertions, 49 deletions
diff --git a/README.md b/README.md
index 4f6fec8..a310c2a 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ GPU Screen Recorder UI uses meson build system so you need to install `meson` to
## Build dependencies
These are the dependencies needed to build GPU Screen Recorder UI:
-* x11 (libx11, libxrandr, libxrender, libxfixes, libxcomposite)
+* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxi)
* libglvnd (which provides libgl, libglx and libegl)
* libevdev
* libudev (systemd-libs)
diff --git a/TODO b/TODO
index 1c98fc0..5ad4078 100644
--- a/TODO
+++ b/TODO
@@ -97,4 +97,6 @@ Add option to select which gpu to record with, or list all monitors and automati
Remove all dependencies from tools/gsr-global-hotkeys and roll our own keyboard events code.
-Test global hotkeys with azerty instead of qwerty. \ No newline at end of file
+Test global hotkeys with azerty instead of qwerty.
+
+Fix cursor grab not working in owlboy, need to use xigrab. \ No newline at end of file
diff --git a/depends/mglpp b/depends/mglpp
-Subproject 7f0a03d90e2d13632d22eb43447f553a695ddd9
+Subproject 4dbee5ac57c10819ff8d06560be573ad3a3e4e0
diff --git a/include/Overlay.hpp b/include/Overlay.hpp
index 580759c..d13d75b 100644
--- a/include/Overlay.hpp
+++ b/include/Overlay.hpp
@@ -54,6 +54,12 @@ namespace gsr {
void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type);
bool is_open() const;
private:
+ void xi_setup();
+ void handle_xi_events();
+ void xi_setup_fake_cursor();
+ void xi_grab_all_devices();
+ void xi_warp_pointer(mgl::vec2i position);
+
void process_key_bindings(mgl::Event &event);
void update_notification_process_status();
@@ -99,6 +105,10 @@ namespace gsr {
mgl::Texture screenshot_texture;
mgl::Sprite screenshot_sprite;
mgl::Rectangle bg_screenshot_overlay;
+ mgl::Texture cursor_texture;
+ mgl::Sprite cursor_sprite;
+ mgl::vec2i cursor_hotspot;
+ bool cursor_drawn = false;
WindowTexture window_texture;
PageStack page_stack;
mgl::Rectangle top_bar_background;
@@ -125,5 +135,10 @@ namespace gsr {
bool focused_window_is_fullscreen = false;
std::array<KeyBinding, 1> key_bindings;
+
+ Display *xi_display = nullptr;
+ int xi_opcode = 0;
+ XEvent *xi_input_xev = nullptr;
+ XEvent *xi_output_xev = nullptr;
};
} \ No newline at end of file
diff --git a/meson.build b/meson.build
index 056fea3..aeb144c 100644
--- a/meson.build
+++ b/meson.build
@@ -42,11 +42,6 @@ src = [
mglpp_proj = subproject('mglpp')
mglpp_dep = mglpp_proj.get_variable('mglpp_dep')
-dep = [
- mglpp_dep,
- dependency('xcomposite'),
-]
-
prefix = get_option('prefix')
datadir = get_option('datadir')
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
@@ -55,7 +50,12 @@ executable(
meson.project_name(),
src,
install : true,
- dependencies : dep,
+ dependencies : [
+ mglpp_dep,
+ dependency('xcomposite'),
+ dependency('xfixes'),
+ dependency('xi'),
+ ],
cpp_args : '-DGSR_UI_RESOURCES_PATH="' + gsr_ui_resources_path + '"',
)
diff --git a/project.conf b/project.conf
index 22bbc78..acf3b2f 100644
--- a/project.conf
+++ b/project.conf
@@ -11,4 +11,6 @@ version = "c++17"
ignore_dirs = ["build", "tools"]
[dependencies]
-xcomposite = ">=0" \ No newline at end of file
+xcomposite = ">=0"
+xfixes = ">=0"
+xi = ">=0" \ No newline at end of file
diff --git a/src/Overlay.cpp b/src/Overlay.cpp
index 25817f5..2802e70 100644
--- a/src/Overlay.cpp
+++ b/src/Overlay.cpp
@@ -21,6 +21,9 @@
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/cursorfont.h>
+#include <X11/extensions/Xfixes.h>
+#include <X11/extensions/XInput2.h>
+#include <X11/extensions/shape.h>
#include <mglpp/system/Rect.hpp>
#include <mglpp/window/Event.hpp>
@@ -117,6 +120,59 @@ namespace gsr {
return texture;
}
+ static bool texture_from_x11_cursor(XFixesCursorImage *x11_cursor_image, bool *visible, mgl::vec2i *hotspot, mgl::Texture &texture) {
+ uint8_t *cursor_data = NULL;
+ uint8_t *out = NULL;
+ const unsigned long *pixels = NULL;
+ *visible = false;
+
+ if(!x11_cursor_image)
+ goto err;
+
+ if(!x11_cursor_image->pixels)
+ goto err;
+
+ hotspot->x = x11_cursor_image->xhot;
+ hotspot->y = x11_cursor_image->yhot;
+
+ pixels = x11_cursor_image->pixels;
+ cursor_data = (uint8_t*)malloc((int)x11_cursor_image->width * (int)x11_cursor_image->height * 4);
+ if(!cursor_data)
+ goto err;
+
+ out = cursor_data;
+ /* Un-premultiply alpha */
+ for(int y = 0; y < x11_cursor_image->height; ++y) {
+ for(int x = 0; x < x11_cursor_image->width; ++x) {
+ uint32_t pixel = *pixels++;
+ uint8_t *in = (uint8_t*)&pixel;
+ uint8_t alpha = in[3];
+ if(alpha == 0) {
+ alpha = 1;
+ } else {
+ *visible = true;
+ }
+
+ out[0] = (float)in[2] * 255.0/(float)alpha;
+ out[1] = (float)in[1] * 255.0/(float)alpha;
+ out[2] = (float)in[0] * 255.0/(float)alpha;
+ out[3] = in[3];
+ out += 4;
+ in += 4;
+ }
+ }
+
+ texture.load_from_memory(cursor_data, x11_cursor_image->width, x11_cursor_image->height, MGL_IMAGE_FORMAT_RGBA);
+ free(cursor_data);
+ XFree(x11_cursor_image);
+ return true;
+
+ err:
+ if(x11_cursor_image)
+ XFree(x11_cursor_image);
+ return false;
+ }
+
static char hex_value_to_str(uint8_t v) {
if(v <= 9)
return '0' + v;
@@ -265,6 +321,14 @@ namespace gsr {
return True;
}
+ static void make_window_click_through(Display *display, Window window) {
+ XRectangle rect;
+ memset(&rect, 0, sizeof(rect));
+ XserverRegion region = XFixesCreateRegion(display, &rect, 1);
+ XFixesSetWindowShapeRegion(display, window, ShapeInput, 0, 0, region);
+ XFixesDestroyRegion(display, region);
+ }
+
static Bool make_window_sticky(Display *dpy, Window window) {
return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_STICKY", False));
}
@@ -320,6 +384,26 @@ namespace gsr {
return is_connected;
}
+ static bool xinput_is_supported(Display *dpy, int *xi_opcode) {
+ *xi_opcode = 0;
+ int query_event = 0;
+ int query_error = 0;
+ if(!XQueryExtension(dpy, "XInputExtension", xi_opcode, &query_event, &query_error)) {
+ fprintf(stderr, "gsr-ui error: X Input extension not available\n");
+ return false;
+ }
+
+ int major = 2;
+ int minor = 1;
+ int retval = XIQueryVersion(dpy, &major, &minor);
+ if (retval != Success) {
+ fprintf(stderr, "gsr-ui error: XInput 2.1 is not supported\n");
+ return false;
+ }
+
+ return true;
+ }
+
Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, egl_functions egl_funcs) :
resources_path(std::move(resources_path)),
gsr_info(gsr_info),
@@ -329,6 +413,9 @@ namespace gsr {
close_button_widget({0.0f, 0.0f}),
config(gsr_info)
{
+ // TODO:
+ //xi_setup();
+
memset(&window_texture, 0, sizeof(window_texture));
key_bindings[0].key_event.code = mgl::Keyboard::Escape;
@@ -377,6 +464,60 @@ namespace gsr {
}
gpu_screen_recorder_process = -1;
}
+
+ free(xi_input_xev);
+ free(xi_output_xev);
+ if(xi_display)
+ XCloseDisplay(xi_display);
+ }
+
+ void Overlay::xi_setup() {
+ xi_display = XOpenDisplay(nullptr);
+ if(!xi_display) {
+ fprintf(stderr, "gsr-ui error: failed to setup XI connection\n");
+ return;
+ }
+
+ if(!xinput_is_supported(xi_display, &xi_opcode))
+ goto error;
+
+ xi_input_xev = (XEvent*)calloc(1, sizeof(XEvent));
+ if(!xi_input_xev)
+ throw std::runtime_error("gsr-ui error: failed to allocate XEvent data");
+
+ xi_output_xev = (XEvent*)calloc(1, sizeof(XEvent));
+ if(!xi_output_xev)
+ throw std::runtime_error("gsr-ui error: failed to allocate XEvent data");
+
+ unsigned char mask[XIMaskLen(XI_LASTEVENT)];
+ memset(mask, 0, sizeof(mask));
+ XISetMask(mask, XI_Motion);
+ XISetMask(mask, XI_ButtonPress);
+ XISetMask(mask, XI_ButtonRelease);
+ XISetMask(mask, XI_KeyPress);
+ XISetMask(mask, XI_KeyRelease);
+
+ XIEventMask xi_masks;
+ xi_masks.deviceid = XIAllMasterDevices;
+ xi_masks.mask_len = sizeof(mask);
+ xi_masks.mask = mask;
+ if(XISelectEvents(xi_display, DefaultRootWindow(xi_display), &xi_masks, 1) != Success) {
+ fprintf(stderr, "gsr-ui error: XISelectEvents failed\n");
+ goto error;
+ }
+
+ XFlush(xi_display);
+ return;
+
+ error:
+ free(xi_input_xev);
+ xi_input_xev = nullptr;
+ free(xi_output_xev);
+ xi_output_xev = nullptr;
+ if(xi_display) {
+ XCloseDisplay(xi_display);
+ xi_display = nullptr;
+ }
}
static uint32_t key_event_to_bitmask(mgl::Event::KeyEvent key_event) {
@@ -397,10 +538,72 @@ namespace gsr {
}
}
+ void Overlay::handle_xi_events() {
+ if(!xi_display)
+ return;
+
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+
+ while(XPending(xi_display)) {
+ XNextEvent(xi_display, xi_input_xev);
+ XGenericEventCookie *cookie = &xi_input_xev->xcookie;
+ if(cookie->type == GenericEvent && cookie->extension == xi_opcode && XGetEventData(xi_display, cookie)) {
+ const XIDeviceEvent *de = (XIDeviceEvent*)cookie->data;
+ if(cookie->evtype == XI_Motion) {
+ memset(xi_output_xev, 0, sizeof(*xi_output_xev));
+ xi_output_xev->type = MotionNotify;
+ xi_output_xev->xmotion.display = display;
+ xi_output_xev->xmotion.window = window->get_system_handle();
+ xi_output_xev->xmotion.subwindow = window->get_system_handle();
+ xi_output_xev->xmotion.x = de->event_x;
+ xi_output_xev->xmotion.y = de->event_y;
+ xi_output_xev->xmotion.x_root = de->root_x;
+ xi_output_xev->xmotion.y_root = de->root_y;
+ //xi_output_xev->xmotion.state = // modifiers // TODO:
+ if(window->inject_x11_event(xi_output_xev, event))
+ on_event(event);
+ } else if(cookie->evtype == XI_ButtonPress || cookie->evtype == XI_ButtonRelease) {
+ memset(xi_output_xev, 0, sizeof(*xi_output_xev));
+ xi_output_xev->type = cookie->evtype == XI_ButtonPress ? ButtonPress : ButtonRelease;
+ xi_output_xev->xbutton.display = display;
+ xi_output_xev->xbutton.window = window->get_system_handle();
+ xi_output_xev->xbutton.subwindow = window->get_system_handle();
+ xi_output_xev->xbutton.x = de->event_x;
+ xi_output_xev->xbutton.y = de->event_y;
+ xi_output_xev->xbutton.x_root = de->root_x;
+ xi_output_xev->xbutton.y_root = de->root_y;
+ //xi_output_xev->xbutton.state = // modifiers // TODO:
+ xi_output_xev->xbutton.button = de->detail;
+ if(window->inject_x11_event(xi_output_xev, event))
+ on_event(event);
+ } else if(cookie->evtype == XI_KeyPress || cookie->evtype == XI_KeyRelease) {
+ memset(xi_output_xev, 0, sizeof(*xi_output_xev));
+ xi_output_xev->type = cookie->evtype == XI_KeyPress ? KeyPress : KeyRelease;
+ xi_output_xev->xkey.display = display;
+ xi_output_xev->xkey.window = window->get_system_handle();
+ xi_output_xev->xkey.subwindow = window->get_system_handle();
+ xi_output_xev->xkey.x = de->event_x;
+ xi_output_xev->xkey.y = de->event_y;
+ xi_output_xev->xkey.x_root = de->root_x;
+ xi_output_xev->xkey.y_root = de->root_y;
+ xi_output_xev->xkey.state = de->mods.effective;
+ xi_output_xev->xkey.keycode = de->detail;
+ if(window->inject_x11_event(xi_output_xev, event))
+ on_event(event);
+ }
+ //fprintf(stderr, "got xi event: %d\n", cookie->evtype);
+ XFreeEventData(xi_display, cookie);
+ }
+ }
+ }
+
void Overlay::handle_events() {
if(!visible || !window)
return;
+ handle_xi_events();
+
while(window->poll_event(event)) {
on_event(event);
}
@@ -452,12 +655,86 @@ namespace gsr {
close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f));
page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f));
+ if(cursor_texture.is_valid()) {
+ if(!cursor_drawn) {
+ cursor_drawn = true;
+ XFixesHideCursor(xi_display, DefaultRootWindow(xi_display));
+ XFlush(xi_display);
+ }
+
+ cursor_sprite.set_position((window->get_mouse_position() - cursor_hotspot).to_vec2f());
+ window->draw(cursor_sprite);
+ }
+
window->display();
return true;
}
+ void Overlay::xi_setup_fake_cursor() {
+ if(!xi_display)
+ return;
+
+ XFixesShowCursor(xi_display, DefaultRootWindow(xi_display));
+ XFlush(xi_display);
+
+ bool cursor_visible = false;
+ texture_from_x11_cursor(XFixesGetCursorImage(xi_display), &cursor_visible, &cursor_hotspot, cursor_texture);
+ if(cursor_texture.is_valid())
+ cursor_sprite.set_texture(&cursor_texture);
+ }
+
+ void Overlay::xi_grab_all_devices() {
+ if(!xi_display)
+ return;
+
+ int num_devices = 0;
+ XIDeviceInfo *info = XIQueryDevice(xi_display, XIAllDevices, &num_devices);
+ if(!info)
+ return;
+
+ for (int i = 0; i < num_devices; ++i) {
+ const XIDeviceInfo *dev = &info[i];
+ XIEventMask masks[1];
+ unsigned char mask0[XIMaskLen(XI_LASTEVENT)];
+ memset(mask0, 0, sizeof(mask0));
+ XISetMask(mask0, XI_Motion);
+ XISetMask(mask0, XI_ButtonPress);
+ XISetMask(mask0, XI_ButtonRelease);
+ XISetMask(mask0, XI_KeyPress);
+ XISetMask(mask0, XI_KeyRelease);
+ masks[0].deviceid = dev->deviceid;
+ masks[0].mask_len = sizeof(mask0);
+ masks[0].mask = mask0;
+ XIGrabDevice(xi_display, dev->deviceid, window->get_system_handle(), CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, XIOwnerEvents, masks);
+ }
+
+ XFlush(xi_display);
+ XIFreeDeviceInfo(info);
+ }
+
+ void Overlay::xi_warp_pointer(mgl::vec2i position) {
+ if(!xi_display)
+ return;
+
+ int num_devices = 0;
+ XIDeviceInfo *info = XIQueryDevice(xi_display, XIAllDevices, &num_devices);
+ if(!info)
+ return;
+
+ for (int i = 0; i < num_devices; ++i) {
+ const XIDeviceInfo *dev = &info[i];
+ XIWarpPointer(xi_display, dev->deviceid, DefaultRootWindow(xi_display), DefaultRootWindow(xi_display), 0, 0, 0, 0, position.x, position.y);
+ }
+
+ XFlush(xi_display);
+ XIFreeDeviceInfo(info);
+ }
+
void Overlay::show() {
+ if(visible)
+ return;
+
window.reset();
window = std::make_unique<mgl::Window>();
deinit_theme();
@@ -471,7 +748,7 @@ namespace gsr {
window_create_params.max_size = window_size;
window_create_params.position = window_pos;
window_create_params.hidden = true;
- window_create_params.override_redirect = false;
+ window_create_params.override_redirect = true;
window_create_params.background_color = bg_color;
window_create_params.support_alpha = true;
window_create_params.window_type = MGL_WINDOW_TYPE_NORMAL;
@@ -637,33 +914,47 @@ namespace gsr {
return true;
};
- window->set_fullscreen(true);
+ //window->set_fullscreen(true);
+ if(gsr_info.system_info.display_server == DisplayServer::X11)
+ make_window_click_through(display, window->get_system_handle());
+
window->set_visible(true);
+
make_window_sticky(display, window->get_system_handle());
hide_window_from_taskbar(display, window->get_system_handle());
- // if(default_cursor) {
- // XFreeCursor(display, default_cursor);
- // default_cursor = 0;
- // }
- // default_cursor = XCreateFontCursor(display, XC_arrow);
+ if(default_cursor) {
+ XFreeCursor(display, default_cursor);
+ default_cursor = 0;
+ }
+ default_cursor = XCreateFontCursor(display, XC_arrow);
- // TODO: Retry if these fail.
- // TODO: Hmm, these dont work in owlboy. Maybe owlboy uses xi2 and that breaks this (does it?).
- // Remove these grabs when debugging with a debugger, or your X11 session will appear frozen
+ // TODO: Remove these grabs when debugging with a debugger, or your X11 session will appear frozen.
+ // There should be a debug mode to not use these
- // XGrabPointer(display, window->get_system_handle(), True,
- // ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
- // Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask |
- // ButtonMotionMask,
- // GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime);
- // TODO: This breaks global hotkeys
- //XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime);
+ XGrabPointer(display, window->get_system_handle(), True,
+ ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
+ Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask |
+ ButtonMotionMask,
+ GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime);
+ // TODO: This breaks global hotkeys (when using x11 global hotkeys)
+ XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime);
- set_focused_window(display, window->get_system_handle());
XFlush(display);
- window->set_fullscreen(true);
+ // The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed
+ xi_setup_fake_cursor();
+
+ // We want to grab all devices to prevent any other application below from receiving events.
+ // Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events.
+ xi_grab_all_devices();
+
+ // if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) {
+ // set_focused_window(display, window->get_system_handle());
+ // XFlush(display);
+ // }
+
+ //window->set_fullscreen(true);
visible = true;
@@ -694,6 +985,9 @@ namespace gsr {
}
void Overlay::hide() {
+ if(!visible)
+ return;
+
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
@@ -701,14 +995,23 @@ namespace gsr {
page_stack.pop();
}
- // if(default_cursor) {
- // XFreeCursor(display, default_cursor);
- // default_cursor = 0;
- // }
+ if(default_cursor) {
+ XFreeCursor(display, default_cursor);
+ default_cursor = 0;
+ }
+
+ XUngrabKeyboard(display, CurrentTime);
+ XUngrabPointer(display, CurrentTime);
+ XFlush(display);
- // XUngrabKeyboard(display, CurrentTime);
- // XUngrabPointer(display, CurrentTime);
- // XFlush(display);
+ if(xi_display) {
+ // TODO: Only show cursor if it wasn't hidden before the ui was shown
+ cursor_drawn = false;
+ //XFixesShowCursor(xi_display, DefaultRootWindow(xi_display));
+ //XFlush(xi_display);
+ cursor_texture.clear();
+ cursor_sprite.set_texture(nullptr);
+ }
window_texture_deinit(&window_texture);
window_texture_sprite.set_texture(nullptr);
@@ -717,8 +1020,19 @@ namespace gsr {
visible = false;
if(window) {
+ const mgl::vec2i new_cursor_position = mgl::vec2i(window->internal_window()->pos.x, window->internal_window()->pos.y) + window->get_mouse_position();
window->set_visible(false);
window.reset();
+
+ if(xi_display) {
+ XFlush(display);
+ XWarpPointer(display, DefaultRootWindow(display), DefaultRootWindow(display), 0, 0, 0, 0, new_cursor_position.x, new_cursor_position.y);
+ XFlush(display);
+ //xi_warp_pointer(new_cursor_position);
+
+ XFixesShowCursor(xi_display, DefaultRootWindow(xi_display));
+ XFlush(xi_display);
+ }
}
deinit_theme();
@@ -831,20 +1145,8 @@ namespace gsr {
}
int exit_code = -1;
- // The process is no longer a child process since gsr ui has restarted
- if(errno == ECHILD) {
- errno = 0;
- kill(gpu_screen_recorder_process, 0);
- if(errno != ESRCH) {
- // Still running
- return;
- }
- // We cant know the exit status, so we assume it succeeded
- exit_code = 0;
- } else {
- if(WIFEXITED(status))
- exit_code = WEXITSTATUS(status);
- }
+ if(WIFEXITED(status))
+ exit_code = WEXITSTATUS(status);
switch(recording_status) {
case RecordingStatus::NONE: