aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.cpp394
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);