Do snapToPixel property can impact my control layout behaviour?

dandem sai pradeep saipradeep.dandem at gmail.com
Sun Oct 6 05:31:41 UTC 2024


Hi,

In Region class we have the property snapToPixel, which is intended to
snap/round-off the pixel values of bounds for achieving crisp user
interfaces (as mentioned in the javadoc
<https://download.java.net/java/GA/javafx21.0.1/e5ab43c6aed54893b0840c1f2dcfca4d/docs/api/javafx.graphics/javafx/scene/layout/Region.html#snapToPixelProperty()>
).

But is it intended/expected that, toggling this property value can affect
the core layout behavior? At-least for standard controls?

*The Problem:*

In scenarios, when the TableRow width is 1px greater than the VirtualFlow
width, the horizontal scroll bar of VirtualFlow is displayed. Which is
valid and I agree with that (as in below screenshot). *In the below
picture, the VirtualFlow width is 307px and the TableRow width is 308px*.
[image: LPH0Ukdr.png]

In the above scenario, when I drag the scroll bar thumb, I would expect a
one pixel movement of the TableRow. But that is not happening. In fact,
nothing happens when I drag the thumb (neither the content is moving nor
the thumb is moving).

Upon careful investigation, it is found that the scroll bar track width is
same as thumb width, which is causing to ignore the drag event.

Below is the code of thumb drag event in ScrollBarSkin class, where you can
notice the if condition to skip the computing if thumb length is not less
than track length.

thumb.setOnMouseDragged(me -> {
    if (me.isSynthesized()) {
        // touch-screen events handled by Scroll handler
        me.consume();
        return;
    }
    /*
    ** if max isn't greater than min then there is nothing to do here
    */
    if (getSkinnable().getMax() > getSkinnable().getMin()) {
        /*
        ** if the tracklength isn't greater then do nothing....
        */
        if (trackLength > thumbLength) {
            Point2D cur = thumb.localToParent(me.getX(), me.getY());
            if (dragStart == null) {
                // we're getting dragged without getting a mouse press
                dragStart = thumb.localToParent(me.getX(), me.getY());
            }
            double dragPos = getSkinnable().getOrientation() ==
Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() -
dragStart.getX();
            behavior.thumbDragged(preDragThumbPos + dragPos /
(trackLength - thumbLength));
        }

        me.consume();
    }
});


Now, when I further investigate why the track and thumb lengths are same, I
noticed that it is in fact the code of thumb length calcuation that snaps
the computed value.

thumbLength = snapSizeX(Utils.clamp(minThumbLength(), (trackLength *
visiblePortion), trackLength));


In the above line, the computed trackLength is 289.0px and the computed
thumbLength is 288.06168831168833px. But because this value is snapped, the
value is rounded to 289.0px which is equal to trackLength.

*Solution / Workaround:*

>From the above observation, it is clear that the snapToPixel property of
ScrollBar is impacting the computed values. So I created a custom
VirtualFlow to access the scroll bars and turn off the snapToPixel property.

class CustomVirtualFlow<T extends IndexedCell> extends VirtualFlow<T> {
    public CustomVirtualFlow() {
        getHbar().setSnapToPixel(false);
        getVbar().setSnapToPixel(false);
    }
}

Once I included this tweak, the scroll bar is active and when I drag the
thumb, it is sliding my content. You can notice the difference in the below
gif.
[image: pBqQPKDf.gif]

So my question here is: Is this intended behavior to have snapToPixel=true
by default, which is causing to show the scrollbar unnecessarily and it is
my responsibility to turn off the snapToPixel properties? Or in other
words, is this a JavaFX bug or not?

Below is the demo code:

import javafx.application.Application;import
javafx.beans.property.ReadOnlyObjectWrapper;import
javafx.geometry.Insets;import javafx.scene.Scene;import
javafx.scene.control.*;import
javafx.scene.control.skin.TableViewSkin;import
javafx.scene.control.skin.VirtualFlow;import
javafx.scene.layout.Priority;import javafx.scene.layout.VBox;import
javafx.stage.Stage;
import java.util.ArrayList;import java.util.List;
public class HorizontalScrollBarIssueDemo extends Application {

    String CSS = "data:text/css," +
            """
                   .label{
                        -fx-font-weight: bold;
                    }
                    """;

    @Override
    public void start(final Stage stage) throws Exception {
        final VBox root = new VBox();
        root.setSpacing(30);
        root.setPadding(new Insets(10));
        final Scene scene = new Scene(root, 500, 350);
        scene.getStylesheets().add(CSS);
        stage.setScene(scene);
        stage.setTitle("Horizontal ScrollBar Issue " +
System.getProperty("javafx.runtime.version"));
        stage.show();

        VBox defaultBox = new VBox(10, new Label("Default TableView"),
getTable(false));
        VBox customBox = new VBox(10, new Label("TableView whose
scrollbar's snapToPixel=false"), getTable(true));
        root.getChildren().addAll(defaultBox, customBox);
    }

    private TableView<List<String>> getTable(boolean custom) {
        TableView<List<String>> tableView;
        if (custom) {
            tableView = new TableView<>() {
                @Override
                protected Skin<?> createDefaultSkin() {
                    return new TableViewSkin<>(this) {
                        @Override
                        protected VirtualFlow<TableRow<List<String>>>
createVirtualFlow() {
                            return new CustomVirtualFlow<>();
                        }
                    };
                }
            };
        } else {
            tableView = new TableView<>();
        }
        tableView.setMaxHeight(98);
        tableView.setMaxWidth(309);
        VBox.setVgrow(tableView, Priority.ALWAYS);
        int colCount = 4;
        for (int i = 0; i < colCount; i++) {
            final int index = i;
            TableColumn<List<String>, String> column = new
TableColumn<>("Option " + i);
            column.setCellValueFactory(param -> new
ReadOnlyObjectWrapper<>(param.getValue().get(index)));
            tableView.getColumns().add(column);
        }
        for (int j = 0; j < 1; j++) {
            List<String> row = new ArrayList<>();
            for (int i = 0; i < colCount; i++) {
                row.add("Row" + j + "-Opt" + i);
            }
            tableView.getItems().add(row);
        }

        return tableView;
    }

    class CustomVirtualFlow<T extends IndexedCell> extends VirtualFlow<T> {
        public CustomVirtualFlow() {
            getHbar().setSnapToPixel(false);
            getVbar().setSnapToPixel(false);
        }
    }
}


Regards,
Sai Pradeep Dandem.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20241006/75ba8d96/attachment-0003.htm>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: LPH0Ukdr.png
Type: image/png
Size: 3231 bytes
Desc: not available
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20241006/75ba8d96/LPH0Ukdr-0003.png>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: pBqQPKDf.gif
Type: image/gif
Size: 76589 bytes
Desc: not available
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20241006/75ba8d96/pBqQPKDf-0003.gif>


More information about the openjfx-dev mailing list