Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8295078: TextField blurry when inside an TitledPane -> AnchorPane #910

Closed
wants to merge 6 commits into from

Conversation

Maran23
Copy link
Member

@Maran23 Maran23 commented Oct 11, 2022

The problem here is, that the AnchorPane does not use its snapped insets.
Therefore, the fix is to replace all getInsets().getXXX calls with their corresponding snappedXXXInset() methods.

Note: The reason the AnchorPane inside a TitledPane is blurry in the first place is because a TitledPane applies padding to its content.
Line 2995 in modena.css:

.titled-pane > .content > AnchorPane {
    -fx-padding: 0.8em;
}

which translates to 9.6px.

EDIT: This is btw a good example of the JUnit 5 feature @ParameterizedTest with @ValueSource


Progress

  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed (2 reviews required, with at least 1 Reviewer, 1 Author)

Issue

  • JDK-8295078: TextField blurry when inside an TitledPane -> AnchorPane

Reviewers

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/910/head:pull/910
$ git checkout pull/910

Update a local copy of the PR:
$ git checkout pull/910
$ git pull https://git.openjdk.org/jfx.git pull/910/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 910

View PR using the GUI difftool:
$ git pr show -t 910

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/910.diff

Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Oct 11, 2022

👋 Welcome back mhanl! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk openjdk bot added the rfr Ready for review label Oct 11, 2022
@mlbridge
Copy link

mlbridge bot commented Oct 11, 2022

Webrevs

@kevinrushforth
Copy link
Member

/reviewers 2

@openjdk
Copy link

openjdk bot commented Oct 11, 2022

@kevinrushforth
The total number of required reviews for this PR (including the jcheck configuration and the last /reviewers command) is now set to 2 (with at least 1 Reviewer, 1 Author).

@kevinrushforth
Copy link
Member

NOTE: this is blocked by the fix for JDK-8296283, which @arapte plans to do next week.

Copy link
Member

@arapte arapte left a comment

Choose a reason for hiding this comment

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

Looks good to me.

@@ -355,15 +350,15 @@ private double computeChildHeight(Node child, Double topAnchor, Double bottomAnc
}

