Skip to content

Conversation

@hjohn
Copy link
Collaborator

@hjohn hjohn commented Dec 15, 2025

When a Window is created with a certain X/Y coordinate to place it on a specific screen, and is subsequently shown for the first time, one of the first things it does is size the window according to the size of the Scene. It does this based on the render scale of the primary screen as it has not moved the peer yet to the correct screen. After the scene has been sized, it is moved to the correct screen, triggering a change of render scale, but not a resizing of the Window (as this is only done once).

The result of this is that due to slight difference in render scale, the size calculated for the scene may be a few pixels off. As the scene's preferred size is used for this calculation, even a few pixels too small can result in Labels being shown with ellipsis on the intended target screen with a different render scale.

When observing the render scale X or Y property, one can observe a change from 1.0 (the default value) to 2.0 (the primary screen's render scale) to another value (depending on the target screen). However, the Window involved (being positioned by the user using setX()/setY() before it is shown) was never shown on the primary screen, yet the size calculation assumed it was.

To solve this problem, the peer should be moved to the correct screen before asking the Scene for its preferred size to use as the initial Window size. Doing so (by adding an additional applyBounds call) also results in the render scale properties to only change once (or not at all) from their default value to the target screen's value (or not at all if the target screen is 1.0 scale).


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-8373688: Wrong render scale is used if Window is on another screen when Scene is sized (Bug - P4)

Reviewers

Reviewing

Using git

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

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

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 2007

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

Using diff file

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

Using Webrev

Link to Webrev Comment

@hjohn hjohn changed the title Fix ellipsis shown on Labels when primary/target screen differ in scale Wrong render scale is used if Window is on another screen when Scene is sized Dec 15, 2025
@hjohn hjohn changed the title Wrong render scale is used if Window is on another screen when Scene is sized 8373688: Wrong render scale is used if Window is on another screen when Scene is sized Dec 15, 2025
@bridgekeeper
Copy link

bridgekeeper bot commented Dec 15, 2025

👋 Welcome back jhendrikx! 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
Copy link

openjdk bot commented Dec 15, 2025

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk openjdk bot added the rfr Ready for review label Dec 15, 2025
@mlbridge
Copy link

mlbridge bot commented Dec 15, 2025

@andy-goryachev-oracle
Copy link
Contributor

andy-goryachev-oracle commented Dec 15, 2025

This fix introduces another issue in the monkey tester, breaking the layout (see the screenshot below). The failure can be seen when the MT window appears on the primary (scale=2) as well as on the secondary (scale=1) monitor, running macOS 26.1.

The layout gets fixed when the user resizes the window.

Interestingly, moving the window to another screen with a different scale does not fixes the issue.

Screenshot 2025-12-15 at 10 47 46

More observations:

@hjohn
Copy link
Collaborator Author

hjohn commented Dec 16, 2025

@andy-goryachev-oracle Well, this is going a bit beyond what I'm trying to fix. I don't see any of the issues you're showing, not with test programs, nor with MonkeyTester. In fact, I saw several positive things after applying this fix (Windows are no longer sized with unused empty space when immediately shown on a different monitor).

As this again seems to be Mac specific, I have very limited means to "fix" this further, so perhaps you could have a look yourself. I've added some debug output that will be helpful (you can share it here). Look specifically at what setBounds is called with.

MonkeyTester is setting X/Y/width/height of its Window explicitly when it has some settings stored from a previous run. This means it generally follows a slightly different code path when the window is shown, since all the "explicit" flags for x, y, width and height will be true. Still, that doesn't explain how you get some unused empty space.

Here's an example of the output on my system, where MonkeyTester is shown on the 1.0 monitor on the right side of a 1.5 monitor:

Window/invalidationListener: shown changed to: true
Window/invalidationListener: screen=javafx.stage.Screen@21e4df3b bounds:Rectangle2D [minX=0.0, minY=0.0, maxX=2560.0, maxY=1440.0, width=2560.0, height=1440.0] visualBounds:Rectangle2D [minX=0.0, minY=0.0, maxX=2560.0, maxY=1400.0, width=2560.0, height=1400.0] dpi:93.0 outputScale:(1.5,1.5)
Window/peer: setBounds(2698.0, 61.0, true, true, winW:1463.0, 708.0, clientW:-1.0, -1.0, 0.0, 0.0, rsx:0.0, rsy:0.0)
Window/invalidationListener: after applying bounds screen=javafx.stage.Screen@55e0e86c bounds:Rectangle2D [minX=2560.0, minY=-194.0, maxX=4480.0, maxY=1006.0, width=1920.0, height=1200.0] visualBounds:Rectangle2D [minX=2560.0, minY=-194.0, maxX=4480.0, maxY=1006.0, width=1920.0, height=1200.0] dpi:94.0 outputScale:(1.0,1.0)
Window/invalidationListener: output scale used before sizing scene: 1.0
Window/invalidationListener: size from scene: 517.0x2646.0
Window/peer: setBounds(2698.0, 61.0, true, true, winW:1471.0, 727.0, clientW:-1.0, -1.0, 0.0, 0.0, rsx:1.0, rsy:1.0)
Window/invalidationListener: exit invalidation listener

What you see above here is that initially the window screen is still the primary screen (at minXY=0,0); after applyBounds this changes to the correct screen. The scene size code is then called with the correct values (render scale 1.0). The scene size is however a bit weird; it is very wide, probably because there are many controls in your Scene that have a wide preferred width.
It's possible this is somehow the cause of that empty space -- still, on my system, those values are unused as MonkeyTester sets its own width/height, which you can see in the final setBounds call -- effectively, the scene sizing is ignored, and it should re-layout with the final size.

Come to think of it, the scene size call is basically superfluous; if width/height is explicit, we could skip that completely.

Anyway, let me know what you find. I'm not seeing any issues with the above outputs, but perhaps something odd pops up on your system that may point us in the right direction.

@andy-goryachev-oracle
Copy link
Contributor

I am testing on macOS 26.1 with this PR merged with the latest master branch.

Here is what I see when the MT shows up on the secondary (scale=1) monitor:

Window/invalidationListener: shown changed to: true
Window/invalidationListener: screen=javafx.stage.Screen@ce424859 bounds:Rectangle2D [minX=0.0, minY=0.0, maxX=1800.0, maxY=1169.0, width=1800.0, height=1169.0] visualBounds:Rectangle2D [minX=0.0, minY=40.0, maxX=1800.0, maxY=1117.0, width=1800.0, height=1077.0] dpi:151.0 outputScale:(2.0,2.0)
Window/peer: setBounds(459.0, -1437.0, true, true, winW:1165.0, 880.0, clientW:-1.0, -1.0, 0.0, 0.0, rsx:0.0, rsy:0.0)
Window/invalidationListener: after applying bounds screen=javafx.stage.Screen@5fd7eb71 bounds:Rectangle2D [minX=-760.0, minY=-1440.0, maxX=1800.0, maxY=0.0, width=2560.0, height=1440.0] visualBounds:Rectangle2D [minX=-760.0, minY=-1440.0, maxX=1800.0, maxY=0.0, width=2560.0, height=1440.0] dpi:108.0 outputScale:(1.0,1.0)
Window/invalidationListener: output scale used before sizing scene: 1.0
Window/invalidationListener: size from scene: 791.0x726.0
Window/peer: setBounds(459.0, -1437.0, true, true, winW:1165.0, 880.0, clientW:-1.0, -1.0, 0.0, 0.0, rsx:1.0, rsy:1.0)
Window/invalidationListener: exit invalidation listener
Screenshot 2025-12-16 at 10 16 42

@andy-goryachev-oracle
Copy link
Contributor

And here is what's logged if the MT appears on the primary (scale=2) screen:

Window/invalidationListener: shown changed to: true
Window/invalidationListener: screen=javafx.stage.Screen@ce424859 bounds:Rectangle2D [minX=0.0, minY=0.0, maxX=1800.0, maxY=1169.0, width=1800.0, height=1169.0] visualBounds:Rectangle2D [minX=0.0, minY=40.0, maxX=1800.0, maxY=1117.0, width=1800.0, height=1077.0] dpi:151.0 outputScale:(2.0,2.0)
Window/peer: setBounds(410.0, 62.0, true, true, winW:1055.0, 655.0, clientW:-1.0, -1.0, 0.0, 0.0, rsx:0.0, rsy:0.0)
Window/invalidationListener: after applying bounds screen=javafx.stage.Screen@ce424859 bounds:Rectangle2D [minX=0.0, minY=0.0, maxX=1800.0, maxY=1169.0, width=1800.0, height=1169.0] visualBounds:Rectangle2D [minX=0.0, minY=40.0, maxX=1800.0, maxY=1117.0, width=1800.0, height=1077.0] dpi:151.0 outputScale:(2.0,2.0)
Window/invalidationListener: output scale used before sizing scene: 2.0
Window/invalidationListener: size from scene: 773.5x730.5
Window/peer: setBounds(410.0, 62.0, true, true, winW:1055.0, 655.0, clientW:-1.0, -1.0, 0.0, 0.0, rsx:2.0, rsy:2.0)
Window/invalidationListener: exit invalidation listener
Screenshot 2025-12-16 at 10 18 51

If I resize the window, the layout gets fixed, though I see no output logged.

Hope this helps.

@andy-goryachev-oracle
Copy link
Contributor

We also need this checked on Linux[es]
/reviewers 2

@openjdk
Copy link

openjdk bot commented Dec 16, 2025

@andy-goryachev-oracle
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).

