Hm,

First I noticed that FX will display a thumb that is the full length of the track when there is nothing to scroll; that's unusual, most scrollbars will hide the thumb when there is nothing to scroll in the case where the bar is set to always visible.  Perhaps something to enhance.

So I was going to say that thumb length == track length is always wrong, but apparently in FX that's allowed.  Still, if the visible portion isn't 100% (1.0), then the thumb should never be the same length as the track as otherwise you can't set the scrollbar's position to its minimum and maximum positions.

So I think it's a bug.  The length of the thumb should be equal to the track length if visible portion is 1.0, otherwise it must be at least one unit smaller in size.

This can be achieved by using the snapPortion functions instead (which will floor values).  Those haven't been exposed in SkinBase though.

I also think it may be wise to make a special case for visiblePortion = 1.0; the result of the visiblePortion multiplication, then clamp, then snapping (which involves multiplication and division) may be subject to some rounding and floating point errors, and it would be a shame if the calculation results in a value that differs from trackLength when visiblePortion == 1.0...

So, I'd change the calculation for the thumb length to something like:

thumbLength= visiblePortion == 1.0 ? trackLength : snapPortionY(Utils.clamp(minThumbLength(), (trackLength* visiblePortion), trackLength));

--John

On 06/10/2024 07:31, dandem sai pradeep wrote:
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/.

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.

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.

Reply via email to