mirror of
https://repo.dec05eba.com/gpu-screen-recorder-ui
synced 2026-03-31 17:27:05 +09:00
437 lines
16 KiB
C++
437 lines
16 KiB
C++
#include "../include/RegionSelector.hpp"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <X11/extensions/XInput2.h>
|
|
#include <X11/extensions/Xrandr.h>
|
|
#include <X11/extensions/shape.h>
|
|
|
|
namespace gsr {
|
|
static const int cursor_window_size = 32;
|
|
static const int cursor_thickness = 5;
|
|
static const int region_border_size = 2;
|
|
|
|
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, "error: RegionSelector: 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, "error: RegionSelector: XInput 2.1 is not supported\n");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static int max_int(int a, int b) {
|
|
return a >= b ? a : b;
|
|
}
|
|
|
|
static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) {
|
|
if(width < 0) {
|
|
x += width;
|
|
width = abs(width);
|
|
}
|
|
|
|
if(height < 0) {
|
|
y += height;
|
|
height = abs(height);
|
|
}
|
|
|
|
XRectangle rectangles[] = {
|
|
{
|
|
(short)max_int(0, x), (short)max_int(0, y),
|
|
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
|
|
}, // Left
|
|
{
|
|
(short)max_int(0, x + width - border_size), (short)max_int(0, y),
|
|
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
|
|
}, // Right
|
|
{
|
|
(short)max_int(0, x + border_size), (short)max_int(0, y),
|
|
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
|
|
}, // Top
|
|
{
|
|
(short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size),
|
|
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
|
|
}, // Bottom
|
|
};
|
|
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted);
|
|
XFlush(dpy);
|
|
}
|
|
|
|
static void set_window_shape_cross(Display *dpy, Window window, int window_width, int window_height, int thickness) {
|
|
XRectangle rectangles[] = {
|
|
{
|
|
(short)(window_width / 2 - thickness / 2), (short)0,
|
|
(unsigned short)thickness, (unsigned short)window_height
|
|
}, // Vertical
|
|
{
|
|
(short)(0), (short)(window_height / 2 - thickness / 2),
|
|
(unsigned short)window_width, (unsigned short)thickness
|
|
}, // Horizontal
|
|
};
|
|
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 2, ShapeSet, Unsorted);
|
|
XFlush(dpy);
|
|
}
|
|
|
|
static void draw_rectangle(Display *dpy, Window window, GC gc, int x, int y, int width, int height) {
|
|
if(width < 0) {
|
|
x += width;
|
|
width = abs(width);
|
|
}
|
|
|
|
if(height < 0) {
|
|
y += height;
|
|
height = abs(height);
|
|
}
|
|
|
|
XDrawRectangle(dpy, window, gc, x, y, width, height);
|
|
}
|
|
|
|
static Window create_cursor_window(Display *dpy, int width, int height, XVisualInfo *vinfo, unsigned long background_pixel) {
|
|
XSetWindowAttributes window_attr;
|
|
window_attr.background_pixel = background_pixel;
|
|
window_attr.border_pixel = 0;
|
|
window_attr.override_redirect = true;
|
|
window_attr.event_mask = StructureNotifyMask | PointerMotionMask;
|
|
window_attr.colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo->visual, AllocNone);
|
|
const Window window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, width, height, 0, vinfo->depth, InputOutput, vinfo->visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr);
|
|
if(window) {
|
|
set_window_size_not_resizable(dpy, window, width, height);
|
|
set_window_shape_cross(dpy, window, width, height, 5);
|
|
make_window_click_through(dpy, window);
|
|
}
|
|
return window;
|
|
}
|
|
|
|
static void draw_rectangle_around_selected_monitor(Display *dpy, Window window, GC region_gc, int region_border_size, bool is_wayland, const std::vector<Monitor> &monitors, mgl::vec2i cursor_pos) {
|
|
const Monitor *focused_monitor = nullptr;
|
|
for(const Monitor &monitor : monitors) {
|
|
if(cursor_pos.x >= monitor.position.x && cursor_pos.x <= monitor.position.x + monitor.size.x
|
|
&& cursor_pos.y >= monitor.position.y && cursor_pos.y <= monitor.position.y + monitor.size.y)
|
|
{
|
|
focused_monitor = &monitor;
|
|
break;
|
|
}
|
|
}
|
|
|
|
int x = 0;
|
|
int y = 0;
|
|
int width = 0;
|
|
int height = 0;
|
|
if(focused_monitor) {
|
|
x = focused_monitor->position.x;
|
|
y = focused_monitor->position.y;
|
|
width = focused_monitor->size.x;
|
|
height = focused_monitor->size.y;
|
|
}
|
|
|
|
if(is_wayland)
|
|
draw_rectangle(dpy, window, region_gc, x, y, width, height);
|
|
else
|
|
set_region_rectangle(dpy, window, x, y, width, height, region_border_size);
|
|
}
|
|
|
|
static void update_cursor_window(Display *dpy, Window window, Window cursor_window, bool is_wayland, int cursor_x, int cursor_y, int cursor_window_size, int thickness, GC cursor_gc) {
|
|
if(is_wayland) {
|
|
const int x = cursor_x - cursor_window_size / 2;
|
|
const int y = cursor_y - cursor_window_size / 2;
|
|
XFillRectangle(dpy, window, cursor_gc, x + cursor_window_size / 2 - thickness / 2 , y, thickness, cursor_window_size);
|
|
XFillRectangle(dpy, window, cursor_gc, x, y + cursor_window_size / 2 - thickness / 2, cursor_window_size, thickness);
|
|
} else {
|
|
XMoveWindow(dpy, cursor_window, cursor_x - cursor_window_size / 2, cursor_y - cursor_window_size / 2);
|
|
}
|
|
XFlush(dpy);
|
|
}
|
|
|
|
static bool is_xwayland(Display *dpy) {
|
|
int opcode, event, error;
|
|
return XQueryExtension(dpy, "XWAYLAND", &opcode, &event, &error);
|
|
}
|
|
|
|
static unsigned long mgl_color_to_x11_color(mgl::Color color) {
|
|
if(color.a == 0)
|
|
return 0;
|
|
return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF);
|
|
}
|
|
|
|
RegionSelector::RegionSelector() {
|
|
|
|
}
|
|
|
|
RegionSelector::~RegionSelector() {
|
|
stop();
|
|
}
|
|
|
|
bool RegionSelector::start(mgl::Color border_color) {
|
|
if(dpy)
|
|
return false;
|
|
|
|
const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color);
|
|
dpy = XOpenDisplay(nullptr);
|
|
if(!dpy) {
|
|
fprintf(stderr, "Error: RegionSelector::start: failed to connect to the X11 server\n");
|
|
return false;
|
|
}
|
|
|
|
xi_opcode = 0;
|
|
if(!xinput_is_supported(dpy, &xi_opcode)) {
|
|
fprintf(stderr, "Error: RegionSelector::start: xinput not supported on your system\n");
|
|
stop();
|
|
return false;
|
|
}
|
|
|
|
is_wayland = is_xwayland(dpy);
|
|
monitors = get_monitors(dpy);
|
|
|
|
Window x11_cursor_window = None;
|
|
cursor_pos = get_cursor_position(dpy, &x11_cursor_window);
|
|
region.pos = {0, 0};
|
|
region.size = {0, 0};
|
|
|
|
XVisualInfo vinfo;
|
|
memset(&vinfo, 0, sizeof(vinfo));
|
|
XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo);
|
|
region_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone);
|
|
|
|
XSetWindowAttributes window_attr;
|
|
window_attr.background_pixel = is_wayland ? 0 : border_color_x11;
|
|
window_attr.border_pixel = 0;
|
|
window_attr.override_redirect = true;
|
|
window_attr.event_mask = StructureNotifyMask | PointerMotionMask;
|
|
window_attr.colormap = region_window_colormap;
|
|
|
|
Screen *screen = XDefaultScreenOfDisplay(dpy);
|
|
region_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0,
|
|
vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr);
|
|
if(!region_window) {
|
|
fprintf(stderr, "Error: RegionSelector::start: failed to create region window\n");
|
|
stop();
|
|
return false;
|
|
}
|
|
set_window_size_not_resizable(dpy, region_window, XWidthOfScreen(screen), XHeightOfScreen(screen));
|
|
|
|
if(!is_wayland) {
|
|
cursor_window = create_cursor_window(dpy, cursor_window_size, cursor_window_size, &vinfo, border_color_x11);
|
|
if(!cursor_window)
|
|
fprintf(stderr, "Warning: RegionSelector::start: failed to create cursor window\n");
|
|
set_region_rectangle(dpy, region_window, 0, 0, 0, 0, 0);
|
|
}
|
|
|
|
XGCValues region_gc_values;
|
|
memset(®ion_gc_values, 0, sizeof(region_gc_values));
|
|
region_gc_values.foreground = border_color_x11;
|
|
region_gc_values.line_width = region_border_size;
|
|
region_gc_values.line_style = LineSolid;
|
|
region_gc = XCreateGC(dpy, region_window, GCForeground | GCLineWidth | GCLineStyle, ®ion_gc_values);
|
|
|
|
XGCValues cursor_gc_values;
|
|
memset(&cursor_gc_values, 0, sizeof(cursor_gc_values));
|
|
cursor_gc_values.foreground = border_color_x11;
|
|
cursor_gc_values.line_width = cursor_thickness;
|
|
cursor_gc_values.line_style = LineSolid;
|
|
cursor_gc = XCreateGC(dpy, region_window, GCForeground | GCLineWidth | GCLineStyle, &cursor_gc_values);
|
|
|
|
if(!region_gc || !cursor_gc) {
|
|
fprintf(stderr, "Error: RegionSelector::start: failed to create gc\n");
|
|
stop();
|
|
return false;
|
|
}
|
|
|
|
XMapWindow(dpy, region_window);
|
|
make_window_sticky(dpy, region_window);
|
|
hide_window_from_taskbar(dpy, region_window);
|
|
XFixesHideCursor(dpy, region_window);
|
|
XGrabPointer(dpy, DefaultRootWindow(dpy), True, ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
|
|
xi_grab_all_mouse_devices(dpy);
|
|
XFlush(dpy);
|
|
|
|
window_set_fullscreen(dpy, region_window, true);
|
|
|
|
if(!is_wayland || x11_cursor_window)
|
|
update_cursor_window(dpy, region_window, cursor_window, is_wayland, cursor_pos.x, cursor_pos.y, cursor_window_size, cursor_thickness, cursor_gc);
|
|
|
|
if(cursor_window) {
|
|
XMapWindow(dpy, cursor_window);
|
|
make_window_sticky(dpy, cursor_window);
|
|
hide_window_from_taskbar(dpy, cursor_window);
|
|
}
|
|
|
|
draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos);
|
|
|
|
XFlush(dpy);
|
|
selected = false;
|
|
return true;
|
|
}
|
|
|
|
void RegionSelector::stop() {
|
|
if(!dpy)
|
|
return;
|
|
|
|
XWarpPointer(dpy, DefaultRootWindow(dpy), DefaultRootWindow(dpy), 0, 0, 0, 0, cursor_pos.x, cursor_pos.y);
|
|
xi_warp_all_mouse_devices(dpy, cursor_pos);
|
|
XFixesShowCursor(dpy, region_window);
|
|
|
|
XUngrabPointer(dpy, CurrentTime);
|
|
xi_ungrab_all_mouse_devices(dpy);
|
|
XFlush(dpy);
|
|
|
|
if(region_gc) {
|
|
XFreeGC(dpy, region_gc);
|
|
region_gc = nullptr;
|
|
}
|
|
|
|
if(cursor_gc) {
|
|
XFreeGC(dpy, cursor_gc);
|
|
cursor_gc = nullptr;
|
|
}
|
|
|
|
if(region_window_colormap) {
|
|
XFreeColormap(dpy, region_window_colormap);
|
|
region_window_colormap = 0;
|
|
}
|
|
|
|
if(region_window) {
|
|
XDestroyWindow(dpy, region_window);
|
|
region_window = 0;
|
|
}
|
|
|
|
XCloseDisplay(dpy);
|
|
dpy = nullptr;
|
|
selecting_region = false;
|
|
}
|
|
|
|
bool RegionSelector::is_started() const {
|
|
return dpy != nullptr;
|
|
}
|
|
|
|
bool RegionSelector::failed() const {
|
|
return !dpy;
|
|
}
|
|
|
|
bool RegionSelector::poll_events() {
|
|
if(!dpy || selected)
|
|
return false;
|
|
|
|
XEvent xev;
|
|
while(XPending(dpy)) {
|
|
XNextEvent(dpy, &xev);
|
|
XGenericEventCookie *cookie = &xev.xcookie;
|
|
if(cookie->type != GenericEvent || cookie->extension != xi_opcode || !XGetEventData(dpy, cookie))
|
|
continue;
|
|
|
|
const XIDeviceEvent *de = (XIDeviceEvent*)cookie->data;
|
|
switch(cookie->evtype) {
|
|
case XI_ButtonPress: {
|
|
on_button_press(de);
|
|
break;
|
|
}
|
|
case XI_ButtonRelease: {
|
|
on_button_release(de);
|
|
break;
|
|
}
|
|
case XI_Motion: {
|
|
on_mouse_motion(de);
|
|
break;
|
|
}
|
|
}
|
|
XFreeEventData(dpy, cookie);
|
|
|
|
if(selected) {
|
|
stop();
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool RegionSelector::is_selected() const {
|
|
return selected;
|
|
}
|
|
|
|
bool RegionSelector::take_selection() {
|
|
const bool result = selected;
|
|
selected = false;
|
|
return result;
|
|
}
|
|
|
|
Region RegionSelector::get_selection() const {
|
|
return region;
|
|
}
|
|
|
|
void RegionSelector::on_button_press(const void *de) {
|
|
const XIDeviceEvent *device_event = (XIDeviceEvent*)de;
|
|
if(device_event->detail != Button1)
|
|
return;
|
|
|
|
region.pos = { (int)device_event->root_x, (int)device_event->root_y };
|
|
selecting_region = true;
|
|
}
|
|
|
|
void RegionSelector::on_button_release(const void *de) {
|
|
const XIDeviceEvent *device_event = (XIDeviceEvent*)de;
|
|
if(device_event->detail != Button1)
|
|
return;
|
|
|
|
if(!selecting_region)
|
|
return;
|
|
|
|
if(is_wayland) {
|
|
XClearWindow(dpy, region_window);
|
|
XFlush(dpy);
|
|
} else {
|
|
set_region_rectangle(dpy, region_window, 0, 0, 0, 0, 0);
|
|
}
|
|
selecting_region = false;
|
|
|
|
cursor_pos = region.pos + region.size;
|
|
|
|
if(region.size.x < 0) {
|
|
region.pos.x += region.size.x;
|
|
region.size.x = abs(region.size.x);
|
|
}
|
|
|
|
if(region.size.y < 0) {
|
|
region.pos.y += region.size.y;
|
|
region.size.y = abs(region.size.y);
|
|
}
|
|
|
|
if(region.size.x > 0)
|
|
region.size.x += 1;
|
|
|
|
if(region.size.y > 0)
|
|
region.size.y += 1;
|
|
|
|
selected = true;
|
|
}
|
|
|
|
void RegionSelector::on_mouse_motion(const void *de) {
|
|
const XIDeviceEvent *device_event = (XIDeviceEvent*)de;
|
|
XClearWindow(dpy, region_window);
|
|
if(selecting_region) {
|
|
region.size.x = device_event->root_x - region.pos.x;
|
|
region.size.y = device_event->root_y - region.pos.y;
|
|
cursor_pos = region.pos + region.size;
|
|
|
|
if(is_wayland)
|
|
draw_rectangle(dpy, region_window, region_gc, region.pos.x, region.pos.y, region.size.x, region.size.y);
|
|
else
|
|
set_region_rectangle(dpy, region_window, region.pos.x, region.pos.y, region.size.x, region.size.y, region_border_size);
|
|
} else {
|
|
cursor_pos = { (int)device_event->root_x, (int)device_event->root_y };
|
|
draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos);
|
|
}
|
|
update_cursor_window(dpy, region_window, cursor_window, is_wayland, cursor_pos.x, cursor_pos.y, cursor_window_size, cursor_thickness, cursor_gc);
|
|
XFlush(dpy);
|
|
}
|
|
} |