fix(gallery): stop MediaInfoPane's horizontal and vertical scrollbars

Both traced to the same root cause: rows (the VBox scroll wraps) was
left at its default min width, which VBox computes from its widest
child — pinned wide here by hashRow's SelectableTextFlow, which by
design keeps its full unwrapped hash on one line. A Region can never
render narrower than its own min width, so fitToWidth was unable to
actually shrink rows down to the viewport, regardless of how much of
it was designed to wrap instead — hence a horizontal scrollbar
despite every row being built to shrink, and a wrong vertical
vbar-needed check riding along on that same too-wide layout.

rows.setMinWidth(0) lets it actually shrink to the viewport width
fitToWidth already asks for; hbarPolicy is now a flat NEVER, since a
fixed-width column like this one should always wrap, never scroll
sideways. That fixed the vbar phantom too, so the previous commit's
reactive height-driven policy toggle (itself a source of feedback-
loop risk between vbarPolicy and viewport width) is no longer needed
and is removed in favour of a plain AS_NEEDED.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016okXfGz39FtQawFWDYpQ5C
This commit is contained in:
2026-09-22 22:40:48 -04:00
co-authored by Claude Sonnet 5
parent 15666a6063
commit e8351cb2d5
@@ -167,18 +167,23 @@ public class MediaInfoPane extends StackPane implements Disposable {
header.setPadding(new Insets(12));
rows.setPadding(new Insets(0, 16, 16, 16));
// VBox's default min width is its widest child's own computed min — driven up here by hashRow's
// SelectableTextFlow, which (see that row's own comment) pins itself to its full unwrapped natural
// width. Left at that default, fitToWidth below is unable to shrink rows down to the viewport's own
// width at all (a Region can never render narrower than its min), so rows renders wider than the
// panel regardless of what fitToWidth asks for — hence a horizontal scrollbar despite everything
// being designed to wrap/shrink instead, and, since a too-wide layout throws off the vertical
// vbar-needed check too (it's computed against that same wrong width), a vertical one alongside it.
rows.setMinWidth(0);
scroll.setFitToWidth(true);
// AS_NEEDED is the obvious policy here, but JavaFX's own vbar-needed check compares rows' preferred
// height against a viewport width estimated before that same check settles — with wrapText labels
// inside rows (whose preferred height depends on the width they wrap at), that estimate regularly
// lands a hair over the real viewport height and leaves the scrollbar shown even once layout
// settles back to plenty of empty space underneath. refreshScrollPolicy below drives the policy
// from rows' and the viewport's own actual, already-settled heights instead.
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
// This is a fixed-width column: wrapping (already how every row here is built) is always the right
// response to not enough width, never a horizontal scroll — NEVER, not AS_NEEDED, makes that the
// actual guarantee rather than something that merely holds as long as rows.setMinWidth(0) above
// keeps working.
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
VBox.setVgrow(scroll, Priority.ALWAYS);
scroll.viewportBoundsProperty().addListener((obs, oldBounds, newBounds) -> refreshScrollPolicy());
rows.heightProperty().addListener((obs, oldHeight, newHeight) -> refreshScrollPolicy());
HBox actionsBar = buildActionsBar();
@@ -189,18 +194,6 @@ public class MediaInfoPane extends StackPane implements Disposable {
getChildren().add(body);
}
/**
* {@link #scroll}'s vbar only when {@link #rows} actually overflows the viewport it's currently given
* — see {@link #scroll}'s own setup in the constructor for why {@code AS_NEEDED} alone leaves a phantom
* scrollbar shown with nothing left to scroll to.
*/
private void refreshScrollPolicy() {
var bounds = scroll.getViewportBounds();
double viewportHeight = bounds == null ? 0 : bounds.getHeight();
boolean overflows = rows.getHeight() > viewportHeight + 1; // +1: guards float rounding, not a real threshold
scroll.setVbarPolicy(overflows ? ScrollPane.ScrollBarPolicy.AS_NEEDED : ScrollPane.ScrollBarPolicy.NEVER);
}
/**
* Docked under {@link #scroll}, outside it — always visible/managed regardless of how many rows fit
* above, unlike anything inside the scrolling area. Just {@link #detailsButton} for now, pinned to the