Skip to content

Conversation

amartya4256
Copy link
Contributor

@amartya4256 amartya4256 commented Aug 5, 2025

This PR contributes to precise scaling of computation of size inside gridLayout by using float precision. This fixes the issue reported in #2166

Explanation

The issue happens because of the loss of precision while calculating the size of the grid in the GridLayout. The code at first calculates the size of widget in GridData#computeSize. Since the width and height in GridData were stored as integer, the returned Point.OfFloat from Control#computeSize is not fully utilized and the residualX and residualY (the floating pointer values) are lost and only integer is used.

Later this less precision integer value is used to set the bounds of the grid, which leads to wrapping the widget with the wrong size. In case of #2166, that leads to wrapping of the text in the widget while the height of the grid is set thinking that the text doesn't need wrapping.

A simple example for loss of precision:
Let's say a pixel value 223 is set at 150%.
when it is obtained in points by GridData#computeSize, it is 223/1.5 = 148.6667 i.e. 149
When scaled up on GridLayout#layout on child.setBounds, it becomes 149 * 1.5 = 223.5 i.e. 224

Hence, this difference of 1 pixel can have the wrong wrapping effect.
While using float in the layout, the inversibility is maintained. Hence, 223/1.5 = 148.66667 and 148.6666667 * 1.5 = 223

How to test

package org.eclipse.swt.snippets;

import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.layout.*;
import org.eclipse.swt.widgets.*;

public class SnippetLabelCutOff {
	public static void main(String[] args) {
		Display display = new Display();


		/* Provide different resolutions for icons to get
		 * high quality rendering wherever the OS needs
		 * large icons. For example, the ALT+TAB window
		 * on certain systems uses a larger icon.
		 */
		Shell shell = new Shell(display);
		PartView partView = new SnippetLabelCutOff.PartView(display);
		partView.postConstruct(shell);
		shell.open();



		while (!shell.isDisposed()) {
			if (!display.readAndDispatch())
				display.sleep();
		}
		partView.preDestroy();
		shell.dispose();
		display.dispose();
	}

	public static class PartView
	{
	   public Display aDisplay;

	   private Composite aMidComposite;

	   private Label aRepairLabel;

	   public Font aNormalFont;

	   public PartView(Display display) {
		aDisplay = display;
	}

	   public void postConstruct(Composite pParent)
	   {
	      FormLayout vLayout = new FormLayout();
	      vLayout.marginHeight = 5;
	      vLayout.marginWidth = 6;
	      pParent.setLayout(vLayout);

	      createResources();

	      createMid(pParent);

	      pParent.pack();
	   }

	   private void createResources()
	   {
	      aNormalFont = new Font(
	            aDisplay,
	            new FontData("Arial", 11, SWT.NORMAL));

	   }

	   private void createMid(Composite pParent)
	   {
	      // MID
	      aMidComposite = new Composite(pParent, SWT.NONE);
	      aMidComposite.setBackground(aDisplay.getSystemColor(SWT.COLOR_WHITE));
	      FormData aFormDataMid = new FormData();
	      aFormDataMid.top = new FormAttachment(0, 0);
	      aFormDataMid.bottom = new FormAttachment(100, 0);
	      aFormDataMid.left = new FormAttachment(0, 0);
	      aFormDataMid.right = new FormAttachment(100, 0);
	      aMidComposite.setLayoutData(aFormDataMid);

	      GridLayout vLayout = new GridLayout();
	      aMidComposite.setLayout(vLayout);

	      aRepairLabel = new Label(aMidComposite, SWT.WRAP);
	      aRepairLabel.setFont(aNormalFont);
	      GridData vRepairLabelGridData = new GridData();
	      vRepairLabelGridData.horizontalAlignment = GridData.BEGINNING;
	      vRepairLabelGridData.verticalAlignment = GridData.CENTER;
	      vRepairLabelGridData.grabExcessHorizontalSpace = true;
	      vRepairLabelGridData.verticalIndent = Math.round(32);
	      aRepairLabel.setLayoutData(vRepairLabelGridData);

	      aRepairLabel
	            .setText(
	                  "Rhe erv fhe eovianae tb orao estnffd rniTaa eultoolte i sssube wreco.");
	   }

	   public void preDestroy()
	   {
	      aNormalFont.dispose();
	   }
	}
}

Before:
image

After:
image

depends on #2486

Copy link
Contributor

github-actions bot commented Aug 5, 2025

