From 2cc53e96bf47a9dae1d9404343e9d077a36ab76b Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Mon, 14 Sep 2026 17:32:41 +0200 Subject: [PATCH] [GTK] Anchor Wayland display coordinates to the monitor of the window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wayland never tells a client where its window is, so gdk_window_get_origin returns window relative values while Monitor geometry is global. Code that clips a location against a monitor then mixes two coordinate spaces. The JFace information controls do exactly that. With a monitor layout whose client areas do not start near y=0, every anchor is rejected: hovers come up empty and the content assist javadoc popup lands on a different monitor. Wayland does report which monitor a surface is on. Anchor display coordinates to that origin, and remove it again when positioning a window or a menu. Display.getCursorLocation() and the MenuDetect event get the same treatment, since callers compare them against Control.toDisplay() and pass them to Menu.setLocation(). A shell tree shares one anchor, the monitor of its root toplevel: child shells are positioned relative to their parent, and a popup that is hidden or straddles a monitor edge reports no or a different monitor of its own. The window's position within its monitor is still unknown. Popups are placed relative to their parent and unaffected by that remaining error. X11 and GTK4 are unchanged. The two tests pass on a single monitor whose origin is 0,0, so they only bite on a multi-monitor Wayland session, but test_popupLocationRoundTrip covers the reuse cycle that JFace performs, which is where a wrong anchor shows up. Both are GTK3 only, since GTK4 toDisplay() is still shell relative. Fixes https://github.com/eclipse-platform/eclipse.platform.swt/issues/3539 Fixes https://github.com/eclipse-platform/eclipse.platform.swt/issues/538 Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../gtk/org/eclipse/swt/widgets/Control.java | 34 ++++++++++++++-- .../gtk/org/eclipse/swt/widgets/Display.java | 29 +++++++++++++- .../gtk/org/eclipse/swt/widgets/Menu.java | 8 +++- .../gtk/org/eclipse/swt/widgets/Shell.java | 30 ++++++++++++-- .../Test_org_eclipse_swt_widgets_Shell.java | 39 +++++++++++++++++++ 5 files changed, 130 insertions(+), 10 deletions(-) diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java index 965b701ff3e..a80e7ef063e 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java @@ -1698,8 +1698,9 @@ public Point toControl(int x, int y) { origin_x[0] = origin.x; origin_y[0] = origin.y; } else { - long window = eventWindow(); - GDK.gdk_window_get_origin(window, origin_x, origin_y); + Point origin = getWindowOrigin(); + origin_x[0] = origin.x; + origin_y[0] = origin.y; } x -= origin_x[0]; @@ -1763,8 +1764,9 @@ public Point toDisplay(int x, int y) { origin_x[0] = origin.x; origin_y[0] = origin.y; } else { - long window = eventWindow(); - GDK.gdk_window_get_origin(window, origin_x, origin_y); + Point origin = getWindowOrigin(); + origin_x[0] = origin.x; + origin_y[0] = origin.y; } if ((style & SWT.MIRRORED) != 0) x = getClientWidth() - x; @@ -6339,6 +6341,12 @@ boolean showMenu (int x, int y) { } boolean showMenu (int x, int y, int detail) { + // GTK3 callers pass root coordinates, which on Wayland lack the monitor origin of display coordinates. + Point monitorOrigin = monitorOrigin (); + if (monitorOrigin != null) { + x += monitorOrigin.x; + y += monitorOrigin.y; + } Event event = new Event (); Rectangle eventRect = new Rectangle (x, y, 0, 0); event.setBounds (eventRect); @@ -6924,9 +6932,27 @@ Point getWindowOrigin () { long window = eventWindow (); GDK.gdk_window_get_origin (window, x, y); + Point monitorOrigin = monitorOrigin (); + if (monitorOrigin != null) { + x [0] += monitorOrigin.x; + y [0] += monitorOrigin.y; + } + return new Point (x [0], y [0]); } +/** + * Offset that maps window relative GDK coordinates into the space of the monitor showing the + * receiver, or null when none is needed. Wayland reports no global position, so + * without it {@link Monitor} geometry and control coordinates cannot be compared. + */ +Point monitorOrigin () { + if (GTK.GTK4 || !OS.isWayland ()) return null; + // One anchor per shell tree: Shell walks up to the root toplevel, whose monitor is the + // space that popup positions are relative to. + return getShell ().monitorOrigin (); +} + /** * Gets the position of the top left corner of the control in root window (display) coordinates. * GTK4 only, do not call on GTK3. diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java index 7d54eb15ffe..3bd578575b0 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java @@ -2009,6 +2009,22 @@ boolean filters (int eventType) { return filterTable.hooks (eventType); } +/** + * Origin of the monitor showing the given GdkWindow, or null when no offset is + * needed. See {@link Control#monitorOrigin()}. + */ +Point monitorOrigin (long window) { + if (GTK.GTK4 || !OS.isWayland () || window == 0) return null; + long displayHandle = GDK.gdk_display_get_default (); + if (displayHandle == 0) return null; + long monitor = GDK.gdk_display_get_monitor_at_window (displayHandle, window); + if (monitor == 0) return null; + GdkRectangle geometry = new GdkRectangle (); + GDK.gdk_monitor_get_geometry (monitor, geometry); + if (geometry.x == 0 && geometry.y == 0) return null; + return new Point (geometry.x, geometry.y); +} + /** * Returns the location of the on-screen pointer relative * to the top left corner of the screen. @@ -2031,7 +2047,7 @@ public Point getCursorLocation() { x[0] = (int)xDouble[0]; y[0] = (int)yDouble[0]; } else { - getWindowPointerPosition(0, x, y, null); + long pointerWindow = getWindowPointerPosition(0, x, y, null); /* * Wayland feature: There is no global x/y coordinates in Wayland for security measures, so they @@ -2052,6 +2068,17 @@ public Point getCursorLocation() { y[0]+= offsetY[0]; tempShell = tempShell.getParent().getShell(); } + /* + * Callers compare this against Control.toDisplay(), so use the same space. + * Prefer the monitor of the window under the pointer, which is the one the + * coordinates are relative to; the active shell is only a fallback. + */ + Point origin = monitorOrigin (pointerWindow); + if (origin == null) origin = tempShell.monitorOrigin (); + if (origin != null) { + x[0] += origin.x; + y[0] += origin.y; + } } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java index dba5080844d..90c35b011fb 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java @@ -449,8 +449,12 @@ void _setVisible (boolean visible) { GTK3.memmove (eventPtr, event, GdkEventButton.sizeof); // Bug in GTK?: testing with SWT_MENU_LOCATION_DEBUGGING=1 shows final_rect.x and // final_rect.y popup menu position is off by 1 compared to this.x and this.y - rect.x = this.x + 1; - rect.y = this.y + 1; + // The rectangle is relative to the shell, so the monitor origin has to go. + Point monitorOrigin = getShell ().monitorOrigin (); + int originX = monitorOrigin != null ? monitorOrigin.x : 0; + int originY = monitorOrigin != null ? monitorOrigin.y : 0; + rect.x = this.x + 1 - originX; + rect.y = this.y + 1 - originY; } // Popup the menu and pin it at the top left corner of the GdkRectangle relative to the GdkWindow GTK3.gtk_menu_popup_at_rect(handle, event.window, rect, GDK.GDK_GRAVITY_NORTH_WEST, diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Shell.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Shell.java index 87f7d78f3a0..3d45185dbb6 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Shell.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Shell.java @@ -1362,10 +1362,28 @@ public Point getLocation() { // TODO: GTK4 GtkWindow no longer has the ability to get position } else { GTK3.gtk_window_get_position (shellHandle, x, y); + applyMonitorOrigin (x, y); } return new Point (x [0], y [0]); } +@Override +Point monitorOrigin () { + if (GTK.GTK4 || !OS.isWayland ()) return null; + // A child Shell is positioned relative to its parent, and while still hidden its own + // monitor is not even known, so the root toplevel is the anchor for the whole tree. + if (parent != null) return parent.monitorOrigin (); + return display.monitorOrigin (gtk_widget_get_window (topHandle ())); +} + +/** Shifts a window relative GTK position into display coordinates. */ +void applyMonitorOrigin (int [] x, int [] y) { + Point origin = monitorOrigin (); + if (origin == null) return; + x [0] += origin.x; + y [0] += origin.y; +} + @Override public boolean getMaximized () { checkWidget(); @@ -1624,6 +1642,7 @@ long gtk3_button_press_event (long widget, long event) { long gtk_configure_event (long widget, long event) { int [] x = new int [1], y = new int [1]; GTK3.gtk_window_get_position (shellHandle, x, y); + applyMonitorOrigin (x, y); if (!isVisible ()) { return 0; //We shouldn't handle move/resize events if shell is hidden. @@ -2468,9 +2487,13 @@ int setBounds (int x, int y, int width, int height, boolean move, boolean resize } if (mapped) positionPopover(); } else if (!GTK.GTK4) { + // GTK positions windows in its own space; x and y arrive in display coordinates. + Point origin = monitorOrigin (); + int gtkX = origin != null ? x - origin.x : x; + int gtkY = origin != null ? y - origin.y : y; int [] x_pos = new int [1], y_pos = new int [1]; GTK3.gtk_window_get_position(shellHandle, x_pos, y_pos); - GTK3.gtk_window_move(shellHandle, x, y); + GTK3.gtk_window_move(shellHandle, gtkX, gtkY); /* * Bug in GTK: gtk_window_get_position () is not always up-to-date right after * gtk_window_move (). The random delays cause problems like bug 445900. @@ -2482,11 +2505,11 @@ int setBounds (int x, int y, int width, int height, boolean move, boolean resize for (int i = 0; i < 1000; i++) { int [] x2_pos = new int [1], y2_pos = new int [1]; GTK3.gtk_window_get_position(shellHandle, x2_pos, y2_pos); - if (x2_pos[0] == x && y2_pos[0] == y) { + if (x2_pos[0] == gtkX && y2_pos[0] == gtkY) { break; } } - if (x_pos [0] != x || y_pos [0] != y) { + if (x_pos [0] != gtkX || y_pos [0] != gtkY) { moved = true; oldX = x; oldY = y; @@ -3663,6 +3686,7 @@ Rectangle getBoundsInPixels () { GDK.gdk_window_get_root_origin(GTK3.gtk_widget_get_window(shellHandle), x, y); } } + if (!GTK.GTK4) applyMonitorOrigin (x, y); GtkAllocation allocation = new GtkAllocation (); GTK.gtk_widget_get_allocation (vboxHandle, allocation); int width = allocation.width; diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java index b660734626e..0d025d20a86 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java @@ -1101,4 +1101,43 @@ public void test_degenerateSizeDoesNotExpandShell() { assertTrue(clientArea.height >= 0, "negative client area: " + clientArea); assertTrue(clientArea.height <= 2, "shell expanded to its natural size: " + clientArea); } + +@Test +public void test_toDisplayIsOnTheShellsMonitor() { + // Display coordinates and Monitor geometry have to share one coordinate space, otherwise + // callers that clip a location against a monitor discard it. See issue 3539. + // GTK4 has no root coordinates yet, so toDisplay() is still shell relative there. + assumeTrue(SwtTestUtil.isGTK && !SwtTestUtil.isGTK4()); + shell.setSize(300, 200); + shell.open(); + SwtTestUtil.processEvents(); + Rectangle monitor = shell.getMonitor().getBounds(); + Point origin = shell.toDisplay(0, 0); + assertTrue(monitor.contains(origin), "toDisplay " + origin + " is outside monitor " + monitor); +} + +@Test +public void test_popupLocationRoundTrip() { + // JFace reuses one popup: position, show, hide, reposition. Every pass has to round-trip, + // including the ones where the shell is still mapped. See issue 3539. + assumeTrue(SwtTestUtil.isGTK && !SwtTestUtil.isGTK4()); + shell.setSize(400, 300); + shell.open(); + SwtTestUtil.processEvents(); + Shell popup = new Shell(shell, SWT.TOOL | SWT.ON_TOP); + popup.setSize(120, 80); + try { + for (int pass = 0; pass < 3; pass++) { + Point location = shell.toDisplay(40 + pass * 10, 40 + pass * 10); + popup.setLocation(location); + popup.setVisible(true); + SwtTestUtil.processEvents(); + assertEquals(location, popup.getLocation(), "pass " + pass); + popup.setVisible(false); + SwtTestUtil.processEvents(); + } + } finally { + popup.dispose(); + } +} }