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..6ec297c6fe5 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
@@ -155,4 +155,205 @@ private FontComparison updateFont(int scalingFactor) {
return new FontComparison(heightInPixels, currentHeightInPixels);
}
+ /**
+ * Scenario:
+ *
+ * - parent has bounds with an offset (x != 0) to its parent
+ *
- child fills the composite, such that both their widths are equal
+ *
+ * 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:
+ *
+ * - parent has bounds with an offset (x = 0) to its parent
+ *
- child has an offset (x != 0) to parent and exactly fills the rest of the
+ * composite, such that child.x+child.width is equal to parent.x
+ *
+ * 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);
+ assertChildFitsIntoParent(parent, child);
+ assertEquals(parentBounds, parent.getBounds());
+ assertEquals(childBounds, child.getBounds());
+
+ child.setSize(childBounds.width, childBounds.height);
+ assertChildFitsIntoParent(parent, child);
+ assertEquals(parentBounds, parent.getBounds());
+ assertEquals(childBounds, child.getBounds());
+ }
+
+ private void assertChildFitsIntoParent(Control parent, Control child) {
+ Rectangle parentBoundsInPixels = parent.getBoundsInPixels();
+ Rectangle childBoundsInPixels = child.getBoundsInPixels();
+ Rectangle childBounds = child.getBounds();
+ assertEquals(parentBoundsInPixels.width, childBoundsInPixels.x + childBoundsInPixels.width);
+ assertEquals(parentBoundsInPixels.height, childBoundsInPixels.y + childBoundsInPixels.height);
+ assertEquals(parent.getBounds().width, childBounds.x + childBounds.width);
+ assertEquals(parent.getBounds().height, childBounds.y + childBounds.height);
+ }
+
+ /**
+ * 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. This serves as unit test
+ * for that behavior in addition to the integration test
+ * {@link #testChildFillsScrollableWithBadlyRoundedClientArea()}
+ */
+ @Test
+ void testFitChildIntoParent() {
+ 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, 3.75, 3.75) --> rounded to (3, 3, 3, 3)
+ Rectangle childBounds = new Rectangle(2, 2, 3, 3);
+
+ 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);
+ assertChildFitsIntoParent(parent, child);
+
+ child.setSize(childBounds.width, childBounds.height);
+ assertChildFitsIntoParent(parent, child);
+ }
+
+ // Ensure that the fitting logic does only apply at off-by-one calculation results
+ // and not for fitting actually larger childs into parts
+ @Test
+ void testFitChildIntoParent_limitedSizeDifference() {
+ 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, 5, 5) --> rounded to (3, 3, 5, 5)
+ Rectangle childBounds = new Rectangle(2, 2, 4, 4);
+
+ 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);
+ assertChildNotFitsIntoParent(parent, child);
+
+ child.setSize(childBounds.width, childBounds.height);
+ assertChildNotFitsIntoParent(parent, child);
+ }
+
+ private void assertChildNotFitsIntoParent(Control parent, Control child) {
+ Rectangle parentBoundsInPixels = parent.getBoundsInPixels();
+ Rectangle childBoundsInPixels = child.getBoundsInPixels();
+ Rectangle childBounds = child.getBounds();
+ assertNotEquals(parentBoundsInPixels.width, childBoundsInPixels.x + childBoundsInPixels.width);
+ assertNotEquals(parentBoundsInPixels.height, childBoundsInPixels.y + childBoundsInPixels.height);
+ assertNotEquals(parent.getBounds().width, childBounds.x + childBounds.width);
+ assertNotEquals(parent.getBounds().height, childBounds.y + childBounds.height);
+ }
+
+ /**
+ * 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);
+
+ child.setSize(childBounds.width, childBounds.height);
+
+ clientAreaInPixels = parent.getClientAreaInPixels();
+ 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 285bf317733..be30cc3da5c 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,54 @@ public void setBounds (Rectangle rect) {
checkWidget ();
if (rect == null) error (SWT.ERROR_NULL_ARGUMENT);
int zoom = computeBoundsZoom();
- setBoundsInPixels(Win32DPIUtils.pointToPixel(rect, zoom));
+ Rectangle boundsInPixels = Win32DPIUtils.pointToPixel(rect, zoom);
+ fitInParentBounds(boundsInPixels, zoom);
+ setBoundsInPixels(boundsInPixels);
+}
+
+
+/**
+ * Cope with limited invertibility of pixel/point conversions.
+ *
+ * Example: 125% monitor, layout fills composite with single child
+ *
+ * - Composite with client area of 527 pixels
+ *
- getClientArea() returns 527 / 1,25 = 421,6 points
+ *
- So layout sets rounded 422 points to child
+ *
- This conforms to 422 * 1,25 = 527,5 pixels, which is rounded up to 528,
+ * i.e., one more than parent size
+ *
+ * Alternatives:
+ *
+ * - rounding down the client area instead could lead to areas not redrawn, as
+ * rounded down 421 points result in 526 pixels, one less than the actual size
+ * of the composite
+ *
- rounding down the passed bounds leads to controls becoming unnecessarily
+ * smaller than their calculated size
+ *
+ * Thus, reduce the control size in case it would not fit anyway
+ */
+private void fitInParentBounds(Rectangle boundsInPixels, int zoom) {
+ if (parent == null) {
+ return;
+ }
+ Rectangle parentBoundsInPixels = parent.getBoundsInPixels();
+ // Check if child does not fit into parent, but that it would fit when taking an
+ // off-by-one for the width/height in points into account, as such an off-by-one
+ // can happen when doing bounds calculations in points due to rounding (e.g., in
+ // layouts)
+ boolean childNotFittingHorizontally = parentBoundsInPixels.width < boundsInPixels.x + boundsInPixels.width;
+ boolean childFittingHorizontallyWithOffByOne = parentBoundsInPixels.width >= boundsInPixels.x + boundsInPixels.width
+ - Win32DPIUtils.pointToPixel(1.0f, zoom);
+ if (childNotFittingHorizontally && childFittingHorizontallyWithOffByOne) {
+ boundsInPixels.width = parentBoundsInPixels.width - boundsInPixels.x;
+ }
+ boolean childNotFittingVertically = parentBoundsInPixels.height < boundsInPixels.y + boundsInPixels.height;
+ boolean childFittingVerticallyWithOffByOne = parentBoundsInPixels.height >= boundsInPixels.y + boundsInPixels.height
+ - Win32DPIUtils.pointToPixel(1.0f, zoom);
+ if (childNotFittingVertically && childFittingVerticallyWithOffByOne) {
+ boundsInPixels.height = parentBoundsInPixels.height - boundsInPixels.y;
+ }
}
void setBoundsInPixels (Rectangle rect) {
@@ -3817,9 +3864,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 +3898,18 @@ 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) {
+ // Use the exact (non-rounded) bounds to apply proper rounding when converting the size from point to pixel
+ Rectangle bounds = Win32DPIUtils.pixelToPoint(Rectangle.OfFloat.from(getBoundsInPixels()), zoom);
+ bounds.width = size.x;
+ bounds.height = size.y;
+ Rectangle boundsInPixels = Win32DPIUtils.pointToPixel(bounds, zoom);
+ fitInParentBounds(boundsInPixels, zoom);
+ setSizeInPixels(boundsInPixels.width, boundsInPixels.height);
}
@Override