Test Results

  320 files   -   226    320 suites   - 226   17m 54s ⏱️ - 11m 39s
4 144 tests  -   287  4 118 ✅  -   296  26 💤 + 9  0 ❌ ±0 
8 359 runs   - 8 405  8 307 ✅  - 8 330  52 💤  - 75  0 ❌ ±0 

Results for commit d7a4725. ± Comparison against base commit fdbb769.

This pull request removes 287 tests.
AllGTKTests Test_GtkConverter ‑ test_HeuristicASCII_dollarSign
AllGTKTests Test_GtkConverter ‑ test_HeuristicASCII_emptyString
AllGTKTests Test_GtkConverter ‑ test_HeuristicASCII_letterA
AllGTKTests Test_GtkConverter ‑ test_HeuristicASCII_letters
AllGTKTests Test_GtkConverter ‑ test_HeuristicUTF16LE_null
AllGTKTests Test_GtkConverter ‑ test_HeuristicUTF16_AsciiLetters
AllGTKTests Test_GtkConverter ‑ test_HeuristicUTF16_Asciiletter
AllGTKTests Test_GtkConverter ‑ test_HeuristicUTF16_LotsOfLetters
AllGTKTests Test_GtkConverter ‑ test_HeuristicUTF16_letter
AllGTKTests Test_GtkConverter ‑ test_HeuristicUTF16_letters
…
This pull request skips 17 tests.
org.eclipse.swt.tests.junit.Test_org_eclipse_swt_browser_Browser ‑ test_Constructor_multipleInstantiationsInDifferentThreads[browser flags: 0]
org.eclipse.swt.tests.junit.Test_org_eclipse_swt_browser_Browser ‑ test_setUrl_local[browser flags: 0]
org.eclipse.swt.tests.junit.Test_org_eclipse_swt_browser_Browser ‑ test_setUrl_remote[browser flags: 0]
org.eclipse.swt.tests.junit.Test_org_eclipse_swt_custom_StyledText ‑ test_GlyphMetricsOnTab_Bug549110
org.eclipse.swt.tests.junit.Test_org_eclipse_swt_custom_StyledText ‑ test_bug551335_lostStyles
org.eclipse.swt.tests.junit.Test_org_eclipse_swt_custom_StyledText ‑ test_clipboardCarryover
org.eclipse.swt.tests.junit.Test_org_eclipse_swt_custom_StyledText ‑ test_lineStyleListener_invalidStyles_render
org.eclipse.swt.tests.junit.Test_org_eclipse_swt_custom_StyledText ‑ test_lineStyleListener_stylesAndRanges_render
org.eclipse.swt.tests.junit.Test_org_eclipse_swt_custom_StyledText ‑ test_lineStyleListener_styles_render
org.eclipse.swt.tests.junit.Test_org_eclipse_swt_custom_StyledText ‑ test_setStyleRanges_render
…

♻️ This comment has been updated with latest results.

@amartya4256 amartya4256 force-pushed the amartya4256/fix_label_cutoff branch 2 times, most recently from 4e2fcda to fc7dedf Compare August 6, 2025 08:43
@amartya4256 amartya4256 changed the title Amartya4256/fix label cutoff Fix label cutoff Aug 6, 2025
@amartya4256 amartya4256 marked this pull request as ready for review August 6, 2025 11:26
@amartya4256 amartya4256 linked an issue Aug 6, 2025 that may be closed by this pull request
Copy link
Contributor

@HeikoKlare HeikoKlare left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One general question on this: wouldn't it possible or even reasonable to stick to width/height values inside GridData but just make them floats and only use Point.OfFloat for the returns of API methods?
Two reasons for this:

  1. SWT is generally lacking some Size object and, unfortunately, at many places Point is used to represent a size (even though that semantically does not make that much sense). Thus, we usually try to stick to width/height when considering sizes.
  2. It would be less error prone as now if you accidentally use, e.g., size.x instead of size.getX(), you may calculate with a wrong value.

@amartya4256
Copy link
Contributor Author

One general question on this: wouldn't it possible or even reasonable to stick to width/height values inside GridData but just make them floats and only use Point.OfFloat for the returns of API methods? Two reasons for this:

  1. SWT is generally lacking some Size object and, unfortunately, at many places Point is used to represent a size (even though that semantically does not make that much sense). Thus, we usually try to stick to width/height when considering sizes.
  2. It would be less error prone as now if you accidentally use, e.g., size.x instead of size.getX(), you may calculate with a wrong value.