@andy-goryachev-oracle
Copy link
Contributor

Note: I am using the latest standalone monkey tester https://github.com/andy-goryachev-oracle/MonkeyTest but a similar issue is present in the one that's currently in the main repo.

@hjohn
Copy link
Collaborator Author

hjohn commented Dec 16, 2025

Judging from how square the MT screen looks (700 x 700 orso), it seems that after the window is shown, it is not resizing it to fit the window. It is actually keeping the sizeToScene information (which it is supposed to ignore as you're setting sizes from preferences stored), or it is not triggering a resize even though the window is a totally different size than the scene. I think I can work with that to have a deeper look.

It's certainly odd, since I'm only moving the applyBounds call to be slightly earlier, so it uses the correct render scale. Later it does it again anyway. In my tests the calculated sizeToScene was usually larger than the Window settings, so perhaps shrinking the window works correctly, but not expanding it when it doesn't fit the scene.

@hjohn
Copy link
Collaborator Author

hjohn commented Dec 21, 2025

Can skip this comment, it's outdated as PopupWindow tests were failing with this change

@andy-goryachev-oracle Thanks for taking the time to test this. I think I may have a fix, included in this PR (I left the print statements in for now).

It was again a Mac specific problem, that I couldn't reproduce on Windows. Luckily I could borrow the mac again (and my stuff was still on it).

The difference between Mac and Windows here was the layout flag status on the Scene root after the Window was shown. On Mac it was DIRTY_BRANCH, while on Windows is was NEEDS_LAYOUT. This means that on Windows, it would fully recalculate the scene's size and adjust it again. On Mac, it would only adjust some child controls, as DIRTY_BRANCH indicates "this node is fine, but some child does need layout".

How this difference arises is probably somewhere in the peer code or native code. On Windows, some control (or perhaps the Scene) probably receives a signal that it should relayout itself when the Window shows, which bubbles up to the root, and the root gets NEEDS_LAYOUT. On Mac this works slightly differently, and this doesn't happen. I don't think we need to look here to resolve the problem, as I think it was still the fault of how sizing when the window is shown is handled.

The solution is luckily much simpler. I was already suspicious of the fact that we ask the Scene to "size itself" in all situations, even if both the width and height of the Window were set explicitly -- this happens with the somewhat poorly named SceneHelper.preferredSize(getScene()) call (it doesn't return the preferred size, it sets it). This hard sets the Scene to its preferred size, and since on Mac the Scene root is DIRTY_BRANCH it would not be recalculated when the Window shows. This could cause both gaps in the UI (on your 1.0 screen), or UI pieces being partially shown (on your 2.0 screen I noticed this).

So, the fix is to only call SceneHelper.preferredSize(getScene()) when one or both dimension are not explicit, since in that case we'll be adjusting the size on the scene for one or both dimensions via the adjustSize(true) call. If both dimensions were explicit, then there is no need to size the scene to its preferred size at all (this is the MonkeyTester case when it has stored dimensions).

Let me know if this resolves the problem for you as well Andy.

@hjohn
Copy link
Collaborator Author

hjohn commented Dec 21, 2025

@andy-goryachev-oracle my first fix broke PopupWindow sizing code; it apparently relies on the scene being sized despite setting a fixed Window width/height. I feel that code is too intertwined to touch, so I decided to go for a different approach.

New approach:

Instead of using applyBounds to have the peer determine the correct screen output scales (but also updates many other values), I now have the Window code determine the correct Screen on its own. I then use the output scales of that Screen to set these values on the peer, but do not update the peer yet.

The scene is then sized (as before) with the correct render scales in order to fix the ellipsis issue.

The applyBounds call is then done in the usual location.

So:

  • I can't applyBounds early because that breaks Mac as the size of the peer is no longer updated after the scene is sized to its preferred size, resulting in the Scene being shown with the wrong size
  • I can't skip the scene sizing when both dimensions on the Window are set explicitly because some PopupWindow tests (context menu tests) then fail, although I didn't see any adverse effects in a real application. I'm not entirely sure if that is a test bug (not realistic enough) or an actual bug.
  • And (the reason for this change) we also can't just use the primary's screen render scale when sizing the scene as otherwise ellipsis appear on labels in certain cases

I think this new solution avoids many pitfalls, and is still a simple fix.

@beldenfox
Copy link
Contributor

On macOS the window's size is tracked in logical coordinates, not pixels. When you move a window to another screen you typically won't get notifyResize on either the Window or View since as far as the OS is concerned the size didn't change. Windows and Linux work in pixels and will almost always send notifyResize when a window changes screens. That might explain the difference in behavior that you're seeing.

Can't test this on Linux right now. Something is going wrong when Linux is configured with multiple screens at different scales. JavaFX recognizes when a window changes screens but doesn't update the window size or scaling factors.

@hjohn
Copy link
Collaborator Author

hjohn commented Jan 6, 2026

@andy-goryachev-oracle Could you have another look? I think this fixes all issues, and we may be able to proceed with #1945 then.

If you agree, I'll remove the debug lines so it can be reviewed.

@andy-goryachev-oracle
Copy link
Contributor

yes, I was planning to take a look at this today

@beldenfox
Copy link
Contributor

FWIW this can't be tested on Linux at the moment. It's possible to set up a system with multiple monitors each with a different scale but due to limitations in the GTK3 API we have to choose between supporting fractional scaling or per-monitor scaling. Currently we favor fractional and so we use the same scaling factor across all monitors.

@andy-goryachev-oracle
Copy link
Contributor

Looks good on macOS and Windows, I see no ill effects opening the monkey tester on external/internal screens with various scale factors.

I've tested this branch merged with the latest master (please sync up the long running branches with the master). Also, please update the (c) to 2026 when you'll be removing the debug printouts.

@andy-goryachev-oracle
Copy link
Contributor

@beldenfox do you see any issues/regressions with/without an external monitor on linux?

@bridgekeeper
Copy link

bridgekeeper bot commented Feb 5, 2026

@hjohn 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 issue a /touch or /keepalive command to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

@hjohn
Copy link
Collaborator Author

hjohn commented Feb 5, 2026

I don't think this will cause any problems on Linux, but does a fix an issue on Mac.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rfr Ready for review

Development

Successfully merging this pull request may close these issues.

3 participants