Do snapToPixel property can impact my control layout behaviour?

John Hendrikx john.hendrikx at gmail.com
Mon Oct 7 14:55:29 UTC 2024


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.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20241007/45d021ca/attachment-0001.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/20241007/45d021ca/LPH0Ukdr-0001.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/20241007/45d021ca/pBqQPKDf-0001.gif>


More information about the openjfx-dev mailing list