<div dir="ltr"><div>Hi,</div><div>
<p>In <code>Region</code> class we have the property <code>snapToPixel</code>, which is intended to snap/round-off the pixel values of bounds for achieving crisp user interfaces (as mentioned in the <a href="https://download.java.net/java/GA/javafx21.0.1/e5ab43c6aed54893b0840c1f2dcfca4d/docs/api/javafx.graphics/javafx/scene/layout/Region.html#snapToPixelProperty()" rel="nofollow noreferrer" target="_blank">javadoc</a>).</p>
<p>But is it intended/expected that, toggling this property value can
affect the core layout behavior? At-least for standard controls?</p>
<p><b>The Problem:</b></p>
<p>In scenarios, when the <code>TableRow</code> width is 1px greater than the <code>VirtualFlow</code> width, the horizontal scroll bar of <code>VirtualFlow</code> is displayed. Which is valid and I agree with that (as in below screenshot). <i>In the below picture, the VirtualFlow width is 307px and the TableRow width is 308px</i>.</p>
</div><div><img src="cid:ii_m1vmrwbb0" alt="LPH0Ukdr.png" width="322" height="113"></div><div><br></div><div><button type="button" id="m_-2723731851586228177gmail-saves-btn-79053064" aria-controls="" aria-describedby="--stacks-s-tooltip-zxnz25er"></button>
<div><p>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).</p>
<p>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.</p>
<p>Below is the code of thumb drag event in ScrollBarSkin class, where you can notice the <code>if</code> condition to skip the computing if thumb length is not less than track length.</p>
</div><div><br></div><div>
<pre class="gmail-lang-java gmail-s-code-block"><code class="gmail-hljs gmail-language-java">thumb.setOnMouseDragged(me -> {
<span class="gmail-hljs-keyword">if</span> (me.isSynthesized()) {
<span class="gmail-hljs-comment">// touch-screen events handled by Scroll handler</span>
me.consume();
<span class="gmail-hljs-keyword">return</span>;
}
<span class="gmail-hljs-comment">/*
** if max isn't greater than min then there is nothing to do here
*/</span>
<span class="gmail-hljs-keyword">if</span> (getSkinnable().getMax() > getSkinnable().getMin()) {
<span class="gmail-hljs-comment">/*
** if the tracklength isn't greater then do nothing....
*/</span>
<span class="gmail-hljs-keyword">if</span> (trackLength > thumbLength) {
<span class="gmail-hljs-type">Point2D</span> <span class="gmail-hljs-variable">cur</span> <span class="gmail-hljs-operator">=</span> thumb.localToParent(me.getX(), me.getY());
<span class="gmail-hljs-keyword">if</span> (dragStart == <span class="gmail-hljs-literal">null</span>) {
<span class="gmail-hljs-comment">// we're getting dragged without getting a mouse press</span>
dragStart = thumb.localToParent(me.getX(), me.getY());
}
<span class="gmail-hljs-type">double</span> <span class="gmail-hljs-variable">dragPos</span> <span class="gmail-hljs-operator">=</span> getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX();
behavior.thumbDragged(preDragThumbPos + dragPos / (trackLength - thumbLength));
}
me.consume();
}
});</code></pre>
</div><div><br></div><div>
Now, when I further investigate why the <code>track</code> and <code>thumb</code> lengths are same, I noticed that it is in fact the code of thumb length calcuation that snaps the computed value. <br></div><div><br></div><div>
<pre class="gmail-lang-java gmail-s-code-block"><code class="gmail-hljs gmail-language-java">thumbLength = snapSizeX(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength));</code></pre>
</div><div><br></div><div>
In the above line, the computed <code>trackLength</code> is 289.0px and the computed <code>thumbLength</code> is 288.06168831168833px. But because this value is snapped, the value is rounded to 289.0px which is equal to trackLength. <br></div><div><br></div><div>
<p><strong>Solution / Workaround:</strong></p>
<p>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.</p>
<pre class="gmail-lang-java gmail-s-code-block"><code class="gmail-hljs gmail-language-java"><span class="gmail-hljs-keyword">class</span> <span class="gmail-hljs-title gmail-class_">CustomVirtualFlow</span><T <span class="gmail-hljs-keyword">extends</span> <span class="gmail-hljs-title gmail-class_">IndexedCell</span>> <span class="gmail-hljs-keyword">extends</span> <span class="gmail-hljs-title gmail-class_">VirtualFlow</span><T> {
<span class="gmail-hljs-keyword">public</span> <span class="gmail-hljs-title gmail-function_">CustomVirtualFlow</span><span class="gmail-hljs-params">()</span> {
getHbar().setSnapToPixel(<span class="gmail-hljs-literal">false</span>);
getVbar().setSnapToPixel(<span class="gmail-hljs-literal">false</span>);
}
}
</code></pre>
<p>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.</p>
</div><div><img src="cid:ii_m1x5aayu1" alt="pBqQPKDf.gif" width="532" height="408"><br><br></div><div>
<p>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?</p>
<p>Below is the demo code:</p>
<pre class="gmail-lang-java gmail-s-code-block"><code class="gmail-hljs gmail-language-java"><span class="gmail-hljs-keyword">import</span> javafx.application.Application;
<span class="gmail-hljs-keyword">import</span> javafx.beans.property.ReadOnlyObjectWrapper;
<span class="gmail-hljs-keyword">import</span> javafx.geometry.Insets;
<span class="gmail-hljs-keyword">import</span> javafx.scene.Scene;
<span class="gmail-hljs-keyword">import</span> javafx.scene.control.*;
<span class="gmail-hljs-keyword">import</span> javafx.scene.control.skin.TableViewSkin;
<span class="gmail-hljs-keyword">import</span> javafx.scene.control.skin.VirtualFlow;
<span class="gmail-hljs-keyword">import</span> javafx.scene.layout.Priority;
<span class="gmail-hljs-keyword">import</span> javafx.scene.layout.VBox;
<span class="gmail-hljs-keyword">import</span> javafx.stage.Stage;
<span class="gmail-hljs-keyword">import</span> java.util.ArrayList;
<span class="gmail-hljs-keyword">import</span> java.util.List;
<span class="gmail-hljs-keyword">public</span> <span class="gmail-hljs-keyword">class</span> <span class="gmail-hljs-title gmail-class_">HorizontalScrollBarIssueDemo</span> <span class="gmail-hljs-keyword">extends</span> <span class="gmail-hljs-title gmail-class_">Application</span> {
<span class="gmail-hljs-type">String</span> <span class="gmail-hljs-variable">CSS</span> <span class="gmail-hljs-operator">=</span> <span class="gmail-hljs-string">"data:text/css,"</span> +
<span class="gmail-hljs-string">"""
.label{
-fx-font-weight: bold;
}
"""</span>;
<span class="gmail-hljs-meta">@Override</span>
<span class="gmail-hljs-keyword">public</span> <span class="gmail-hljs-keyword">void</span> <span class="gmail-hljs-title gmail-function_">start</span><span class="gmail-hljs-params">(<span class="gmail-hljs-keyword">final</span> Stage stage)</span> <span class="gmail-hljs-keyword">throws</span> Exception {
<span class="gmail-hljs-keyword">final</span> <span class="gmail-hljs-type">VBox</span> <span class="gmail-hljs-variable">root</span> <span class="gmail-hljs-operator">=</span> <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">VBox</span>();
root.setSpacing(<span class="gmail-hljs-number">30</span>);
root.setPadding(<span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">Insets</span>(<span class="gmail-hljs-number">10</span>));
<span class="gmail-hljs-keyword">final</span> <span class="gmail-hljs-type">Scene</span> <span class="gmail-hljs-variable">scene</span> <span class="gmail-hljs-operator">=</span> <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">Scene</span>(root, <span class="gmail-hljs-number">500</span>, <span class="gmail-hljs-number">350</span>);
scene.getStylesheets().add(CSS);
stage.setScene(scene);
stage.setTitle(<span class="gmail-hljs-string">"Horizontal ScrollBar Issue "</span> + System.getProperty(<span class="gmail-hljs-string">"javafx.runtime.version"</span>));
stage.show();
<span class="gmail-hljs-type">VBox</span> <span class="gmail-hljs-variable">defaultBox</span> <span class="gmail-hljs-operator">=</span> <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">VBox</span>(<span class="gmail-hljs-number">10</span>, <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">Label</span>(<span class="gmail-hljs-string">"Default TableView"</span>), getTable(<span class="gmail-hljs-literal">false</span>));
<span class="gmail-hljs-type">VBox</span> <span class="gmail-hljs-variable">customBox</span> <span class="gmail-hljs-operator">=</span> <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">VBox</span>(<span class="gmail-hljs-number">10</span>, <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">Label</span>(<span class="gmail-hljs-string">"TableView whose scrollbar's snapToPixel=false"</span>), getTable(<span class="gmail-hljs-literal">true</span>));
root.getChildren().addAll(defaultBox, customBox);
}
<span class="gmail-hljs-keyword">private</span> TableView<List<String>> <span class="gmail-hljs-title gmail-function_">getTable</span><span class="gmail-hljs-params">(<span class="gmail-hljs-type">boolean</span> custom)</span> {
TableView<List<String>> tableView;
<span class="gmail-hljs-keyword">if</span> (custom) {
tableView = <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">TableView</span><>() {
<span class="gmail-hljs-meta">@Override</span>
<span class="gmail-hljs-keyword">protected</span> Skin<?> createDefaultSkin() {
<span class="gmail-hljs-keyword">return</span> <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">TableViewSkin</span><>(<span class="gmail-hljs-built_in">this</span>) {
<span class="gmail-hljs-meta">@Override</span>
<span class="gmail-hljs-keyword">protected</span> VirtualFlow<TableRow<List<String>>> <span class="gmail-hljs-title gmail-function_">createVirtualFlow</span><span class="gmail-hljs-params">()</span> {
<span class="gmail-hljs-keyword">return</span> <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">CustomVirtualFlow</span><>();
}
};
}
};
} <span class="gmail-hljs-keyword">else</span> {
tableView = <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">TableView</span><>();
}
tableView.setMaxHeight(<span class="gmail-hljs-number">98</span>);
tableView.setMaxWidth(<span class="gmail-hljs-number">309</span>);
VBox.setVgrow(tableView, Priority.ALWAYS);
<span class="gmail-hljs-type">int</span> <span class="gmail-hljs-variable">colCount</span> <span class="gmail-hljs-operator">=</span> <span class="gmail-hljs-number">4</span>;
<span class="gmail-hljs-keyword">for</span> (<span class="gmail-hljs-type">int</span> <span class="gmail-hljs-variable">i</span> <span class="gmail-hljs-operator">=</span> <span class="gmail-hljs-number">0</span>; i < colCount; i++) {
<span class="gmail-hljs-keyword">final</span> <span class="gmail-hljs-type">int</span> <span class="gmail-hljs-variable">index</span> <span class="gmail-hljs-operator">=</span> i;
TableColumn<List<String>, String> column = <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">TableColumn</span><>(<span class="gmail-hljs-string">"Option "</span> + i);
column.setCellValueFactory(param -> <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">ReadOnlyObjectWrapper</span><>(param.getValue().get(index)));
tableView.getColumns().add(column);
}
<span class="gmail-hljs-keyword">for</span> (<span class="gmail-hljs-type">int</span> <span class="gmail-hljs-variable">j</span> <span class="gmail-hljs-operator">=</span> <span class="gmail-hljs-number">0</span>; j < <span class="gmail-hljs-number">1</span>; j++) {
List<String> row = <span class="gmail-hljs-keyword">new</span> <span class="gmail-hljs-title gmail-class_">ArrayList</span><>();
<span class="gmail-hljs-keyword">for</span> (<span class="gmail-hljs-type">int</span> <span class="gmail-hljs-variable">i</span> <span class="gmail-hljs-operator">=</span> <span class="gmail-hljs-number">0</span>; i < colCount; i++) {
row.add(<span class="gmail-hljs-string">"Row"</span> + j + <span class="gmail-hljs-string">"-Opt"</span> + i);
}
tableView.getItems().add(row);
}
<span class="gmail-hljs-keyword">return</span> tableView;
}
<span class="gmail-hljs-keyword">class</span> <span class="gmail-hljs-title gmail-class_">CustomVirtualFlow</span><T <span class="gmail-hljs-keyword">extends</span> <span class="gmail-hljs-title gmail-class_">IndexedCell</span>> <span class="gmail-hljs-keyword">extends</span> <span class="gmail-hljs-title gmail-class_">VirtualFlow</span><T> {
<span class="gmail-hljs-keyword">public</span> <span class="gmail-hljs-title gmail-function_">CustomVirtualFlow</span><span class="gmail-hljs-params">()</span> {
getHbar().setSnapToPixel(<span class="gmail-hljs-literal">false</span>);
getVbar().setSnapToPixel(<span class="gmail-hljs-literal">false</span>);
}
}
}</code></pre>
</div><div><button class="gmail-js-saves-btn gmail-s-btn gmail-s-btn__unset gmail-c-pointer gmail-py4" type="button" id="gmail-saves-btn-79053064" aria-controls="" aria-describedby="--stacks-s-tooltip-zxnz25er"></button></div>
</div><div><br></div><div>Regards,<br>Sai Pradeep Dandem.</div></div>