From b8cded77f13c9c75fa641a0e3f8c14b870a2543e Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Tue, 25 Aug 2026 19:45:09 +0200 Subject: [PATCH 1/3] [GTK] Do not pass a negative size to gtk_widget_size_allocate A Shell with a container border (SWT.TOOL | SWT.ON_TOP, as used by the JFace information controls) that is asked for a height of 0 ends up with a client box of -1: setBounds clamps the height to 1 and resizeBounds then subtracts the 2 pixel border. GTK rejects the negative allocation, so the shell keeps its natural size while its content box is never laid out. That shows up as a correctly sized but completely empty popup, together with Gtk-CRITICAL: gtk_widget_get_preferred_width_for_height: assertion 'height >= 0' failed Gtk-WARNING : gtk_widget_size_allocate(): attempt to allocate widget with width 226 and height -1 Clamp the box size to zero so a degenerate request stays degenerate instead of corrupting the layout. Contributes to https://github.com/eclipse-platform/eclipse.platform.swt/issues/3539 --- .../Eclipse SWT/gtk/org/eclipse/swt/widgets/Shell.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 c55f99be603..7957f7e99d9 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 @@ -2361,8 +2361,10 @@ void resizeBounds (int width, int height, boolean notify) { GDK.gdk_window_resize (enableWindow, width, height); } } - int boxWidth = width - 2*border; - int boxHeight = height - 2*border; + // GTK rejects negative allocations; a shell smaller than its own border must not + // leak a negative size into gtk_widget_size_allocate(). + int boxWidth = Math.max (0, width - 2*border); + int boxHeight = Math.max (0, height - 2*border); if ((style & SWT.RESIZE) == 0) { GTK.gtk_widget_set_size_request (vboxHandle, boxWidth, boxHeight); } From 11e33fc04d4e8aa9463b0f77710d078a506dd662 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Wed, 26 Aug 2026 18:30:26 +0200 Subject: [PATCH 2/3] [GTK] Anchor Wayland display coordinates to the monitor of the window 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() gets the same treatment, since callers compare it against Control.toDisplay(). 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. Fixes https://github.com/eclipse-platform/eclipse.platform.swt/issues/3539 --- .../gtk/org/eclipse/swt/widgets/Control.java | 32 ++++++++++++++++--- .../gtk/org/eclipse/swt/widgets/Display.java | 29 ++++++++++++++++- .../gtk/org/eclipse/swt/widgets/Menu.java | 8 +++-- .../gtk/org/eclipse/swt/widgets/Shell.java | 28 ++++++++++++++-- 4 files changed, 87 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 2bfa4ee4980..0d782a21718 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 @@ -1691,8 +1691,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]; @@ -1756,8 +1757,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; @@ -6919,9 +6921,31 @@ 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; + // An unmapped Shell has no GdkWindow; the nearest ancestor that has one is on the same monitor. + long window = 0; + for (Control control = this; control != null; control = control.parent) { + window = gtk_widget_get_window (control.getShell ().topHandle ()); + if (window != 0) break; + } + return display.monitorOrigin (window); +} + /** * 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 4a1bc89cce3..3204b5affe2 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 @@ -2002,6 +2002,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. @@ -2024,7 +2040,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 @@ -2045,6 +2061,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 19467e4b2d3..56d5636e86b 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 @@ -447,8 +447,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 7957f7e99d9..97c4cc6512f 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 @@ -1348,10 +1348,26 @@ 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 () { + // A child Shell is positioned while still hidden, before its own monitor is known. + if (parent != null) return parent.monitorOrigin (); + return super.monitorOrigin (); +} + +/** 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(); @@ -1601,6 +1617,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. @@ -2417,9 +2434,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. @@ -2431,11 +2452,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; @@ -3613,6 +3634,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; From cae94f1cd326321b8d680f5cc6b7b53088f9a5c9 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Wed, 26 Aug 2026 19:28:45 +0200 Subject: [PATCH 3/3] [GTK] Add regression tests for Wayland popup coordinates and sizing test_degenerateSizeDoesNotExpandShell fails without the size clamp on both X11 and Wayland: the shell falls back to its natural size instead of honouring the requested one. The other two encode invariants the coordinate anchoring restores. They pass on a single monitor whose origin is 0,0, so they only bite on a multi-monitor Wayland session, but test_popupLocationRoundTrip does cover the reuse cycle that JFace performs, which is where a wrong anchor shows up. All three are GTK3 only. GTK4 backs these shells with a GtkPopover that never went through the box maths and enforces a minimum size of its own, and its toDisplay() is still shell relative because root coordinates are not available. --- .../Test_org_eclipse_swt_widgets_Shell.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) 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 d16f1e47ae1..18a114a39c2 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 @@ -1054,4 +1054,63 @@ public void test_setLocationII() { } super.test_setLocationII(); } + +@Test +public void test_degenerateSizeDoesNotExpandShell() { + // A bordered popup asked for a height of 0 used to be allocated -1, which GTK rejects, + // so it fell back to its natural size with its content never laid out. See issue 3539. + // GTK4 backs these shells with a GtkPopover, which never went through that box maths + // and enforces a minimum height of its own. + assumeTrue(SwtTestUtil.isGTK && !SwtTestUtil.isGTK4()); + Shell popup = new Shell(shell, SWT.TOOL | SWT.ON_TOP); + popup.setLayout(new FillLayout()); + Button child = new Button(popup, SWT.PUSH); + child.setText("some content that is clearly taller than two pixels"); + popup.setSize(228, 0); + popup.setVisible(true); + SwtTestUtil.processEvents(); + Rectangle clientArea = popup.getClientArea(); + popup.dispose(); + 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(); + } +} }