<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<p>Hm,</p>
<p>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.<br>
</p>
<p>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.</p>
<p>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.</p>
<p>This can be achieved by using the snapPortion functions instead
(which will floor values). Those haven't been exposed in SkinBase
though.<br>
</p>
<p>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...</p>
<p>So, I'd change the calculation for the thumb length to something
like:</p>
<div style="background-color:#ffffff;padding:0px 0px 0px 2px;">
<div
style="color:#000000;background-color:#ffffff;font-family:"Consolas";font-size:11pt;white-space:pre;"><p
style="margin:0;"><span style="color:#000000;"> </span><span
style="color:#0000c0;">thumbLength</span><span
style="color:#000000;"> = visiblePortion == 1.0 ? trackLength : snapPortionY(Utils.</span><span
style="color:#000000;font-style:italic;">clamp</span><span
style="color:#000000;">(minThumbLength(), (</span><span
style="color:#0000c0;">trackLength</span><span
style="color:#000000;"> * </span><span
style="color:#000000;background-color:#d4d4d4;">visiblePortion</span><span
style="color:#000000;">), </span><span style="color:#0000c0;">trackLength</span><span
style="color:#000000;">));</span></p><p style="margin:0;">
</p></div>
</div>
<p></p>
<p>--John<br>
</p>
<div class="moz-cite-prefix">On 06/10/2024 07:31, dandem sai pradeep
wrote:<br>
</div>
<blockquote type="cite"
cite="mid:CACCExvZrxmYKY5QVdUPa37DQLd5Y6EsCzTrdkjq95TmXUAKVkQ@mail.gmail.com">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<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"
moz-do-not-send="true">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:part1.4lEklkVe.BmyGokSl@gmail.com"
alt="LPH0Ukdr.png" class="" 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:part2.1YMPH5FC.6yt8M03v@gmail.com"
alt="pBqQPKDf.gif" class="" 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"><a class="moz-txt-link-rfc2396E" href="data:text/css,">"data:text/css,"</a></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>
</blockquote>
</body>
</html>