Skip to content

Commit cbdcdcb

Browse files
committed
[Win32] Fix control bounds exceeding parent client area size
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. 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.
1 parent db8608f commit cbdcdcb

File tree

2 files changed

+262
-6
lines changed

2 files changed

+262
-6
lines changed

bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ControlWin32Tests.java

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,205 @@ private FontComparison updateFont(int scalingFactor) {
155155
return new FontComparison(heightInPixels, currentHeightInPixels);
156156
}
157157

158+
/**
159+
* Scenario:
160+
* <ul>
161+
* <li>parent has bounds with an offset (x != 0) to its parent
162+
* <li>child fills the composite, such that both their widths are equal
163+
* </ul>
164+
* Depending on how the offset of the parent (x value of bounds) is taken
165+
* into account when rounding during point-to-pixel conversion, the parent
166+
* composite may become one pixel too large or small for the child.
167+
*/
168+
@Test
169+
void testChildFillsCompositeWithOffset() {
170+
Win32DPIUtils.setMonitorSpecificScaling(true);
171+
// pixel values at 125%: (2.5, 2.5, 2.5, 2.5) --> when rounding bottom right
172+
// corner (pixel value (5, 5)) instead of width/height independently, will be
173+
// rounded to (3, 3, 2, 2) --> too small for child
174+
Rectangle parentBounds = new Rectangle(2, 2, 2, 2);
175+
// pixel values at 125%: (0, 0, 2.5, 2.5) --> will be rounded to (0, 0, 3, 3)
176+
Rectangle childBounds = new Rectangle(0, 0, 2, 2);
177+
178+
Display display = Display.getDefault();
179+
Shell shell = new Shell(display);
180+
Composite parent = new Composite(shell, SWT.NONE);
181+
DPITestUtil.changeDPIZoom(shell, 125);
182+
parent.setBounds(parentBounds);
183+
Button child = new Button(parent, SWT.PUSH);
184+
child.setBounds(childBounds);
185+
186+
Rectangle parentBoundsInPixels = parent.getBoundsInPixels();
187+
Rectangle childBoundsInPixels = child.getBoundsInPixels();
188+
assertEquals(parentBoundsInPixels.x, 3);
189+
assertEquals(childBoundsInPixels.x, 0);
190+
assertEquals(parentBoundsInPixels.width, childBoundsInPixels.width);
191+
assertEquals(parentBoundsInPixels.height, childBoundsInPixels.height);
192+
assertEquals(childBounds, child.getBounds());
193+
}
194+
195+
/**
196+
* Scenario:
197+
* <ul>
198+
* <li>parent has bounds with an offset (x = 0) to its parent
199+
* <li>child has an offset (x != 0) to parent and exactly fills the rest of the
200+
* composite, such that child.x+child.width is equal to parent.x
201+
* </ul>
202+
* Depending on how the offset of the child (x value of bounds) is taken into
203+
* account when rounding during point-to-pixel conversion, the child may become
204+
* one pixel too large to fit into the parent.
205+
*/
206+
@Test
207+
void testChildWithOffsetFillsComposite() {
208+
Win32DPIUtils.setMonitorSpecificScaling(true);
209+
// pixel values at 125%: (0, 0, 5, 5)
210+
Rectangle parentBounds = new Rectangle(0, 0, 4, 4);
211+
// pixel values at 125%: (2.5, 2.5, 2.5, 2.5) --> when rounding width/height
212+
// independently instead of bottom right corner, will be rounded to
213+
// (3, 3, 3, 3) --> too large for parent
214+
Rectangle childBounds = new Rectangle(2, 2, 2, 2);
215+
216+
Display display = Display.getDefault();
217+
Shell shell = new Shell(display);
218+
Composite parent = new Composite(shell, SWT.NONE);
219+
DPITestUtil.changeDPIZoom(shell, 125);
220+
parent.setBounds(parentBounds);
221+
Button child = new Button(parent, SWT.PUSH);
222+
223+
child.setBounds(childBounds);
224+
assertChildFitsIntoParent(parent, child);
225+
assertEquals(parentBounds, parent.getBounds());
226+
assertEquals(childBounds, child.getBounds());
227+
228+
child.setSize(childBounds.width, childBounds.height);
229+
assertChildFitsIntoParent(parent, child);
230+
assertEquals(parentBounds, parent.getBounds());
231+
assertEquals(childBounds, child.getBounds());
232+
}
233+
234+
private void assertChildFitsIntoParent(Control parent, Control child) {
235+
Rectangle parentBoundsInPixels = parent.getBoundsInPixels();
236+
Rectangle childBoundsInPixels = child.getBoundsInPixels();
237+
Rectangle childBounds = child.getBounds();
238+
assertEquals(parentBoundsInPixels.width, childBoundsInPixels.x + childBoundsInPixels.width);
239+
assertEquals(parentBoundsInPixels.height, childBoundsInPixels.y + childBoundsInPixels.height);
240+
assertEquals(parent.getBounds().width, childBounds.x + childBounds.width);
241+
assertEquals(parent.getBounds().height, childBounds.y + childBounds.height);
242+
}
243+
244+
/**
245+
* Scenario: Layouting
246+
* <p>
247+
* Layouts use client area of composites to calculate the sizes of the contained
248+
* controls. The rounded values of that client area can lead to child bounds be
249+
* calculated larger than the actual available size. This serves as unit test
250+
* for that behavior in addition to the integration test
251+
* {@link #testChildFillsScrollableWithBadlyRoundedClientArea()}
252+
*/
253+
@Test
254+
void testFitChildIntoParent() {
255+
Win32DPIUtils.setMonitorSpecificScaling(true);
256+
// pixel values at 125%: (0, 0, 5, 5)
257+
Rectangle parentBounds = new Rectangle(0, 0, 4, 4);
258+
// pixel values at 125%: (2.5, 2.5, 3.75, 3.75) --> rounded to (3, 3, 3, 3)
259+
Rectangle childBounds = new Rectangle(2, 2, 3, 3);
260+
261+
Display display = Display.getDefault();
262+
Shell shell = new Shell(display);
263+
Composite parent = new Composite(shell, SWT.NONE);
264+
DPITestUtil.changeDPIZoom(shell, 125);
265+
parent.setBounds(parentBounds);
266+
Button child = new Button(parent, SWT.PUSH);
267+
268+
child.setBounds(childBounds);
269+
assertChildFitsIntoParent(parent, child);
270+
271+
child.setSize(childBounds.width, childBounds.height);
272+
assertChildFitsIntoParent(parent, child);
273+
}
274+
275+
// Ensure that the fitting logic does only apply at off-by-one calculation results
276+
// and not for fitting actually larger childs into parts
277+
@Test
278+
void testFitChildIntoParent_limitedSizeDifference() {
279+
Win32DPIUtils.setMonitorSpecificScaling(true);
280+
// pixel values at 125%: (0, 0, 5, 5)
281+
Rectangle parentBounds = new Rectangle(0, 0, 4, 4);
282+
// pixel values at 125%: (2.5, 2.5, 5, 5) --> rounded to (3, 3, 5, 5)
283+
Rectangle childBounds = new Rectangle(2, 2, 4, 4);
284+
285+
Display display = Display.getDefault();
286+
Shell shell = new Shell(display);
287+
Composite parent = new Composite(shell, SWT.NONE);
288+
DPITestUtil.changeDPIZoom(shell, 125);
289+
parent.setBounds(parentBounds);
290+
Button child = new Button(parent, SWT.PUSH);
291+
292+
child.setBounds(childBounds);
293+
assertChildNotFitsIntoParent(parent, child);
294+
295+
child.setSize(childBounds.width, childBounds.height);
296+
assertChildNotFitsIntoParent(parent, child);
297+
}
298+
299+
private void assertChildNotFitsIntoParent(Control parent, Control child) {
300+
Rectangle parentBoundsInPixels = parent.getBoundsInPixels();
301+
Rectangle childBoundsInPixels = child.getBoundsInPixels();
302+
Rectangle childBounds = child.getBounds();
303+
assertNotEquals(parentBoundsInPixels.width, childBoundsInPixels.x + childBoundsInPixels.width);
304+
assertNotEquals(parentBoundsInPixels.height, childBoundsInPixels.y + childBoundsInPixels.height);
305+
assertNotEquals(parent.getBounds().width, childBounds.x + childBounds.width);
306+
assertNotEquals(parent.getBounds().height, childBounds.y + childBounds.height);
307+
}
308+
309+
/**
310+
* Scenario: Layouting
311+
* <p>
312+
* Layouts use client area of composites to calculate the sizes of the contained
313+
* controls. The rounded values of that client area can lead to child bounds be
314+
* calculated larger than the actual available size.
315+
*/
316+
@Test
317+
void testChildFillsScrollableWithBadlyRoundedClientArea() {
318+
Win32DPIUtils.setMonitorSpecificScaling(true);
319+
Display display = Display.getDefault();
320+
Shell shell = new Shell(display);
321+
Composite parent = new Composite(shell, SWT.H_SCROLL|SWT.V_SCROLL);
322+
DPITestUtil.changeDPIZoom(shell, 125);
323+
// Find parent bounds such that client area is rounded to a value that,
324+
// when converted back to pixels, is one pixel too large
325+
Rectangle parentBounds = new Rectangle(0, 0, 4, 4);
326+
Rectangle clientAreaInPixels;
327+
do {
328+
do {
329+
parentBounds.width += 1;
330+
parentBounds.height += 1;
331+
parent.setBounds(parentBounds);
332+
Rectangle clientArea = parent.getClientArea();
333+
clientAreaInPixels = Win32DPIUtils
334+
.pointToPixel(new Rectangle(clientArea.x, clientArea.y, clientArea.width, clientArea.height), 125);
335+
} while (clientAreaInPixels.width <= parent.getClientAreaInPixels().width && clientAreaInPixels.width < 50);
336+
parentBounds.x += 1;
337+
parentBounds.y += 1;
338+
if (parentBounds.x >= 50) {
339+
fail("No scrolable size with non-invertible point/pixel conversion for its client area could be created");
340+
}
341+
} while (clientAreaInPixels.width <= parent.getClientAreaInPixels().width);
342+
Button child = new Button(parent, SWT.PUSH);
343+
Rectangle childBounds = new Rectangle(0, 0, parent.getClientArea().width, parent.getClientArea().height);
344+
child.setBounds(childBounds);
345+
346+
clientAreaInPixels = parent.getClientAreaInPixels();
347+
Rectangle childBoundsInPixels = child.getBoundsInPixels();
348+
assertTrue(clientAreaInPixels.width <= childBoundsInPixels.x + childBoundsInPixels.width);
349+
assertTrue(clientAreaInPixels.height <= childBoundsInPixels.y + childBoundsInPixels.height);
350+
351+
child.setSize(childBounds.width, childBounds.height);
352+
353+
clientAreaInPixels = parent.getClientAreaInPixels();
354+
childBoundsInPixels = child.getBoundsInPixels();
355+
assertTrue(clientAreaInPixels.width <= childBoundsInPixels.x + childBoundsInPixels.width);
356+
assertTrue(clientAreaInPixels.height <= childBoundsInPixels.y + childBoundsInPixels.height);
357+
}
358+
158359
}

bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Control.java

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3302,7 +3302,54 @@ public void setBounds (Rectangle rect) {
33023302
checkWidget ();
33033303
if (rect == null) error (SWT.ERROR_NULL_ARGUMENT);
33043304
int zoom = computeBoundsZoom();
3305-
setBoundsInPixels(Win32DPIUtils.pointToPixel(rect, zoom));
3305+
Rectangle boundsInPixels = Win32DPIUtils.pointToPixel(rect, zoom);
3306+
fitInParentBounds(boundsInPixels, zoom);
3307+
setBoundsInPixels(boundsInPixels);
3308+
}
3309+
3310+
3311+
/**
3312+
* Cope with limited invertibility of pixel/point conversions.
3313+
* <p>
3314+
* Example: 125% monitor, layout fills composite with single child
3315+
* <ul>
3316+
* <li>Composite with client area of 527 pixels
3317+
* <li>getClientArea() returns 527 / 1,25 = 421,6 points
3318+
* <li>So layout sets rounded 422 points to child
3319+
* <li>This conforms to 422 * 1,25 = 527,5 pixels, which is rounded up to 528,
3320+
* i.e., one more than parent size
3321+
* </ul>
3322+
* Alternatives:
3323+
* <ul>
3324+
* <li>rounding down the client area instead could lead to areas not redrawn, as
3325+
* rounded down 421 points result in 526 pixels, one less than the actual size
3326+
* of the composite
3327+
* <li>rounding down the passed bounds leads to controls becoming unnecessarily
3328+
* smaller than their calculated size
3329+
* </ul>
3330+
* Thus, reduce the control size in case it would not fit anyway
3331+
*/
3332+
private void fitInParentBounds(Rectangle boundsInPixels, int zoom) {
3333+
if (parent == null) {
3334+
return;
3335+
}
3336+
Rectangle parentBoundsInPixels = parent.getBoundsInPixels();
3337+
// Check if child does not fit into parent, but that it would fit when taking an
3338+
// off-by-one for the width/height in points into account, as such an off-by-one
3339+
// can happen when doing bounds calculations in points due to rounding (e.g., in
3340+
// layouts)
3341+
boolean childNotFittingHorizontally = parentBoundsInPixels.width < boundsInPixels.x + boundsInPixels.width;
3342+
boolean childFittingHorizontallyWithOffByOne = parentBoundsInPixels.width >= boundsInPixels.x + boundsInPixels.width
3343+
- Win32DPIUtils.pointToPixel(1.0f, zoom);
3344+
if (childNotFittingHorizontally && childFittingHorizontallyWithOffByOne) {
3345+
boundsInPixels.width = parentBoundsInPixels.width - boundsInPixels.x;
3346+
}
3347+
boolean childNotFittingVertically = parentBoundsInPixels.height < boundsInPixels.y + boundsInPixels.height;
3348+
boolean childFittingVerticallyWithOffByOne = parentBoundsInPixels.height >= boundsInPixels.y + boundsInPixels.height
3349+
- Win32DPIUtils.pointToPixel(1.0f, zoom);
3350+
if (childNotFittingVertically && childFittingVerticallyWithOffByOne) {
3351+
boundsInPixels.height = parentBoundsInPixels.height - boundsInPixels.y;
3352+
}
33063353
}
33073354