I agree, I'll make the changes.

@selundqma
Copy link

Hi! Is this fix included in the M3 build released last Friday?

@merks
Copy link
Contributor

merks commented Aug 25, 2025

Given the PR isn't merged, no.

@HeikoKlare
Copy link
Contributor

The necessary change was too risky to do that late (M3) in the development cycle, so it will not go in the upcoming 2025-09 relesae. We plan to process it as early as possible for 2025-12 M1.

@amartya4256 amartya4256 force-pushed the amartya4256/fix_label_cutoff branch from fc7dedf to b0d972e Compare September 1, 2025 15:08
@amartya4256
Copy link
Contributor Author

@HeikoKlare converted the fields back to float instead of Point.OfFloat and refactored as needed. Please have a look.

Copy link
Contributor

@HeikoKlare HeikoKlare left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am not mistaken, this change breaks the calculations inside GridLayout, as previous integer divisions and remainder calculations are simply changed to float arithmetics even though the considerations of int division remainders seems to be wrong there.

Comment on lines 336 to 337
float equalWidth = (w + spanWidth) / hSpan;
float remainder = (w + spanWidth) % hSpan;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this calculation break given that this is no integer division anymore (applies to subsequent places as well)? The equalWidth wil now contain an exact division value but remainder still contains the remainder as if doing an int division.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at the logic and I realized, it was only done to regularize the size of grid cells. But now they we have size in float, we do not need to calculate remainder and we can get rid of it. I tested the code and it seems fine to me. I'll update the code here so that you can review that.

