diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main.cpp | 394 |
1 files changed, 371 insertions, 23 deletions
diff --git a/src/main.cpp b/src/main.cpp index 565eabd..7c62591 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,9 +16,10 @@ #include <assert.h> #include <unistd.h> #include <limits.h> +#include <poll.h> #include <X11/Xlib.h> -#include <X11/extensions/Xfixes.h> +#include <X11/Xatom.h> #include <X11/extensions/shape.h> extern "C" { @@ -91,13 +92,25 @@ static Bool make_window_sticky(Display* display, Window window) { 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); + XShapeCombineRectangles(display, window, ShapeInput, 0, 0, &rect, 1, ShapeSet, YXBanded); +} + +static void set_window_clip_region(Display *display, Window window, mgl::vec2i pos, mgl::vec2i size) { + if(size.x < 0) + size.x = 0; + if(size.y < 0) + size.y = 0; + XRectangle rectangle = {(short)pos.x, (short)pos.y, (unsigned short)size.x, (unsigned short)size.y}; + XShapeCombineRectangles(display, window, ShapeBounding, 0, 0, &rectangle, 1, ShapeSet, YXBanded); +} + +static bool is_xwayland(Display *display) { + int opcode, event, error; + return XQueryExtension(display, "XWAYLAND", &opcode, &event, &error); } static void usage() { - fprintf(stderr, "usage: gsr-notify <--text text> <--timeout timeout> [--icon filepath] [--icon-color color] [--bg-color color]\n"); + fprintf(stderr, "usage: gsr-notify --text text --timeout timeout [--icon filepath] [--icon-color color] [--bg-color color]\n"); fprintf(stderr, "options:\n"); fprintf(stderr, " --text The text to display in the notification. Required.\n"); fprintf(stderr, " --timeout The time to display the notification in seconds (excluding animation time), expected to be a floating point number. Required.\n"); @@ -145,22 +158,321 @@ static mgl::Color parse_hex_color(const char *str) { } // Returns the first monitor if not found. Assumes there is at least one monitor connected. -static const mgl_monitor* find_monitor_by_cursor_position(mgl::Window &window) { +static const mgl_monitor* find_monitor_at_position(mgl::Window &window, mgl::vec2i pos) { const mgl_window *win = window.internal_window(); assert(win->num_monitors > 0); for(int i = 0; i < win->num_monitors; ++i) { const mgl_monitor *mon = &win->monitors[i]; - if(mgl::IntRect({ mon->pos.x, mon->pos.y }, { mon->size.x, mon->size.y }).contains({ win->cursor_position.x, win->cursor_position.y })) + if(mgl::IntRect({ mon->pos.x, mon->pos.y }, { mon->size.x, mon->size.y }).contains(pos)) return mon; } return &win->monitors[0]; } +static bool window_has_atom(Display *dpy, Window window, Atom atom) { + Atom type; + unsigned long len, bytes_left; + int format; + unsigned char *properties = NULL; + if(XGetWindowProperty(dpy, window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success) + return false; + + if(properties) + XFree(properties); + + return type != None; +} + +static bool window_is_user_program(Display *dpy, Window window) { + const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); + const Atom wm_state_atom = XInternAtom(dpy, "WM_STATE", False); + return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom); +} + +static Window window_get_target_window_child(Display *display, Window window) { + if(window == None) + return None; + + if(window_is_user_program(display, window)) + return window; + + Window root; + Window parent; + Window *children = nullptr; + unsigned int num_children = 0; + if(!XQueryTree(display, window, &root, &parent, &children, &num_children) || !children) + return None; + + Window found_window = None; + for(int i = num_children - 1; i >= 0; --i) { + if(children[i] && window_is_user_program(display, children[i])) { + found_window = children[i]; + goto finished; + } + } + + for(int i = num_children - 1; i >= 0; --i) { + if(children[i]) { + Window win = window_get_target_window_child(display, children[i]); + if(win) { + found_window = win; + goto finished; + } + } + } + + finished: + XFree(children); + return found_window; +} + +static bool window_has_title(Display *dpy, Window window) { + bool result = false; + const Atom net_wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False); + const Atom wm_name_atom = XInternAtom(dpy, "WM_NAME", False); + const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False); + + Atom type = None; + int format = 0; + unsigned long num_items = 0; + unsigned long bytes_left = 0; + unsigned char *data = NULL; + XGetWindowProperty(dpy, window, net_wm_name_atom, 0, 1024, False, utf8_string_atom, &type, &format, &num_items, &bytes_left, &data); + + if(type == utf8_string_atom && format == 8 && data) { + result = true; + goto done; + } + + if(data) + XFree(data); + + type = None; + format = 0; + num_items = 0; + bytes_left = 0; + data = NULL; + XGetWindowProperty(dpy, window, wm_name_atom, 0, 1024, False, 0, &type, &format, &num_items, &bytes_left, &data); + + if((type == XA_STRING || type == utf8_string_atom) && data) { + result = true; + goto done; + } + + done: + if(data) + XFree(data); + return result; +} + +static mgl::vec2i get_cursor_position(Display *dpy, Window *window) { + Window root_window = None; + *window = None; + int dummy_i; + unsigned int dummy_u; + mgl::vec2i root_pos; + XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u); + + const Window direct_window = *window; + *window = window_get_target_window_child(dpy, *window); + // HACK: Count some other x11 windows as having an x11 window focused. Some games seem to create an Input window and that gets focused. + if(!*window) { + XWindowAttributes attr; + memset(&attr, 0, sizeof(attr)); + XGetWindowAttributes(dpy, direct_window, &attr); + if(attr.c_class == InputOnly && !window_has_title(dpy, direct_window)) + *window = direct_window; + } + return root_pos; +} + +typedef struct { + unsigned long flags; + unsigned long functions; + unsigned long decorations; + long input_mode; + unsigned long status; +} MotifHints; + +#define MWM_HINTS_DECORATIONS 2 + +#define MWM_DECOR_NONE 0 +#define MWM_DECOR_ALL 1 + +static void window_set_decorations_visible(Display *display, Window window, bool visible) { + const Atom motif_wm_hints_atom = XInternAtom(display, "_MOTIF_WM_HINTS", False); + MotifHints motif_hints; + memset(&motif_hints, 0, sizeof(motif_hints)); + motif_hints.flags = MWM_HINTS_DECORATIONS; + motif_hints.decorations = visible ? MWM_DECOR_ALL : MWM_DECOR_NONE; + XChangeProperty(display, window, motif_wm_hints_atom, motif_wm_hints_atom, 32, PropModeReplace, (unsigned char*)&motif_hints, sizeof(motif_hints) / sizeof(long)); +} + +static bool create_window_get_center_position_kde(Display *display, mgl::vec2i &position) { + const int size = 1; + XSetWindowAttributes window_attr; + window_attr.event_mask = StructureNotifyMask; + window_attr.background_pixel = 0; + const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr); + if(!window) + return false; + + const Atom net_wm_window_type_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False); + const Atom net_wm_window_type_notification_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE_NOTIFICATION", False); + const Atom net_wm_window_type_utility = XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False); + const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False); + + const Atom window_type_atoms[2] = { + net_wm_window_type_notification_atom, + net_wm_window_type_utility + }; + XChangeProperty(display, window, net_wm_window_type_atom, XA_ATOM, 32, PropModeReplace, (const unsigned char*)window_type_atoms, 2L); + + const double alpha = 0.0; + const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha); + XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L); + + window_set_decorations_visible(display, window, false); + + XSizeHints *size_hints = XAllocSizeHints(); + size_hints->width = size; + size_hints->height = size; + size_hints->min_width = size; + size_hints->min_height = size; + size_hints->max_width = size; + size_hints->max_height = size; + size_hints->flags = PSize | PMinSize | PMaxSize; + XSetWMNormalHints(display, window, size_hints); + XFree(size_hints); + + XMapWindow(display, window); + XFlush(display); + + bool got_data = false; + const int x_fd = XConnectionNumber(display); + XEvent xev; + while(true) { + struct pollfd poll_fd; + poll_fd.fd = x_fd; + poll_fd.events = POLLIN; + poll_fd.revents = 0; + const int fds_ready = poll(&poll_fd, 1, 200); + if(fds_ready == 0) { + fprintf(stderr, "Error: timed out waiting for ConfigureNotify after XCreateWindow\n"); + break; + } else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) { + continue; + } + + while(XPending(display)) { + XNextEvent(display, &xev); + if(xev.type == ConfigureNotify && xev.xconfigure.window == window) { + got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0; + position.x = xev.xconfigure.x + xev.xconfigure.width / 2; + position.y = xev.xconfigure.y + xev.xconfigure.height / 2; + goto done; + } + } + } + + done: + XDestroyWindow(display, window); + XFlush(display); + + return got_data; +} + +static bool create_window_get_center_position_gnome(Display *display, mgl::vec2i &position) { + const int size = 32; + XSetWindowAttributes window_attr; + window_attr.event_mask = StructureNotifyMask | ExposureMask; + window_attr.background_pixel = 0; + const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr); + if(!window) + return false; + + const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False); + const double alpha = 0.0; + const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha); + XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L); + + window_set_decorations_visible(display, window, false); + + XSizeHints *size_hints = XAllocSizeHints(); + size_hints->width = size; + size_hints->height = size; + size_hints->min_width = size; + size_hints->min_height = size; + size_hints->max_width = size; + size_hints->max_height = size; + size_hints->flags = PSize | PMinSize | PMaxSize; + XSetWMNormalHints(display, window, size_hints); + XFree(size_hints); + + XMapWindow(display, window); + XFlush(display); + + bool got_data = false; + const int x_fd = XConnectionNumber(display); + XEvent xev; + while(true) { + struct pollfd poll_fd; + poll_fd.fd = x_fd; + poll_fd.events = POLLIN; + poll_fd.revents = 0; + const int fds_ready = poll(&poll_fd, 1, 200); + if(fds_ready == 0) { + fprintf(stderr, "Error: timed out waiting for MapNotify/ConfigureNotify after XCreateWindow\n"); + break; + } else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) { + continue; + } + + while(XPending(display)) { + XNextEvent(display, &xev); + if(xev.type == MapNotify && xev.xmap.window == window) { + int x = 0; + int y = 0; + Window w = None; + XTranslateCoordinates(display, window, DefaultRootWindow(display), 0, 0, &x, &y, &w); + + got_data = x > 0 && y > 0; + position.x = x + size / 2; + position.y = y + size / 2; + if(got_data) + goto done; + } else if(xev.type == ConfigureNotify && xev.xconfigure.window == window) { + got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0; + position.x = xev.xconfigure.x + xev.xconfigure.width / 2; + position.y = xev.xconfigure.y + xev.xconfigure.height / 2; + if(got_data) + goto done; + } + } + } + + done: + XDestroyWindow(display, window); + XFlush(display); + + return got_data; +} + +static mgl::vec2i create_window_get_center_position(Display *display) { + mgl::vec2i pos; + if(!create_window_get_center_position_kde(display, pos)) { + pos.x = 0; + pos.y = 0; + create_window_get_center_position_gnome(display, pos); + } + return pos; +} + int main(int argc, char **argv) { setlocale(LC_ALL, "C"); // Sigh... stupid C std::string resources_path; - if(access("sibs-build", F_OK) == 0) { + if(access("sibs-build/linux_x86_64/debug/gsr-notify", F_OK) == 0) { resources_path = "./"; } else { #ifdef GSR_NOTIFY_RESOURCES_PATH @@ -225,28 +537,36 @@ int main(int argc, char **argv) { const mgl::Color bg_color = parse_hex_color(bg_color_str ? bg_color_str : "76b900"); mgl::Init init; + Display *display = (Display*)mgl_get_context()->connection; + const bool wayland = is_xwayland(display); + const bool use_transparency = wayland; mgl::Window::CreateParams window_create_params; - window_create_params.size = { 1280, 720 }; + window_create_params.size = { 32, 32 }; window_create_params.min_size = window_create_params.size; window_create_params.max_size = window_create_params.size; window_create_params.hidden = true; window_create_params.override_redirect = true; - window_create_params.background_color = bg_color; + window_create_params.support_alpha = true; + window_create_params.background_color = use_transparency ? mgl::Color(0, 0, 0, 0) : bg_color; window_create_params.hide_decorations = true; window_create_params.window_type = MGL_WINDOW_TYPE_NOTIFICATION; mgl::Window window; - if(!window.create("GPU Screen Recorder Notification", window_create_params)) + if(!window.create("gsr notify", window_create_params)) return 1; - mgl_window *win = window.internal_window(); + const mgl_window *win = window.internal_window(); if(win->num_monitors == 0) { fprintf(stderr, "Error: no monitors found\n"); exit(1); } - const mgl_monitor *focused_monitor = find_monitor_by_cursor_position(window); + // The cursor position is wrong on wayland if an x11 window is not focused. On wayland we instead create a window and get the position where the wayland compositor puts it + Window x11_cursor_window = None; + const mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window); + const mgl::vec2i monitor_position_query_value = (x11_cursor_window || !wayland) ? cursor_position : create_window_get_center_position(display); + const mgl_monitor *focused_monitor = find_monitor_at_position(window, monitor_position_query_value); const std::string noto_sans_bold_filepath = resources_path + "fonts/NotoSans-Bold.ttf"; mgl::MemoryMappedFile font_file; @@ -281,7 +601,7 @@ int main(int argc, char **argv) { text.set_color(mgl::Color(255, 255, 255, 0)); mgl::Sprite logo_sprite; - float logo_sprite_padding_x = 20.0f; + float logo_sprite_padding_x = (int)(window_height * 0.5f); float padding_between_icon_and_text_x = 0.0f; if(logo_texture.is_valid()) { logo_sprite.set_texture(&logo_texture); @@ -291,14 +611,20 @@ int main(int argc, char **argv) { padding_between_icon_and_text_x = (int)(logo_sprite_padding_x * 0.7f); } - Display *display = (Display*)mgl_get_context()->connection; + unsigned char data = 1; // Prefer not being composed to not reduce display fps on AMD when an application is using 100% of GPU + XChangeProperty(display, window.get_system_handle(), XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); + + data = 1; + XChangeProperty(display, window.get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); // TODO: Make sure the notification always stays on top. Test with starting the notification and then opening youtube in fullscreen. const int window_width = content_padding_left + logo_sprite_padding_x + logo_sprite.get_size().x + padding_between_icon_and_text_x + text.get_bounds().size.x + logo_sprite_padding_x + padding_between_icon_and_text_x; const mgl::vec2i window_size{window_width, window_height}; - const mgl::vec2i window_start_position{focused_monitor->pos.x + focused_monitor->size.x, focused_monitor->pos.y + window_size.y}; + const mgl::vec2i window_start_position{focused_monitor->pos.x + focused_monitor->size.x - window_size.x, focused_monitor->pos.y + window_size.y}; window.set_size_limits(window_size, window_size); window.set_size(window_size); + if(!use_transparency) + set_window_clip_region(display, window.get_system_handle(), {0, 0}, {0, 0}); window.set_position(window_start_position); make_window_click_through(display, window.get_system_handle()); window.set_visible(true); @@ -322,6 +648,10 @@ int main(int argc, char **argv) { mgl::Clock state_timer; + mgl::Rectangle transparency_bg(window_size.to_vec2f()); + transparency_bg.set_color(bg_color); + transparency_bg.set_position({(float)window_size.x, 0.0f}); + mgl::Rectangle content_bg(window_size.to_vec2f() - mgl::vec2f(content_padding_left, 0.0f)); content_bg.set_color(mgl::Color(0, 0, 0)); @@ -353,8 +683,14 @@ int main(int argc, char **argv) { switch(current_state.state) { case State::SLIDE_IN_WINDOW: { - double new_slide_x = interpolate(slide_window_start_x, slide_window_end_x, state_interpolation); - window.set_position(mgl::vec2i(new_slide_x, window_start_position.y)); + const double new_slide_x = interpolate(slide_window_start_x, slide_window_end_x, state_interpolation); + const mgl::vec2i window_clip(std::max(0.0, slide_window_start_x - new_slide_x), window_size.y); + if(use_transparency) { + transparency_bg.set_size(window_clip.floor().to_vec2f()); + } else { + set_window_clip_region(display, window.get_system_handle(), {window_size.x - window_clip.x, 0}, window_clip); + XFlush(display); + } break; } case State::SLIDE_IN_CONTENT: { @@ -362,13 +698,13 @@ int main(int argc, char **argv) { break; } case State::FADE_IN_CONTENT: { - double new_alpha = interpolate(content_start_alpha, content_end_alpha, state_interpolation); + const double new_alpha = interpolate(content_start_alpha, content_end_alpha, state_interpolation); text.set_color(mgl::Color(255, 255, 255, new_alpha * 255.0f)); logo_sprite.set_color(mgl::Color(icon_color.r, icon_color.g, icon_color.b, new_alpha * 255.0f)); break; } case State::FADE_OUT_CONTENT: { - double new_alpha = interpolate(content_end_alpha, content_start_alpha, state_interpolation); + const double new_alpha = interpolate(content_end_alpha, content_start_alpha, state_interpolation); text.set_color(mgl::Color(255, 255, 255, new_alpha * 255.0f)); logo_sprite.set_color(mgl::Color(icon_color.r, icon_color.g, icon_color.b, new_alpha * 255.0f)); break; @@ -378,8 +714,14 @@ int main(int argc, char **argv) { break; } case State::SLIDE_OUT_WINDOW: { - double new_slide_x = interpolate(slide_window_end_x, slide_window_start_x, state_interpolation); - window.set_position(mgl::vec2i(new_slide_x, window_start_position.y)); + const double new_slide_x = interpolate(slide_window_end_x, slide_window_start_x, state_interpolation); + const mgl::vec2i window_clip(std::max(0.0, slide_window_start_x - new_slide_x), window_size.y); + if(use_transparency) { + transparency_bg.set_size(window_clip.floor().to_vec2f()); + } else { + set_window_clip_region(display, window.get_system_handle(), {window_size.x - window_clip.x, 0}, window_clip); + XFlush(display); + } break; } case State::PAUSE: { @@ -387,7 +729,13 @@ int main(int argc, char **argv) { } } - window.clear(bg_color); + if(use_transparency) { + window.clear(mgl::Color(0, 0, 0, 0)); + transparency_bg.set_position(mgl::vec2f(window_size.x - transparency_bg.get_size().x, 0.0f)); + window.draw(transparency_bg); + } else { + window.clear(bg_color); + } window.draw(content_bg); window.draw(logo_sprite); window.draw(text); |