On Tue, 23 Dec 2025 07:11:04 GMT, John Hendrikx <[email protected]> wrote:
>> It can get complicated, I had to think a bit on the response. The main
>> danger is switching to `snapSize` for your final step, as it is always
>> rounds up, which can easily result in a huge jump (ie. 6.0000001 -> 7) that
>> clearly isn't intended at all.
>>
>> Also, each Node has its own snapping setting. You are doing calculations
>> for the current node. The node you are accessing with `prefHeight` may have
>> a different setting. It is therefore probably wise to snap any external
>> values first so they adhere to the same snapping setting as the current node
>> you are working on before continuing calculations. This is especially
>> important when there are multiple nodes involved as you may get multiple
>> unsnapped values that you really shouldn't add together before snapping them
>> to your current node's setting.
>>
>> But you are also now using the `snapSize` function (in the outer part) which
>> IMHO is the "dangerous" variant as it does a ceiling operation.
>>
>> So if either `spacing` or `x` are even slightly off, it rounds up. For
>> example on a 150% screen pixels are 2/3, so let's say spacing is `x` is 2/3,
>> `spacing` is 4/3, adding those two together can yield subtle errors, and
>> could result in for example `1.9999` or `2.0001`. Now because the outer
>> function is rounding up (to the next pixel), that can get amplified badly.
>> You would expect the result to be `2` (3 pixels of 2/3 size) but it may
>> easily become `2.66666` (4 pixels of 2/3 size)...
>>
>> By applying the ceiling function only to `prefHeight` then rounding, you are
>> much more likely to get a stable result when adding many values together.
>>
>> Perhaps a test case is missing that can show this subtle problem :)
>
> To add a bit more to this, I think there are a few general rules when it
> comes to snapping:
>
> ## 1. Snap External Values Early
> - Each node has its own snap setting.
> - When using values from other nodes (e.g., `prefHeight`), **convert them as
> early as possible into the current node’s snap space** to ensure consistency
> in layout calculations.
>
> ## 2. Use Ceiling for Size Values from Other Nodes
> - External values are usually **sizes**.
> - It is important to fully respect them—for example, a `Text` node requiring
> 11.5774 pixels should not be rounded down to 11.5, as that could trigger
> ellipsis or clipping.
> - When bringing these values into your snap space, use a **ceiling function**
> (`snapSize`) to guarantee the content fits.
>
> ## 3. Use Rounding for Internal Calculations
> - When combining values that **belong to your current node** (spacing,
> margins, gaps) or have been imported already (by snapping an external
> source), use **rounding functions** (`snapPosition`, `snapSpace`).
> - This prevents accumulation of extra pixels that can cause visual
> discontinuities.
>
> ## 4. Current Node Sizes
> - Any sizes that are important for content layout (like text widths or
> heights) should be snapped with **ceiling** (`snapSize`).
> - Other quantities such as spacing or gaps do not need ceiling; small
> discrepancies here are visually insignificant and won’t cause clipping.
> - Apply ceiling **once** when needed; repeated ceiling calls can lead to
> overestimation of total sizes.
>
> ---
>
> ## Summary
>
> | Value Type | Snap Function |
> |--------------------------------|---------------|
> | External node sizes | `ceil` (`snapSize`) |
> | Internal sums / positions | `round` (`snapPosition`, `snapSpace`) |
> | Current node sizes (important) | `ceil` (`snapSize`), once only |
> | Internal spacings / gaps | `round` or unsnapped |
Thanks @hjohn. That all makes sense. I think I am understanding the purpose of
these now (have been shooting from the hip a bit).
Side not: would be good to get the above message somewhere more visible (not
sure where though...), maybe a JavaDoc in Region?
Anyway, after all that, I've pushed what might hopefully be the final change,
and it's confined to just this code in getOverflowNodeIndex()(:
if (node.isManaged()) {
if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
x = snapPositionY(x + snapSizeY(node.prefHeight(-1)) +
getSpacing());
length = snapPositionY(length);
} else {
x = snapPositionX(x + snapSizeX(node.prefWidth(-1)) +
getSpacing());
length = snapPositionX(length);
}
}
I decided to fix `length` within getOverflowNodeIndex() itself rather than in
the calling method (i.e., don't assume it comes pre-snapped). I think since
getOverflowNodeIndex() is doing the comparison, it's its responsibility.
-------------
PR Review Comment: https://git.openjdk.org/jfx/pull/2016#discussion_r2643165825