Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions v3/UNRELEASED_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ After processing, the content will be moved to the main changelog and this file

## Fixed
<!-- Bug fixes -->
- 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
<!-- Soon-to-be removed features -->
Expand Down
68 changes: 68 additions & 0 deletions v3/pkg/application/linux_cgo.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#ifdef GDK_WINDOWING_X11
#include <gdk/x11/gdkx.h>
#include <X11/Xatom.h>
#include <dlfcn.h>

// Function pointer types for Xlib functions loaded at runtime via dlsym.
Expand All @@ -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) {
Expand All @@ -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

Expand Down Expand Up @@ -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));
Expand Down
10 changes: 10 additions & 0 deletions v3/pkg/application/linux_cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions v3/pkg/application/linux_cgo.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
19 changes: 19 additions & 0 deletions v3/pkg/application/systemtray_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Loading