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: + * + * 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); + 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 + *

+ * 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 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