From 39242005510fb7657af61d85135193a1945f006f Mon Sep 17 00:00:00 2001 From: Heiko Klare Date: Mon, 10 Nov 2025 21:53:23 +0100 Subject: [PATCH] [Win32] Make Control bounds point/pixel conversion cope with rounding The invertibility of point/pixel conversions is limited as point values are int-based and with lower resolution than pixel values. In consequence, values need to be rounded when converted between the two, which inevitably leads to rounded values that do not fit for every use case. This adds test cases that demonstrate such use cases, including simple parent/child scenarios, in which the child is supposed to fill the parent, and including layouting scenarios incorporating the client area of a composite, and how the current implementation is not capable of producing proper results for them. This change also adapts the methods for setting bounds/size of controls to deal with the limited invertibility: 1. They transform the passed bounds into a "global" coordinate system (the one of the containing shell) such that all values are rounded in the same way. Otherwise, every control uses a coordinate system relative to its parent, leading to the same global point coordinates being transformed to different pixel coordinates, such that parent and child may not fit to each other even though their point coordinates perfectly match. 2. They shrink the calculated pixel values by at most the maximum error that can be made when transforming from point to pixel values, such that rounding errors due to layouts that calculated control bounds based on a composites client area are evened out. Without that, layouted controls may be up to one point too large to fit into the composite. --- .../swt/widgets/ControlWin32Tests.java | 123 +++++++++++++++++- .../org/eclipse/swt/widgets/Control.java | 95 +++++++++++++- 2 files changed, 210 insertions(+), 8 deletions(-) diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ControlWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ControlWin32Tests.java index 66f0b3c400e..e7a8547ae82 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ControlWin32Tests.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ControlWin32Tests.java @@ -103,11 +103,11 @@ public void testCorrectScaleUpUsingDifferentSetBoundsMethod() { button.setBounds(new Rectangle(0, 47, 200, 47)); assertEquals("Control::setBounds(Rectangle) doesn't scale up correctly", - new Rectangle(0, 82, 350, 83), button.getBoundsInPixels()); + new Rectangle(0, 82, 350, 82), button.getBoundsInPixels()); button.setBounds(0, 47, 200, 47); assertEquals("Control::setBounds(int, int, int, int) doesn't scale up correctly", - new Rectangle(0, 82, 350, 83), button.getBoundsInPixels()); + new Rectangle(0, 82, 350, 82), button.getBoundsInPixels()); } @ParameterizedTest @@ -155,4 +155,123 @@ private FontComparison updateFont(int scalingFactor) { return new FontComparison(heightInPixels, currentHeightInPixels); } + /** + * Scenario: + * + * Depending on how the offset of the parent (x value of bounds) is taken + * into account when rounding during point-to-pixel conversion, the parent + * composite may become one pixel too large or small for the child. + */ + @Test + void testChildFillsCompositeWithOffset() { + Win32DPIUtils.setMonitorSpecificScaling(true); + // pixel values at 125%: (2.5, 2.5, 2.5, 2.5) --> when rounding bottom right + // corner (pixel value (5, 5)) instead of width/height independently, will be + // rounded to (3, 3, 2, 2) --> too small for child + Rectangle parentBounds = new Rectangle(2, 2, 2, 2); + // pixel values at 125%: (0, 0, 2.5, 2.5) --> will be rounded to (0, 0, 3, 3) + Rectangle childBounds = new Rectangle(0, 0, 2, 2); + + Display display = Display.getDefault(); + Shell shell = new Shell(display); + Composite parent = new Composite(shell, SWT.NONE); + DPITestUtil.changeDPIZoom(shell, 125); + parent.setBounds(parentBounds); + Button child = new Button(parent, SWT.PUSH); + child.setBounds(childBounds); + + Rectangle parentBoundsInPixels = parent.getBoundsInPixels(); + Rectangle childBoundsInPixels = child.getBoundsInPixels(); + assertEquals(parentBoundsInPixels.x, 3); + assertEquals(childBoundsInPixels.x, 0); + assertEquals(parentBoundsInPixels.width, childBoundsInPixels.width); + assertEquals(parentBoundsInPixels.height, childBoundsInPixels.height); + assertEquals(childBounds, child.getBounds()); + } + + /** + * Scenario: + * + * Depending on how the offset of the child (x value of bounds) is taken into + * account when rounding during point-to-pixel conversion, the child may become + * one pixel too large to fit into the parent. + */ + @Test + void testChildWithOffsetFillsComposite() { + Win32DPIUtils.setMonitorSpecificScaling(true); + // pixel values at 125%: (0, 0, 5, 5) + Rectangle parentBounds = new Rectangle(0, 0, 4, 4); + // pixel values at 125%: (2.5, 2.5, 2.5, 2.5) --> when rounding width/height + // independently instead of bottom right corner, will be rounded to + // (3, 3, 3, 3) --> too large for parent + Rectangle childBounds = new Rectangle(2, 2, 2, 2); + + Display display = Display.getDefault(); + Shell shell = new Shell(display); + Composite parent = new Composite(shell, SWT.NONE); + DPITestUtil.changeDPIZoom(shell, 125); + parent.setBounds(parentBounds); + Button child = new Button(parent, SWT.PUSH); + child.setBounds(childBounds); + + Rectangle parentBoundsInPixels = parent.getBoundsInPixels(); + Rectangle childBoundsInPixels = child.getBoundsInPixels(); + assertEquals(parentBoundsInPixels.x, 0); + assertEquals(childBoundsInPixels.x, 3); + assertEquals(parentBoundsInPixels.width, childBoundsInPixels.x + childBoundsInPixels.width); + assertEquals(parentBoundsInPixels.height, childBoundsInPixels.y + childBoundsInPixels.height); + assertEquals(parentBounds, parent.getBounds()); + assertEquals(childBounds, child.getBounds()); + } + + /** + * Scenario: Layouting + *

+ * Layouts use client area of composites to calculate the sizes of the contained + * controls. The rounded values of that client area can lead to child bounds be + * calculated larger than the actual available size. + */ + @Test + void testChildFillsScrollableWithBadlyRoundedClientArea() { + Win32DPIUtils.setMonitorSpecificScaling(true); + Display display = Display.getDefault(); + Shell shell = new Shell(display); + Composite parent = new Composite(shell, SWT.H_SCROLL|SWT.V_SCROLL); + DPITestUtil.changeDPIZoom(shell, 125); + // Find parent bounds such that client area is rounded to a value that, + // when converted back to pixels, is one pixel too large + Rectangle parentBounds = new Rectangle(0, 0, 4, 4); + Rectangle clientAreaInPixels; + do { + do { + parentBounds.width += 1; + parentBounds.height += 1; + parent.setBounds(parentBounds); + Rectangle clientArea = parent.getClientArea(); + clientAreaInPixels = Win32DPIUtils + .pointToPixel(new Rectangle(clientArea.x, clientArea.y, clientArea.width, clientArea.height), 125); + } while (clientAreaInPixels.width <= parent.getClientAreaInPixels().width && clientAreaInPixels.width < 50); + parentBounds.x += 1; + parentBounds.y += 1; + if (parentBounds.x >= 50) { + fail("No scrolable size with non-invertible point/pixel conversion for its client area could be created"); + } + } while (clientAreaInPixels.width <= parent.getClientAreaInPixels().width); + Button child = new Button(parent, SWT.PUSH); + Rectangle childBounds = new Rectangle(0, 0, parent.getClientArea().width, parent.getClientArea().height); + child.setBounds(childBounds); + + clientAreaInPixels = parent.getClientAreaInPixels(); + Rectangle childBoundsInPixels = child.getBoundsInPixels(); + assertTrue(clientAreaInPixels.width <= childBoundsInPixels.x + childBoundsInPixels.width); + assertTrue(clientAreaInPixels.height <= childBoundsInPixels.y + childBoundsInPixels.height); + } + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Control.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Control.java index 1b4c98222ea..c521382464a 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Control.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Control.java @@ -3302,7 +3302,83 @@ public void setBounds (Rectangle rect) { checkWidget (); if (rect == null) error (SWT.ERROR_NULL_ARGUMENT); int zoom = computeBoundsZoom(); - setBoundsInPixels(Win32DPIUtils.pointToPixel(rect, zoom)); + Rectangle boundsInPixels = boundsToPixelsViaShellCoordinates(rect, zoom); + fitInParentBounds(boundsInPixels, zoom); + setBoundsInPixels(boundsInPixels); +} + +/** + * Converts bounds to pixels via the shell coordinate system, such that the + * coordinates for every control are rounded in the same. Otherwise, child and + * parent controls may not fit as their coordinates are relative to different + * coordinate systems (the ones with their individual parent as origin), such + * that applied rounding leads to different values for the actually same + * coordinates. One consequence when not doing this is that child controls with + * x and y set to 0 and width and height set to the parent's bounds may be + * larger than the parent. + */ +private Rectangle boundsToPixelsViaShellCoordinates(Rectangle bounds, int zoom) { + Point.OfFloat parentOffsetToShell = calculateParentOffsetToShell(); + Point.OfFloat parentOffsetToShellInPixels = Point.OfFloat + .from(Win32DPIUtils.pointToPixelAsSize(parentOffsetToShell, zoom)); + Rectangle.OfFloat offsetRectangle = new Rectangle.OfFloat(bounds.x + parentOffsetToShell.getX(), + bounds.y + parentOffsetToShell.getY(), bounds.width, bounds.height); + Rectangle.OfFloat offsetRectangleInPixels = Rectangle.OfFloat + .from(Win32DPIUtils.pointToPixel(offsetRectangle, zoom)); + int xInPixels = offsetRectangleInPixels.x - parentOffsetToShellInPixels.x; + int yInPixels = offsetRectangleInPixels.y - parentOffsetToShellInPixels.y; + int widthInPixels = offsetRectangleInPixels.width; + int heightInPixels = offsetRectangleInPixels.height; + return new Rectangle(xInPixels, yInPixels, widthInPixels, heightInPixels); +} + +private Point.OfFloat calculateParentOffsetToShell() { + float parentX = 0; + float parentY = 0; + Control parent = getParent(); + while (parent != null & !(parent instanceof Shell)) { + Rectangle.OfFloat parentLocation = Rectangle.OfFloat.from(parent.getBounds()); + parentX += parentLocation.getX(); + parentY += parentLocation.getY(); + parent = parent.getParent(); + } + return new Point.OfFloat(parentX, parentY); +} + +/** + * Cope with limited invertibility of pixel/point conversions. + *

+ * Example: 125% monitor, layout fills composite with single child + *

+ * Alternatives: + * + * Thus, reduce the control size in case it would not fit anyway + */ +private void fitInParentBounds(Rectangle boundsInPixels, int zoom) { + if (parent == null) { + return; + } + Rectangle parentBounds = parent.getBoundsInPixels(); + if (parentBounds.width < boundsInPixels.x + boundsInPixels.width + && parentBounds.width >= boundsInPixels.x + boundsInPixels.width - Win32DPIUtils.pointToPixel(1.0f, zoom)) { + boundsInPixels.width = parentBounds.width - boundsInPixels.x; + } + if (parentBounds.height < boundsInPixels.y + boundsInPixels.height && parentBounds.height >= boundsInPixels.y + + boundsInPixels.height - Win32DPIUtils.pointToPixel(1.0f, zoom)) { + boundsInPixels.height = parentBounds.height - boundsInPixels.y; + } } void setBoundsInPixels (Rectangle rect) { @@ -3817,9 +3893,7 @@ public void setRegion (Region region) { public void setSize (int width, int height) { checkWidget (); int zoom = computeBoundsZoom(); - width = DPIUtil.pointToPixel(width, zoom); - height = DPIUtil.pointToPixel(height, zoom); - setSizeInPixels(width, height); + setSize(new Point(width, height), zoom); } void setSizeInPixels (int width, int height) { @@ -3853,8 +3927,17 @@ void setSizeInPixels (int width, int height) { public void setSize (Point size) { checkWidget (); if (size == null) error (SWT.ERROR_NULL_ARGUMENT); - size = Win32DPIUtils.pointToPixelAsSize(size, computeBoundsZoom()); - setSizeInPixels(size.x, size.y); + int zoom = computeBoundsZoom(); + setSize(size, zoom); +} + +private void setSize(Point size, int zoom) { + Rectangle bounds = getBounds(); + bounds.width = size.x; + bounds.height = size.y; + Rectangle boundsInPixels = boundsToPixelsViaShellCoordinates(bounds, zoom); + fitInParentBounds(boundsInPixels, zoom); + setSizeInPixels(boundsInPixels.width, boundsInPixels.height); } @Override