diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 33638e7fc86..4b36422ca3d 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -23,6 +23,7 @@ After processing, the content will be moved to the main changelog and this file ## Fixed +- Fix a system tray window being positioned over the taskbar on Linux by using the desktop work area rather than the full screen (#6019) ## Deprecated diff --git a/v3/pkg/application/linux_cgo.c b/v3/pkg/application/linux_cgo.c index 29d4da0c9df..2ab8a8d9460 100644 --- a/v3/pkg/application/linux_cgo.c +++ b/v3/pkg/application/linux_cgo.c @@ -4,6 +4,7 @@ #ifdef GDK_WINDOWING_X11 #include +#include #include // Function pointer types for Xlib functions loaded at runtime via dlsym. @@ -14,12 +15,20 @@ typedef int (*WailsXFlushFunc)(Display*); typedef Bool (*WailsXTranslateCoordinatesFunc)(Display*, Window, Window, int, int, int*, int*, Window*); typedef Status (*WailsXSendEventFunc)(Display*, Window, Bool, long, XEvent*); typedef Atom (*WailsXInternAtomFunc)(Display*, const char*, Bool); +typedef Display* (*WailsXOpenDisplayFunc)(const char*); +typedef int (*WailsXCloseDisplayFunc)(Display*); +typedef int (*WailsXGetWindowPropertyFunc)(Display*, Window, Atom, long, long, Bool, Atom, Atom*, int*, unsigned long*, unsigned long*, unsigned char**); +typedef int (*WailsXFreeFunc)(void*); static WailsXMoveWindowFunc wails_XMoveWindow = NULL; static WailsXFlushFunc wails_XFlush = NULL; static WailsXTranslateCoordinatesFunc wails_XTranslateCoordinates = NULL; static WailsXSendEventFunc wails_XSendEvent = NULL; static WailsXInternAtomFunc wails_XInternAtom = NULL; +static WailsXOpenDisplayFunc wails_XOpenDisplay = NULL; +static WailsXCloseDisplayFunc wails_XCloseDisplay = NULL; +static WailsXGetWindowPropertyFunc wails_XGetWindowProperty = NULL; +static WailsXFreeFunc wails_XFree = NULL; static gboolean x11_funcs_resolved = FALSE; static void resolve_x11_funcs(void) { @@ -30,6 +39,10 @@ static void resolve_x11_funcs(void) { wails_XTranslateCoordinates = (WailsXTranslateCoordinatesFunc)dlsym(RTLD_DEFAULT, "XTranslateCoordinates"); wails_XSendEvent = (WailsXSendEventFunc)dlsym(RTLD_DEFAULT, "XSendEvent"); wails_XInternAtom = (WailsXInternAtomFunc)dlsym(RTLD_DEFAULT, "XInternAtom"); + wails_XOpenDisplay = (WailsXOpenDisplayFunc)dlsym(RTLD_DEFAULT, "XOpenDisplay"); + wails_XCloseDisplay = (WailsXCloseDisplayFunc)dlsym(RTLD_DEFAULT, "XCloseDisplay"); + wails_XGetWindowProperty = (WailsXGetWindowPropertyFunc)dlsym(RTLD_DEFAULT, "XGetWindowProperty"); + wails_XFree = (WailsXFreeFunc)dlsym(RTLD_DEFAULT, "XFree"); } #endif @@ -996,6 +1009,61 @@ void clipboard_free_text(char *text) { // Window position (X11 only) // ============================================================================ + +// screen_work_area reports the desktop area left free by panels and docks. +// GTK4 removed gdk_monitor_get_workarea and Wayland has no protocol for it, so +// Screen.WorkArea is the whole monitor on this backend and anything positioning +// itself lands under the taskbar. +// +// The window manager still publishes _NET_WORKAREA on the X11 root window, and +// a Wayland session keeps it current through XWayland, so it is read from there +// — including from a Wayland app, whose process already has libX11 mapped +// because GTK links both backends. Returns FALSE when there is no X display at +// all, leaving the caller on the full monitor as before. +gboolean screen_work_area(int *x, int *y, int *width, int *height) { +#ifdef GDK_WINDOWING_X11 + resolve_x11_funcs(); + if (wails_XOpenDisplay == NULL || wails_XGetWindowProperty == NULL || + wails_XInternAtom == NULL || wails_XCloseDisplay == NULL) { + return FALSE; + } + + Display *display = wails_XOpenDisplay(NULL); + if (display == NULL) return FALSE; + + gboolean ok = FALSE; + Atom property = wails_XInternAtom(display, "_NET_WORKAREA", True); + if (property != None) { + Atom type = None; + int format = 0; + unsigned long items = 0, after = 0; + unsigned char *data = NULL; + + // Only the first desktop's rectangle is read: a per-desktop work area + // is a concept no other platform here has. + if (wails_XGetWindowProperty(display, DefaultRootWindow(display), property, + 0, 4, False, XA_CARDINAL, + &type, &format, &items, &after, &data) == Success) { + if (data != NULL && items >= 4 && format == 32) { + long *area = (long *)data; + *x = (int)area[0]; + *y = (int)area[1]; + *width = (int)area[2]; + *height = (int)area[3]; + ok = *width > 0 && *height > 0; + } + if (data != NULL && wails_XFree != NULL) wails_XFree(data); + } + } + + wails_XCloseDisplay(display); + return ok; +#else + (void)x; (void)y; (void)width; (void)height; + return FALSE; +#endif +} + void window_move_x11(GtkWindow *window, int x, int y) { #ifdef GDK_WINDOWING_X11 GtkNative *native = gtk_widget_get_native(GTK_WIDGET(window)); diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index e4751c88262..2a3a2d346fc 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -1336,6 +1336,16 @@ func (w *linuxWebviewWindow) setResizable(resizable bool) { w.execJS(fmt.Sprintf("if(window._wails&&window._wails.setResizable)window._wails.setResizable(%v);", resizable)) } +// screenWorkArea returns the area panels and docks leave free, and whether it +// could be determined at all. +func screenWorkArea() (x, y, width, height int, ok bool) { + var cx, cy, cw, ch C.int + if C.screen_work_area(&cx, &cy, &cw, &ch) == 0 { + return 0, 0, 0, 0, false + } + return int(cx), int(cy), int(cw), int(ch), true +} + func (w *linuxWebviewWindow) move(x, y int) { // C-side GDK_IS_X11_DISPLAY check handles X11 vs Wayland correctly, // including XWayland and GDK_BACKEND=x11 scenarios. diff --git a/v3/pkg/application/linux_cgo.h b/v3/pkg/application/linux_cgo.h index 52eafd88dd2..21af07960cb 100644 --- a/v3/pkg/application/linux_cgo.h +++ b/v3/pkg/application/linux_cgo.h @@ -158,6 +158,7 @@ void beginWindowResize(GtkWindow *window, GdkSurfaceEdge edge, int button, doubl // Window position (X11 only) // ============================================================================ +gboolean screen_work_area(int *x, int *y, int *width, int *height); void window_move_x11(GtkWindow *window, int x, int y); void window_get_position_x11(GtkWindow *window, int *x, int *y); void window_set_always_on_top(GtkWindow *window, gboolean always_on_top); diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index fc8307ee88f..21ee980b974 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -254,10 +254,29 @@ func (s *linuxSystemTray) positionWindow(window Window, offset int) error { return fmt.Errorf("unable to get screen information") } + // The area panels and docks leave free, so the window is not laid over the + // taskbar. currentScreen.WorkArea is the whole monitor on this backend — + // GTK4 dropped the API it came from — so it is asked for separately. + // + // What comes back describes the whole desktop rather than one monitor, so + // it is intersected with the screen the window is opening on: taking it + // as-is would let a window near a shared edge be placed on the neighbouring + // monitor. An empty intersection means the two disagree, and the monitor + // wins. screenX := currentScreen.X screenY := currentScreen.Y screenWidth := currentScreen.Size.Width screenHeight := currentScreen.Size.Height + if x, y, width, height, ok := screenWorkArea(); ok { + left := max(screenX, x) + top := max(screenY, y) + right := min(screenX+screenWidth, x+width) + bottom := min(screenY+screenHeight, y+height) + if right > left && bottom > top { + screenX, screenY, screenWidth, screenHeight = left, top, right-left, bottom-top + } + } + windowWidth := window.Width() windowHeight := window.Height()