From 883ef55150ca4f73f0a59dba81490fec07f52b60 Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 24 Aug 2026 00:27:43 +0700 Subject: [PATCH 1/3] fix(linux): position tray windows against the real work area A window attached to the system tray is laid over the taskbar. positionWindow clamps against Screen.Size, and Screen.WorkArea is no help because on the GTK4 backend it is the whole monitor: gdk_monitor_get_workarea was removed in GTK4 and Wayland has no protocol for it, so there is currently nothing that reports the space panels and docks leave free. The window manager still publishes _NET_WORKAREA on the X11 root window, so that is read instead. A Wayland session keeps the property current through XWayland, and the process already has libX11 mapped there because GTK links both backends, so the existing dlsym-from-RTLD_DEFAULT approach reaches it without adding a link dependency. Where there is no X display at all, the caller keeps the full monitor it uses today. --- v3/pkg/application/linux_cgo.c | 68 ++++++++++++++++++++++++++ v3/pkg/application/linux_cgo.go | 10 ++++ v3/pkg/application/linux_cgo.h | 1 + v3/pkg/application/systemtray_linux.go | 7 +++ 4 files changed, 86 insertions(+) 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..f8afb5fa6d1 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -254,10 +254,17 @@ 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. screenX := currentScreen.X screenY := currentScreen.Y screenWidth := currentScreen.Size.Width screenHeight := currentScreen.Size.Height + if x, y, width, height, ok := screenWorkArea(); ok { + screenX, screenY, screenWidth, screenHeight = x, y, width, height + } + windowWidth := window.Width() windowHeight := window.Height() From a5d2cd1d51e60bf0aed35f6d184647879aa432b0 Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 24 Aug 2026 00:27:43 +0700 Subject: [PATCH 2/3] docs: changelog entry --- v3/UNRELEASED_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 33638e7fc86..5ebffe6f207 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 ## Deprecated From f0bee1880e5765f65a343db94516a69382f57123 Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 24 Aug 2026 00:53:05 +0700 Subject: [PATCH 3/3] fix(linux): clamp the work area to the screen being positioned on _NET_WORKAREA describes the whole desktop, not one monitor, so taking it as the bounds let a window opening near a shared edge be placed on the neighbouring monitor. It is intersected with the screen instead, and the monitor is kept where the two do not overlap. --- v3/UNRELEASED_CHANGELOG.md | 2 +- v3/pkg/application/systemtray_linux.go | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 5ebffe6f207..4b36422ca3d 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -23,7 +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 +- 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/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index f8afb5fa6d1..21ee980b974 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -257,12 +257,24 @@ func (s *linuxSystemTray) positionWindow(window Window, offset int) error { // 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 { - screenX, screenY, screenWidth, screenHeight = x, y, width, height + 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()