if (leftAnchor != null) {
x = insets.getLeft() + leftAnchor;
x = snappedLeftInset() + leftAnchor;
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: should we use unsnapped coordinates in the computations, and only snap at the final stage?

For example, if leftAnchor is 0.5 the child will be positioned between the pixels, so to speak.

Or is this indeed an intended functionality?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not 100% sure about this. It sounds somewhat logical. If we set the left anchor to be 5. Do we expect it to be 10 with a render scale of 2 -> 200% resolution?

Copy link
Contributor

@andy-goryachev-oracle andy-goryachev-oracle Nov 8, 2022

Choose a reason for hiding this comment

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

Not sure about render scale - it should be done elsewhere.

As for snapping, I think this code might be incorrect - it should honor Region.snapToPixel setting. All the intermediary computations need to use unsnapped (original) values, then, at the last moment, call proper Region.snap*() which either rounds or not, depending on the snapToPixel.

The only exception for this rule (of using snapped values in the intermediary computations) is to ensure some kind of specific alignment, which I believe is not the case here.

edit: my reasoning here is incorrect. any computation should use snapped values.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure about this. Other places do the same, and there are lot of places actually which do something like snappedLeftInsets + snappedRightInsets or final double w = snapSizeX(getWidth()) - x - snappedRightInset(); or similar.
I think this was for a reason.
And I have only oriented myself to the existing code in this case. If you are right, then this was done wrong everywhere.

Btw. I had a look and other container do snap their constraints, e.g. H/VBox its spacing, GridPane his H/VGap. So pretty sure we also should snap the anchor values.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the idea is this (and it only applies when snapToPixels=true). We only need to use snapped coordinates/sizes when placing components. In order to avoid accumulating errors, which can be seen during gradual resizing for example, we should avoid snapping of intermediary results, UNLESS there is a possibility of introducing visual artifacts.

What I mean by this is imagine a container that aligns its children in a table-like layout (similar to https://github.com/andy-goryachev/FxDock/blob/master/doc/CPane.md). Depending on constraints, there might be a situation where rounding might shift certain children in relation to the vertical guide lines - we don't want that.

So in this case we need to snap the guide lines as an intermediate step (i.e. compute the first guide line position, then compute the next column, snap, next column, etc.)

Whereas in a simple case (one child node), we can simply use snapped*Inset() - we don't have intermediate computations.

In the case of AnchorPane, I think, we should first sum insets and anchor, then snap - because developers might want to use or not to use snapToPixels, and might set a fractional anchor.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I now understand the snapping better, especially after discussion in #1111

it looks like we should be operating in terms of final pixels, that is, in snapped coordinates. this might mean what we should not, generally, mix snapped and unsnapped values:

result = snap(a) + snap(b); // ok

result = snap(snap(a) + b) // also ok

result = snap(snap(a) + b + c) // not ok, should be written as
result = snap(snap(a) + snap(b) + snap(c)) or
result = snap(a) + snap(b) + snap(c)

What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree but that is also what I do here, isn't it?

Copy link
Contributor

Choose a reason for hiding this comment

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

here: yes!

Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks good to me, the snapping as explained by Andy is how it should be. A source of error can be still be that floating point calculations can introduce small deviations so sometimes it is needed to resnap after doing calculations that seem harmless (ie. snappedSize * 10 or snappedWidth - snappedLeftInset may need resnapping).

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. That is something we should think about - this may or may not be a problem with some of the snapping code.

@@ -355,15 +350,15 @@ private double computeChildHeight(Node child, Double topAnchor, Double bottomAnc
}

if (leftAnchor != null) {
x = insets.getLeft() + leftAnchor;
x = snappedLeftInset() + leftAnchor;
Copy link
Contributor

@andy-goryachev-oracle andy-goryachev-oracle Nov 8, 2022

Choose a reason for hiding this comment

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

Not sure about render scale - it should be done elsewhere.

As for snapping, I think this code might be incorrect - it should honor Region.snapToPixel setting. All the intermediary computations need to use unsnapped (original) values, then, at the last moment, call proper Region.snap*() which either rounds or not, depending on the snapToPixel.

The only exception for this rule (of using snapped values in the intermediary computations) is to ensure some kind of specific alignment, which I believe is not the case here.

edit: my reasoning here is incorrect. any computation should use snapped values.

Copy link
Member

@kevinrushforth kevinrushforth left a comment

Choose a reason for hiding this comment

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

Not a complete review, but I did leave some inline comments. I note that there is precedent for using the snapped insets in intermediate computations, so that part of the change seems OK to me. If that is going to be the pattern, you need to make sure that you are snapping the values you add to the snapped insets.

I'd like @arapte to take a closer look as well.

@@ -355,15 +350,15 @@ private double computeChildHeight(Node child, Double topAnchor, Double bottomAnc
}

if (leftAnchor != null) {
x = insets.getLeft() + leftAnchor;
x = snappedLeftInset() + leftAnchor;
Copy link
Member

Choose a reason for hiding this comment

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

I looked at the other layout panes and they already do something similar to what this PR does, using the snapped insets for intermediate compuation. I also note that some of the computation is calling superclass methods that do snapping already, and expect it to be added to snapped insets (if there are insets), for example, computeWidth(). In this particular case -- snappedLeftInset() + leftAnchor -- I don't think leftAnchor is snapped, but it probably should be.

@@ -355,15 +350,15 @@ private double computeChildHeight(Node child, Double topAnchor, Double bottomAnc
}

if (leftAnchor != null) {
x = insets.getLeft() + leftAnchor;
x = snappedLeftInset() + leftAnchor;
Copy link
Contributor

Choose a reason for hiding this comment

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

I still think this code is incorrect.

To illustrate, let's set insets to 0.8 and anchor to 0.5
Here is what I think it should do:
snap=off
result=1.3

snap=on
result=1.0 (and not 1.5, as I think the proposed fix would produce)

@mstr2
Copy link
Collaborator

mstr2 commented Nov 10, 2022

@andy-goryachev-oracle
Snapping doesn't necessarily imply integer coordinates or sizes. The reason is that we're snapping to pixels, not integers. For example, with 200% scale, every other pixel will have a .5 coordinate. It's also important to not confuse snapping with rounding to integers because integer coordinates might not even fall on pixel boundaries for some scaling factors.

@andy-goryachev-oracle
Copy link
Contributor

@mstr2 :
You are right, I wasn't aware of scale. Snapping code looks like Math.round(value * scale) / scale;, so I take the 'whole integer' words back. Sorry for the confusion.

Having said that, my earlier comment is still valid - if snapToPixel==true, the final location and sizes must go through snap*() methods. And the earlier example is also valid, assuming scale=1.0.

…textfield-blurry

� Conflicts:
�	modules/javafx.graphics/src/test/java/test/javafx/scene/layout/AnchorPaneTest.java
@Maran23
Copy link
Member Author

Maran23 commented Dec 8, 2022

I thought about this one more time.
Example:

  • render scale of 1.25 (125%).
  • Control with a width of 200px
  • left inset = 10, right inset = 15
  • left anchor = 10, right anchor = 15

Now the UI would look like this currently:
[Left inset = 10.4][Left anchor = 10.4][Control = 148.8][Right anchor = 15.2][Right inset = 15.2]
Control width is calculated like this: 200 - 10.4 - 15.2 - 10.4 - 15.2 = 148.8

If we would not snap intermediate values, the left side (inset + anchor) would be 20 instead of 20.8.
The right side would still be 30.4.

Thinking about this again, isn't the current behaviour what we would expect from the layout with a scale of 1.25?
Snapping 'intermediate' values seems logical to me (current behaviour), but maybe I'm missing something.
But we should avoid snapping things more than one time.

@bridgekeeper
Copy link

bridgekeeper bot commented Mar 31, 2023

@Maran23 This pull request has been inactive for more than 4 weeks and will be automatically closed if another 4 weeks passes without any activity. To avoid this, simply add a new comment to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

@bridgekeeper
Copy link

bridgekeeper bot commented Apr 29, 2023

@Maran23 This pull request has been inactive for more than 8 weeks and will now be automatically closed. If you would like to continue working on this pull request in the future, feel free to reopen it! This can be done using the /open pull request command.

@bridgekeeper bridgekeeper bot closed this Apr 29, 2023
@Maran23
Copy link
Member Author

Maran23 commented Apr 29, 2023

/open

@openjdk openjdk bot reopened this Apr 29, 2023
@openjdk
Copy link

openjdk bot commented Apr 29, 2023

@Maran23 This pull request is now open

@@ -355,15 +350,15 @@ private double computeChildHeight(Node child, Double topAnchor, Double bottomAnc
}

if (leftAnchor != null) {
x = insets.getLeft() + leftAnchor;
x = snappedLeftInset() + leftAnchor;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I now understand the snapping better, especially after discussion in #1111

it looks like we should be operating in terms of final pixels, that is, in snapped coordinates. this might mean what we should not, generally, mix snapped and unsnapped values:

result = snap(a) + snap(b); // ok

result = snap(snap(a) + b) // also ok

result = snap(snap(a) + b + c) // not ok, should be written as
result = snap(snap(a) + snap(b) + snap(c)) or
result = snap(a) + snap(b) + snap(c)

What do you think?

}
return computeChildPrefAreaWidth(child, -1, Insets.EMPTY, height, true);
}

private double computeChildHeight(Node child, Double topAnchor, Double bottomAnchor, double areaHeight, double width) {
if (topAnchor != null && bottomAnchor != null && child.isResizable()) {
final Insets insets = getInsets();
return areaHeight - insets.getTop() - insets.getBottom() - topAnchor - bottomAnchor;
return areaHeight - snappedTopInset() - snappedBottomInset() - topAnchor - bottomAnchor;
Copy link
Contributor

Choose a reason for hiding this comment

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

same comment about areaHeight - should it be snapSize'd?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, given that we (should) snap everything including the calculation of the width, I expected that the areaWidth and height, which are basically getWidth and getHeight are already snapped.
That was at least my thinking back then, not sure now. I need to check everything again since this was a while ago. :)

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder.

This might be a contrived example: what would happen if the parent containing the TitledPane is unsnapped, but TitledPane is?
In this case getWidth() may not be snapped since the titled pane has been laid out by an unsnapped parent.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think once there is an unsnapped parent, all children should probably also be unsnapped (ie. the property should work like the visible property; if I turn it off for one parent, all children become invisible/unsnapped).

There would otherwise be little point in setting a parent to unsnapped if one didn't also turn off snapping on all the children.

Note that we tested what happens when you put snapped content in an unsnapped container; currently this results in all children to be misaligned (not pixel aligned). I think that's the only real solution, as trying to resnap your positions when your parent container is misaligning you would otherwise need to introduce some kind of flexible border that "absorbs" some of the misalignment so the rest of the content can be snapped again...

Basically, there are three strategies for children when dealing with an unsnapped parent:

  • Just do snapping as normal (JavaFX currently) and when the parent translation is added, all final positions will be shifted by an unsnapped amount resulting in all calculations (even though they used snapping logic) to be in the "wrong" positions
  • Ignore snapping when a parent is unsnapped and use standard calculations
  • Somehow try to resnap content taking the parent misalignment into account; this is going to cause visual artifacts as the parent is moved, and requires some kind of flexible border

Copy link
Collaborator

Choose a reason for hiding this comment

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

In this case getWidth() may not be snapped since the titled pane has been laid out by an unsnapped parent.

And when that happens, you can't do anything about it either. If you adjust the width, you risk not covering the entire area that the parent gave you (or more if you round up), and if you use it in any calculations you need to resnap any values resulting from it, and you would still end up with some dead space that your snapped calculations can't cover...

The only real solution seems to be to treat snapping like visibility IMHO.

Copy link
Member

Choose a reason for hiding this comment

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

I think once there is an unsnapped parent, all children should probably also be unsnapped (ie. the property should work like the visible property; if I turn it off for one parent, all children become invisible/unsnapped).

One complication with this is that the snapToPixel property is on Region, so Group (or any other Parent that isn't a Region) doesn't have that property. What you describe might be OK as long as you ignore any parent that isn't a Region when asking the question "are all my ancestors have snapToPixel".

Basically, there are three strategies for children when dealing with an unsnapped parent:

  • Just do snapping as normal (JavaFX currently) and when the parent translation is added, all final positions will be shifted by an unsnapped amount resulting in all calculations (even though they used snapping logic) to be in the "wrong" positions

Right. If a non-snap-to-pixel ancestor lays out it children, then all bets are off. Absent a compelling reason to change it, this seems like a reasonable default (perhaps it should be documented better).

  • Ignore snapping when a parent is unsnapped and use standard calculations

A change to do this might be OK, with the caveat I mentioned above about non-Region parents, but this would need careful analysis. As part of this, I would want to understand the motivation for doing this (i.e., what is the benefit?).

  • Somehow try to resnap content taking the parent misalignment into account; this is going to cause visual artifacts as the parent is moved, and requires some kind of flexible border

Yeah, this doesn't seems like a good idea.

If you want to explore this further, you could file a new RFE to consider making a change.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's probably not worth doing. When the application developer sets snapping off when it's on by default, they do it for a reason, so the current behavior is the right one. May be they want to smooth some transition during animation (a-la scrolling ticker tape), who knows.

FX gives this choice at per-Region basis for this kind of flexibility, otherwise there would be one global setting, right?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it's probably not worth doing. When the application developer sets snapping off when it's on by default, they do it for a reason, so the current behavior is the right one. May be they want to smooth some transition during animation (a-la scrolling ticker tape), who knows.

FX gives this choice at per-Region basis for this kind of flexibility, otherwise there would be one global setting, right?

For sure it shouldn't be a global setting, and the per Region choice seems absolutely the right one. What I was talking about is more how sensible it would be for children of an unsnapped Region to still use snapping logic, and if it shouldn't work similar to how visibility works (ie. if a parent is invisible, all its children are invisible, regardless of those children's individual visible status).

If the scrolling ticker tape you mentioned contains any other children, the user probably wants snapping off for those as well as the ticker tape would otherwise not be as smooth as it could be; if the parent region is misaligned, all the children (if snapped) would still advance by an exact multiple of pixels, but they would always fall in the same spot between pixels. Also, if the speed is not a nice pixel multiple (ie. 4.5 pixels per frame) then the children would sometimes move 4 pixels, sometimes 5.

It's not truly a big issue, just something that may be unexpected.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@kevinrushforth it probably isn't worth pursuing a change in the child/parent snapping logic right now, at least not without some compelling case. First I think snapping is rarely disabled (I never have, despite working on graphics heavy JavaFX applications). Second, one can already get the (possibly more) desired situation by taking care all children are unsnapped as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree with the consensus here. And same as @hjohn, I actually also never disabled snapping. There was never usecase where I needed to do so (And I worked on a lot of UI related things, including creating custom components of all kind).

Right. If a non-snap-to-pixel ancestor lays out it children, then all bets are off. Absent a compelling reason to change it, this seems like a reasonable default (perhaps it should be documented better).

I agree here.The defaultis fine, and it's hard to say what would be a better default 'setting' here (if there really is a better way). Does a developer really expect that all children of a unsnapped node are unsnapped as well? Or is there a usecase where you don't want this? It's hard to answer, so as written above, the default seems fine and logicial.

@@ -355,15 +350,15 @@ private double computeChildHeight(Node child, Double topAnchor, Double bottomAnc
}

if (leftAnchor != null) {
x = insets.getLeft() + leftAnchor;
x = snappedLeftInset() + leftAnchor;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks good to me, the snapping as explained by Andy is how it should be. A source of error can be still be that floating point calculations can introduce small deviations so sometimes it is needed to resnap after doing calculations that seem harmless (ie. snappedSize * 10 or snappedWidth - snappedLeftInset may need resnapping).

@mstr2
Copy link
Collaborator

mstr2 commented May 12, 2023

I think we should document the insights gained in this discussion (and others) somewhere in JavaFX, since snapping is such a difficult thing to get right, and the source of so many visual artifacts.

Copy link
Contributor

@andy-goryachev-oracle andy-goryachev-oracle left a comment

Choose a reason for hiding this comment

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

Thank you @hjohn for a detailed writeup!

I fully agree with the idea of explaining snapping in some form of "application note", let research the subject of where and how we can do that.

I suppose the next step would be to review other layouts to see if they fail with snapping, especially with fractional scale (hm, HBox).

@openjdk
Copy link

openjdk bot commented May 12, 2023

@Maran23 This change now passes all automated pre-integration checks.

ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details.

After integration, the commit message for the final commit will be:

8295078: TextField blurry when inside an TitledPane -> AnchorPane

Reviewed-by: angorya, kcr, jhendrikx

You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed.

At the time when this comment was updated there had been 6 new commits pushed to the master branch:

  • bff41c2: 8308114: Bump minimum version of macOS for x64 to 11.0 (matching aarch64)
  • c1a1179: 8245919: Region#padding property rendering error
  • e7974bc: 8308028: Replace more uses of System.getProperty("os.name") with PlatformUtil calls
  • 8aff525: 8307960: Create Table Column PopupMenu lazily
  • 7095364: 8307807: Replace use of System.getProperty("os.name") with PlatformUtil calls
  • 604fc26: 8307363: TextFlow.underlineShape()

Please see this link for an up-to-date comparison between the source branch of this pull request and the master branch.
As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details.

➡️ To integrate this PR with the above commit message to the master branch, type /integrate in a new comment.

@openjdk openjdk bot added the ready Ready to be integrated label May 12, 2023
@Maran23
Copy link
Member Author

Maran23 commented May 17, 2023

/integrate

@openjdk
Copy link

openjdk bot commented May 17, 2023

Going to push as commit 6aeaff3.
Since your change was applied there have been 6 commits pushed to the master branch:

  • bff41c2: 8308114: Bump minimum version of macOS for x64 to 11.0 (matching aarch64)
  • c1a1179: 8245919: Region#padding property rendering error
  • e7974bc: 8308028: Replace more uses of System.getProperty("os.name") with PlatformUtil calls
  • 8aff525: 8307960: Create Table Column PopupMenu lazily
  • 7095364: 8307807: Replace use of System.getProperty("os.name") with PlatformUtil calls
  • 604fc26: 8307363: TextFlow.underlineShape()

Your commit was automatically rebased without conflicts.

@openjdk openjdk bot added the integrated Pull request has been integrated label May 17, 2023
@openjdk openjdk bot closed this May 17, 2023
@openjdk openjdk bot removed ready Ready to be integrated rfr Ready for review labels May 17, 2023
@openjdk
Copy link

openjdk bot commented May 17, 2023

@Maran23 Pushed as commit 6aeaff3.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

@Maran23 Maran23 deleted the 8295078-textfield-blurry branch July 29, 2024 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integrated Pull request has been integrated
Development

Successfully merging this pull request may close these issues.

6 participants