Comment on lines +162 to +167
public static Point.OfFloat from(Point point) {
if (point instanceof Point.OfFloat pointOfFloat) {
return new Point.OfFloat(pointOfFloat.getX(), pointOfFloat.getY());
}
return new Point.OfFloat(point.x, point.y);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a duplication of the method in FloatAwareGeometryFactory, isn't it? Shouldn't that be streamlined in an OS-independent way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With vi-eclipse/Eclipse-Platform#320 we wanted to provide such utility methods directly inside rectangle and point since, they are platform independent. FloatAwareGeometryFactory is a private class inside Win32DPIUtils. Do you think it makes sense to keep using that or maybe we should move towards Point.from and Rectangle.from.

For now I can use FloatAwareGeometryFactory and create a PR following the refactoring as suggested. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean that we should not introduce duplicated code unnecessarily. If we introduce this method as proposed, we can remove the according method from the FloatAwareGeometryFactory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do that separately. I would also need to adapt other places. For now I'll remove this from here and use the Factory method. And I'll refactor everything in the next PR. Sounds good?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that work? The factory is win32-specific while this is OS-independent code.

Copy link
Contributor

@laeubi laeubi Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get what you mean.

I mean why is it important where GridLayout is located for the part about FloatAwareGeometryFactory and Win32DPIUtils? A Layout Manger shold never need to know anything about DPI scaling and alike...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please read my comment above. Everything should be in there: it does not make sense (and technically you cannot even) reference OS-specific code from an OS-agnostic class (like the layout-related classes).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HeikoKlare I read the comments but they do not explain why GridLayout now needs to use Point.OfFloat and be rewritten in using internal API now, so for me it means EVERY LayoutManger now needs to be potentially be rewritten as well (what could be code outside of SWT as well).

As LayoutMangers should already work on the "points" and not the "pixels" so something seems fundamentally wrong here!

And while I can understand that there might be some slight rounding errors, the example above shows a noticeable cut-of that can not be explained by a sub-pixel rounding error to me!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HeikoKlare I read the comments but they do not explain why GridLayout now needs to use Point.OfFloat and be rewritten in using internal API now, so for me it means EVERY LayoutManger now needs to be potentially be rewritten as well (what could be code outside of SWT as well).

Yes, that might be the case.

As LayoutMangers should already work on the "points" and not the "pixels" so something seems fundamentally wrong here!

As we know, points are imprecise because of their limitation to integers and according rounding errors.

And while I can understand that there might be some slight rounding errors, the example above shows a noticeable cut-of that can not be explained by a sub-pixel rounding error to me!

It is such an off-by-one rounding error.

Copy link
Contributor

@laeubi laeubi Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is such an off-by-one rounding error.

It does not really look like being of by a few pixels. For me it looks like more the reported size of the label is smaller than it actually should, so the Label class needs to be fixed (or the Point/Rectangle) to not round down but round up instead so in such case we get a slightly larger size (and maybe show a tiny little extra whitespace) than one that is too small. This would then fix the issue for many other cases as well instead of trying to "fix" all callers.

@@ -179,7 +179,7 @@ static int checkStyle (int style) {
if (hHint != SWT.DEFAULT) height = hHint;
width += border * 2;
height += border * 2;
return new Point (width, height);
return new Point.OfFloat (width, height);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary? The calling method seems to the wrap the point into an OfFloat (via Win32DPIUtil#pixelToPoint()) anyway, doesn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't if the passed object is not OfFloat. Since we want to have precision scaling here, we need to return it as OfFloat so that the scaling method uses the OfFloat scaling. We have such behavior in Win32DPIUtil#pixelToPoint() because we did not want to enable OfFloat scaling SWT wide, because we had problems in some widgets if you remember. Hence, we decided to enable is specifically for those where it is needed. Which means those methods need to return an OfFloat variant to enable themselves for precision scaling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would enable it for SWT wide once we have figured out the issues with problematic implementations. We have separate issue for that. One of them being vi-eclipse/Eclipse-Platform#303

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't get it. Control#computeSize() calls Win32DPIUtils.pixelToPoint():

public Point computeSize (int wHint, int hHint, boolean changed){
checkWidget ();
int zoom = getZoom();
wHint = (wHint != SWT.DEFAULT ? Win32DPIUtils.pointToPixel(wHint, zoom) : wHint);
hHint = (hHint != SWT.DEFAULT ? Win32DPIUtils.pointToPixel(hHint, zoom) : hHint);
return Win32DPIUtils.pixelToPoint(computeSizeInPixels(wHint, hHint, changed), zoom);
}

And that method returns a Point.OfFloat if zoom is not 100:
public static Point pixelToPoint(Point point, int zoom) {
if (zoom == 100 || point == null) return point;
Point.OfFloat fPoint = FloatAwareGeometryFactory.createFrom(point);
float scaleFactor = DPIUtil.getScalingFactor(zoom);
float scaledX = fPoint.getX() / scaleFactor;
float scaledY = fPoint.getY() / scaleFactor;
return new Point.OfFloat(scaledX, scaledY);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, you are right. The behaviour I explained earlier is only valid for Rectangle, not for Point. My bad. Yes we can just return Point.

@amartya4256 amartya4256 force-pushed the amartya4256/fix_label_cutoff branch 3 times, most recently from e536441 to 2c7ad44 Compare September 4, 2025 10:03
This commit adds Rectangle.OfFloat.from and Point.OfFloat.from methods
and removes the Win32DPIUtils.FloatAwareGeometryFactory class to make
these methods OS-independent.
@amartya4256 amartya4256 force-pushed the amartya4256/fix_label_cutoff branch from 2c7ad44 to 81790ee Compare September 4, 2025 10:23
Copy link
Contributor

@laeubi laeubi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layouts have always operated on whole "points" and requiring one specific layout manger for correct function seems totally wrong to me.

So I miss the following:

  1. A general description why the issue happens at all and fractions of a point make a difference now
  2. What are the plans to potentially test (and fix) other layout mangers as well
  3. How we expect custom LayoutMangers out in the wild to adapt here if the float based API is still not public.
  4. I miss test-cases that testing the new GridLayout behaviour

From the referenced issue it more seems that a relay-outing is missing here and wrapping should instead happen.

@HeikoKlare
Copy link
Contributor

  1. A general description why the issue happens at all and fractions of a point make a difference now

I agree that the description needs to be extended to make it easier to understand for everyone (and document for ourselves) what's going on. @amartya4256 please do so.

  1. What are the plans to potentially test (and fix) other layout mangers as well

We plan to do that. Sorry that we did not create a public issue for it so far. We have it in our backlog:

  1. How we expect custom LayoutMangers out in the wild to adapt here if the float based API is still not public.

Let's do one step after another. We will check the other layouts with the above mentioned ticket and see what problems we can expect in general. We have not experienced any issues with any layout but the GridLayout so far. And then it may be up to providing guidelines for others to adapt and potentially making the float-based API public.

  1. I miss test-cases that testing the new GridLayout behaviour

Yes, adding test cases for problematic cases definitely makes sense. @amartya4256 please do so.

From the referenced issue it more seems that a relay-outing is missing here and wrapping should instead happen.

That's not the case. If it was that easy, we would already have solved it like that.

@laeubi
Copy link
Contributor

laeubi commented Sep 4, 2025

We have not experienced any issues with any layout but the GridLayout so far

GridLayout gives bad result on Label changes anyways but that's a known issue. In any case a LayoutManger must be sure it can trust the reported size, so as mentioned above I more see this as a bug of the Label (and probably others) to report a wrong size, so I would first look at that issue.

In general a call to the size should report the size the component would need for the given constrains. This might not always be sufficient to show the component if constraints are to small, but given we pass in -1, -1 here it should never be too small!

@laeubi
Copy link
Contributor

laeubi commented Sep 4, 2025

Taking a quick look at the current implementation.

I think using Math.round() is wrong here for width/height because it will give a possible too small size for components being reported. Instead for width/height one would need to use Math.ceil (I think we can ignore negative numbers here) to never report a smaller size. or one would even need to introduce some rounding mode so the caller can decide an what it want to lean to.

For Point.OfFloat that is often used to measure size, I also thing it would be more safe to use Math.ceil for positive values and Math.floor for negative values instead of Math.round(). In any case I think there should be a small testcase that document (and tests!) the different rounding of "subpoint" values.

@amartya4256
Copy link
Contributor Author

I have updated the description. When it comes to tests, testing this whole issue in GridLayout as a whole is difficult as nothing is exposed as an API and the problem happens when the pixels values are wrong and not in point values. It all depends on if we have Point object or Point.OfFloat object. And testing the inversibility of pixel to point to pixel is already there in org.eclipse.swt.tests.win32.Win32DPIUtilTests.scaleDownscaleUpPointInvertible() and org.eclipse.swt.tests.win32.Win32DPIUtilTests.scaleDownscaleUpRectangleInvertible().

And on top, I can use the snippet on top as an example that shows how the label cuts off on master but this PR fixes it. Is that enough? @HeikoKlare

@laeubi
Copy link
Contributor

laeubi commented Sep 5, 2025

@amartya4256 Your example is not really intuitive to me.

You argue that the value without using floats is actually larger than it should (what is somehow expected) but why should setting it to a larger width should trigger wrapping here what should happen when the label is actually smaller so I suspect it is the other way round.

This is even reproducible under Linux GTK by modify the example with

aRepairLabel = new Label(aMidComposite, SWT.WRAP) {
	    	  @Override
	    	public void setBounds(int x, int y, int width, int height) {
	    		super.setBounds(x, y, width - 1, height);
	    	}

	    	  @Override
	    	protected void checkSubclass() {
	    	}
	      };

then the label wraps because I set a bound 1 pixel to small, but if I change it to +1 pixel (or even +10 pixels) it does not wrap.

This commit contributes to enable GridLayout:layout to utilize
float values in GridData to calculate precise size on scaling using
Point.OfFloat APIs.

contributes to eclipse-platform#2166
@amartya4256 amartya4256 force-pushed the amartya4256/fix_label_cutoff branch from 81790ee to d7a4725 Compare September 8, 2025 13:18
@amartya4256
Copy link
Contributor Author

@amartya4256 Your example is not really intuitive to me.

@laeubi I had a look at the snippet itself and found the following difference in the behavior after comparing this branch with master:
On Master:

  • The label computes its size in pixel in GridData to be (659, 25) in Point.OfFloat
  • The pixels to point conversion is (439.33, 16.67) in Point.OfFloat but since GridData uses Point APIs, it is considered as (439, 17).
  • Later, child.setBounds is called in GridLayout.layout which sets the size Point(439, 17) in points.
  • The scaled value is (658, 25).
  • Here, the label shrinks by a pixel of width on scaling leading to wrapping of the text but the height remains the same.
  • (659, 25) -> (439, 17) -> (658, 25)

On this branch:

  • The label computes its size in pixel in GridData to be (659, 25) in Point.OfFloat
  • The pixels to point conversion is (439.33, 16.67) in Point.OfFloat.
  • Later child.setBounds is called in GridLayout.layout which sets the size Point.OfFloat(439.33, 16.67) in points.
  • The scaled value is (659, 25).
  • The label is set with the same size it had computed hence, the scaling is more precise and no faulty wrapping occurs.
  • (659, 25) -> (439.33, 16.67) -> (659, 25)

@laeubi
Copy link
Contributor

laeubi commented Sep 8, 2025

@amartya4256 that makes much more sense here, and that's why I would suggest to round up any values that represent a width/height. In general I think this can only solved consistently with something like this:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Labels are cut off by autoscaling
7 participants