<!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>