TUI::Grid
Inherits TUI::Widget < Reference < Object
Positions an arbitrary number of widgets in a row/column grid, GTK Gtk.Grid-style: #attach(child, col, row, col_span, row_span) places a child at a cell (optionally spanning several columns/rows), and Grid repositions it every #composite the same way HSplit repositions its two panes — by writing directly to the child's own x/y/width/height. Each child still owns and composites its own buffer; Grid draws no content of its own beyond an optional border.
Unlike GTK's Grid, columns are NOT sized by measuring each child's
size requisition — this toolkit has no size-negotiation protocol
(Widget's width/height are always set by a parent, never requested
by a child), so auto-sizing from content isn't available to build
on. Instead a caller supplies relative column weights at
construction (e.g. [1] for one full-width column, [1, 2] for a
1:2 split) and #layout converts those to actual pixel widths from
Grid's own current width every #composite — the same "re-derive
from current geometry every frame" technique HSplit#layout uses for
its own ratio mode, generalized from 2 panes to N columns, so a Grid
whose own width changes (e.g. a terminal resize propagating down
through Form::Host, see Form::Host#composite) keeps every attached
child's width in sync automatically rather than staying pinned to
whatever width was current when #attach was first called. Per-row
height stays a fixed cell count (row_span the way FieldSpec#rows
already reserves multiple rows for one field) since rows are a
counted list of fields, not a proportional split of the available
height.
Focus is tracked as a single flat index into attachment order (not
per-row/per-column), generalizing the binary @active pane toggle
HSplit/SplitWindow both hard-code to N children — Tab/Shift+Tab move
forward/backward through attachments and wrap, exactly like
Form::Host's historical Enter-starts-edit / Esc-or-Enter-commits
convention meant Up/Down only ever navigated fields while nothing was
being edited. Up/Down are also bound as a second way to move focus,
matching that same historical Form::Host convention — but as a
fallback, tried only after the focused child's own #handle_key
declines the key (returns false), not intercepted upfront the way
Tab/Shift+Tab are (see #handle_key). This ordering is what lets
Up/Down double as both "move between fields" while idle and "move
the cursor within a field" while a ScrollableField-backed cell
(src/tui/form/scrollable_field.cr) is actively editing multi-line
text: an idle FormFieldCell's own #handle_key only ever consumes
Enter (see FormFieldCell#handle_nav_key), so Up/Down fall through to
Grid; an actively-editing cell always reports the key consumed once
an edit session is open (see FormFieldCell#handle_editor_key), so
Up/Down never reach Grid's fallback while a field is mid-edit,
matching the old Form::Host behavior of Up/Down only navigating
between fields, never while one is being edited.
Scrolls when total attached row extent exceeds Grid's own viewport
(see #total_rows/#visible_rows) — owns a Scroller exactly like
Window/SplitWindow do for their own single Scrollable, generalized
here to a row offset shared by every attachment's row. Unlike
Window, Grid's children are independent Widgets that blit themselves
directly via Screen#blit rather than rendering into one shared
scratch Buffer Grid could bound on its own — so clipping instead
relies on Screen#with_clip (src/tui/core/screen.cr), which bounds
every #blit call for the duration of Grid's own #composite to Grid's
box. This is what lets a child taller than what's currently visible
(row_span > 1, scrolled so only part of it should show) clip
correctly with zero cooperation from the child itself: it always
renders its full local buffer as normal; Screen#blit's clip check
silently drops whichever cells land outside Grid's rect this frame.
PageUp/PageDown/mouse-wheel scroll the same way Up/Down navigate —
as a fallback tried only after the focused child declines the key,
since TextEdit (wrapped by ScrollableField) unconditionally consumes
those keys for its own internal scrolling while being edited.
Constructors
Instance methods
Places child at grid position (col, row), optionally spanning
col_span columns and row_span rows (row_span lets one child
reserve several row-heights, the same role FieldSpec#rows plays
for a Form::Host field today). Re-runs #layout immediately so the
child's geometry is valid even before the next #composite.
Read-only list of attached children in attachment order — the order Tab/Shift+Tab/Up/Down traverse.
Applied to the border drawn by #render when #bordered? is true.
Whether to draw a box border around the grid. Mirrors Window#bordered?.
Wraps the per-child composite loop in Screen#with_clip, bounded to Grid's own inner rect (the same rect its scrollbar/content occupy) — this is what stops an attachment positioned outside the current scroll window from bleeding its cells onto whatever else is on screen below/above Grid, see the class doc comment above. Skipping a fully-out-of-view child's #composite call entirely is a cheap optimization, not load-bearing for correctness: the clip alone would already turn its blit into a no-op.
Menu (Tab/Shift+Tab) dispatches unconditionally, before the focused child ever sees the key — same as always. Everything else goes to the focused child first; only once it declines does Grid try its own fallbacks (scroll, then nav) — see the class doc comment for why this order matters for ScrollableField.
Draw into @buffer using LOCAL coordinates (0, 0 = this widget's own
top-left). Widgets never need to know their own x/y offset to draw
themselves — that arithmetic is handled entirely by composite.
Applied to the scrollbar track/thumb drawn by #render — defaults to whatever #border_style currently is, resolved fresh on every read, same convention as Window#scrollbar_style.
Plain text describing the actions available in the widget's current state. Rendered by the App in the global status bar at the bottom of the screen — widgets must NOT draw their own hint lines.