Hi,

I've been doing some investigation into a layout bug in JavaFX on Windows with 
non-integer scaling values. I think it's related to JDK-8199592, and I've put a 
small example that will reproduce these layout bugs at the end of this email. 
The most obvious layout error is truncation of labels in many controls. I've 
used CheckBoxes in this example, but the truncation errors are apparent in any 
labeled control. I suspect the issue affects other controls too (and probably 
the entire layout), but in more subtle ways. With scaling values of 1.25 or 
1.5, the label truncation only happens in the dialog box. At 1.75 the issue is 
apparent in both windows.

I _think_ the issue is a combination of caching and invalid calculations. I 
stepped through the `layoutLabelInArea(x, y, w, h, alignment)` method in 
LabeledSkinBase and found that 3 layout passes take place.

In the first and second passes, all the layout calculations appear to be done 
without taking scaling into account.
The 3rd pass appears to be when a scene is involved, and the snap* methods 
start having an impact. It looks like some values are scaled appropriately, but 
others are still using the pre-scene cached values. In particular, the call to 
`leftLabelPadding()` returns 5 for the first 2 passes, but 5.6 in the 3rd (at 
1.25 scaling). The value of `w` doesn't change though, and that .6 increase in 
padding causes the label to be truncated. When you interact with the CheckBox 
to trigger a 4th layout, the value of `w` is increased by .6, and the layout 
appears to work as expected.

I've hacked some of the caching out of `Parent` by adding a call to 
clearSizeCache() to the start of the `prefWidth(height)` and 
`prefHeight(width)` methods, which forces JavaFX to re-compute everything on 
every call. This resolves the issues for simple layouts at 1.25 and 1.5 scaling 
(but not 1.75).

This doesn't resolve issues in more complicated layouts, however, as many 
subclasses of `Parent` have caching of their own. For example, controls in 
GridPanes will still exhibit this behaviour. I've hacked some caching out of 
that by removing the early return in the `computePrefHeights(widths)` and 
`computePrefWidths(heights)` methods, which resolves it for the GridPane case. 
I imagine there are many other instances where these cached values aren't 
correctly invalidated.

I think there's also a fundamental issue with the layout calculations with 
non-integer scaling, as the layout is _always_ wrong at 1.75. I suspect that 
the layout calculations are always wrong, but the error at lower scaling values 
isn't enough to cause visible issues after pixel snapping.

I'm not really sure where to go from here, as I'm not at all familiar with how 
and when JavaFX invalidates its layout calculations. If anyone can point me at 
some other threads to pull, I'd be grateful.

Thanks,

Sam


Code to reproduce:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class LayoutBug
{
    public static void main(String[] args)
    {
        System.setProperty("glass.win.uiScale", "1.5");
        Application.launch(MainWindow.class);
    }
    public static class MainWindow extends Application
    {
        public void start(final Stage primaryStage)
        {
            final var button = new Button("Show Dialog");
            button.setOnAction(e ->
            {
                final var alert = new Alert(Alert.AlertType.NONE);
                alert.initOwner(primaryStage);
                alert.getButtonTypes().add(ButtonType.OK);
                alert.getDialogPane().setContent(createTestUi());
                alert.show();
            });
            final var root = createTestUi();
            root.getChildren().add(button);
            primaryStage.setScene(new Scene(root, 400, 600));
            primaryStage.show();
        }
        private VBox createTestUi()
        {
            final var gridPane = new GridPane();
            gridPane.addRow(0, new CheckBox("Checkbox in gridpane"));
            return new VBox(10,
                gridPane,
                new CheckBox("Checkbox outside gridpane"));
        }
    }
}

Reply via email to