33083355
void setBoundsInPixels (Rectangle rect) {
@@ -3817,9 +3864,7 @@ public void setRegion (Region region) {
38173864
public void setSize (int width, int height) {
38183865
checkWidget ();
38193866
int zoom = computeBoundsZoom();
3820-
width = DPIUtil.pointToPixel(width, zoom);
3821-
height = DPIUtil.pointToPixel(height, zoom);
3822-
setSizeInPixels(width, height);
3867+
setSize(new Point(width, height), zoom);
38233868
}
38243869

38253870
void setSizeInPixels (int width, int height) {
@@ -3853,8 +3898,18 @@ void setSizeInPixels (int width, int height) {
38533898
public void setSize (Point size) {
38543899
checkWidget ();
38553900
if (size == null) error (SWT.ERROR_NULL_ARGUMENT);
3856-
size = Win32DPIUtils.pointToPixelAsSize(size, computeBoundsZoom());
3857-
setSizeInPixels(size.x, size.y);
3901+
int zoom = computeBoundsZoom();
3902+
setSize(size, zoom);
3903+
}
3904+
3905+
private void setSize(Point size, int zoom) {
3906+
// Use the exact (non-rounded) bounds to apply proper rounding when converting the size from point to pixel
3907+
Rectangle bounds = Win32DPIUtils.pixelToPoint(Rectangle.OfFloat.from(getBoundsInPixels()), zoom);
3908+
bounds.width = size.x;
3909+
bounds.height = size.y;
3910+
Rectangle boundsInPixels = Win32DPIUtils.pointToPixel(bounds, zoom);
3911+
fitInParentBounds(boundsInPixels, zoom);
3912+
setSizeInPixels(boundsInPixels.width, boundsInPixels.height);
38583913
}
38593914

38603915
@Override

0 commit comments

Comments
 (0)