Summary
SelectionListNode<T> has no way to fill the height its parent allocates. A
long list is capped at the _visibleRows default of 10 regardless of
terminal size, and the only control — WithVisibleRows(int) — takes a static
count rather than a "fill available space" mode.
Observed in Termina 0.8.0.
Current behavior
In SelectionListNode<T>:
private int _visibleRows = 10;
public SizeConstraint HeightConstraint => SizeConstraint.AutoSize();
Measure(Size available) returns Math.Min(TotalLineCount, _visibleRows) for
height — note it does not consult available.Height.
WithVisibleRows(int rows) sets _visibleRows = Math.Max(1, rows) — the only
knob, and it's a fixed number.
The scrolling internals are otherwise fine: Render clamps
num = Math.Min(_visibleRows, bounds.Height) and draws a scrollbar whenever
TotalLineCount > num. The problem is purely that the node's measured height
is stuck at _visibleRows, with no fill option.
Net effect: drop 100+ items into a SelectionListNode on a 50-row terminal and
only 10 render, with a 10-cell scrollbar floating in an otherwise empty panel.
Why a static WithVisibleRows isn't a fix
EnsureVisible() uses _visibleRows for its scroll-offset math, so
_visibleRows must equal the actual number of rows on screen. Passing a large
constant (WithVisibleRows(1000)) breaks scrolling: the node believes 1000 rows
are visible and never advances _scrollOffset, so the highlight walks off the
bottom of the viewport.
The correct visible-row count is only known at measure/render time (from
available.Height / bounds.Height), which a caller can't supply up front.
Workaround
Consumers can wrap the node in a Fill-constrained LayoutNode whose
Measure/Render push the allocated height into WithVisibleRows(...) before
delegating:
internal sealed class FillSelectionListNode<T> : LayoutNode, IInvalidatingNode
{
private readonly SelectionListNode<T> _inner;
public FillSelectionListNode(SelectionListNode<T> inner)
{
_inner = inner;
Fill();
WidthFill();
}
public Observable<Unit> Invalidated => _inner.Invalidated;
public override Size Measure(Size available)
{
_inner.WithVisibleRows(available.Height);
return available;
}
public override void Render(IRenderContext context, Rect bounds)
{
_inner.WithVisibleRows(bounds.Height);
_inner.Render(context, bounds);
}
public override void Dispose()
{
_inner.Dispose();
base.Dispose();
}
}
This works, but every consumer that wants a full-height list has to reinvent it,
and GetChildNodes() is internal so the wrapper can't participate fully in
tree traversal.
Proposed enhancement
Give SelectionListNode<T> a first-class fill mode. Either:
Option A — explicit builder. WithFillHeight() flips HeightConstraint to
SizeConstraint.Fill, and Measure/Render use available.Height /
bounds.Height as the visible-row count (keeping _visibleRows — or whatever
backs EnsureVisible() — in sync so scroll math and the scrollbar stay
correct).
Option B — honor a caller-set constraint. If the caller sets a Fill
HeightConstraint, size to the allocated height instead of _visibleRows.
Either way the 10-row AutoSize default stays as-is for the short menus that
are the common case — this only adds an opt-in path for long, scrollable lists.
Context
Surfaced while fixing a downstream TUI (netclaw approvals) where a
100+ entry approvals list rendered only 10 rows. The fix there is currently the
FillSelectionListNode<T> wrapper above; it would be deleted in favour of a
built-in fill mode.
Summary
SelectionListNode<T>has no way to fill the height its parent allocates. Along list is capped at the
_visibleRowsdefault of 10 regardless ofterminal size, and the only control —
WithVisibleRows(int)— takes a staticcount rather than a "fill available space" mode.
Observed in Termina 0.8.0.
Current behavior
In
SelectionListNode<T>:private int _visibleRows = 10;public SizeConstraint HeightConstraint => SizeConstraint.AutoSize();Measure(Size available)returnsMath.Min(TotalLineCount, _visibleRows)forheight — note it does not consult
available.Height.WithVisibleRows(int rows)sets_visibleRows = Math.Max(1, rows)— the onlyknob, and it's a fixed number.
The scrolling internals are otherwise fine:
Renderclampsnum = Math.Min(_visibleRows, bounds.Height)and draws a scrollbar wheneverTotalLineCount > num. The problem is purely that the node's measured heightis stuck at
_visibleRows, with no fill option.Net effect: drop 100+ items into a
SelectionListNodeon a 50-row terminal andonly 10 render, with a 10-cell scrollbar floating in an otherwise empty panel.
Why a static
WithVisibleRowsisn't a fixEnsureVisible()uses_visibleRowsfor its scroll-offset math, so_visibleRowsmust equal the actual number of rows on screen. Passing a largeconstant (
WithVisibleRows(1000)) breaks scrolling: the node believes 1000 rowsare visible and never advances
_scrollOffset, so the highlight walks off thebottom of the viewport.
The correct visible-row count is only known at measure/render time (from
available.Height/bounds.Height), which a caller can't supply up front.Workaround
Consumers can wrap the node in a
Fill-constrainedLayoutNodewhoseMeasure/Renderpush the allocated height intoWithVisibleRows(...)beforedelegating:
This works, but every consumer that wants a full-height list has to reinvent it,
and
GetChildNodes()isinternalso the wrapper can't participate fully intree traversal.
Proposed enhancement
Give
SelectionListNode<T>a first-class fill mode. Either:Option A — explicit builder.
WithFillHeight()flipsHeightConstrainttoSizeConstraint.Fill, andMeasure/Renderuseavailable.Height/bounds.Heightas the visible-row count (keeping_visibleRows— or whateverbacks
EnsureVisible()— in sync so scroll math and the scrollbar staycorrect).
Option B — honor a caller-set constraint. If the caller sets a
FillHeightConstraint, size to the allocated height instead of_visibleRows.Either way the 10-row
AutoSizedefault stays as-is for the short menus thatare the common case — this only adds an opt-in path for long, scrollable lists.
Context
Surfaced while fixing a downstream TUI (
netclaw approvals) where a100+ entry approvals list rendered only 10 rows. The fix there is currently the
FillSelectionListNode<T>wrapper above; it would be deleted in favour of abuilt-in fill mode.