class
TUI::Grid
- TUI::Grid
- TUI::Widget
- Reference
- Object
Overview
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.
Defined in:
tui/layout/grid.crConstructors
Instance Method Summary
-
#attach(child : Widget, col : Int32, row : Int32, col_span : Int32 = 1, row_span : Int32 = 1) : Nil
Places
childat grid position (col, row), optionally spanningcol_spancolumns androw_spanrows (row_span lets one child reserve several row-heights, the same role FieldSpec#rows plays for a Form::Host field today). -
#attachments : Array(TUI::Grid::Attachment)
Read-only list of attached children in attachment order — the order Tab/Shift+Tab/Up/Down traverse.
-
#border_style : Style
Applied to the border drawn by #render when #bordered? is true.
-
#border_style=(border_style : Style)
Applied to the border drawn by #render when #bordered? is true.
-
#bordered=(bordered : Bool)
Whether to draw a box border around the grid.
-
#bordered? : Bool
Whether to draw a box border around the grid.
-
#composite(screen : Screen) : Nil
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.
-
#handle_key(ev : KeyEvent) : Bool
Menu (Tab/Shift+Tab) dispatches unconditionally, before the focused child ever sees the key — same as always.
-
#render : Nil
Draw into @buffer using LOCAL coordinates (0, 0 = this widget's own top-left).
-
#scrollbar_style : Style
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.
- #scrollbar_style=(style : Style) : Nil
-
#status_hint : String
Plain text describing the actions available in the widget's current state.
Instance methods inherited from class TUI::Widget
absolute(row : Int32, col : Int32) : NamedTuple(row: Int32, col: Int32)
absolute,
composite(screen : Screen) : Nil
composite,
focus_if(condition : Bool) : Nil
focus_if,
focused=(focused : Bool)
focused=,
focused? : Bool
focused?,
handle_key(ev : KeyEvent) : Bool
handle_key,
height : Int32
height,
height=(height : Int32)
height=,
local(row : Int32, col : Int32) : NamedTuple(row: Int32, col: Int32)
local,
render : Nil
render,
render_to(buffer : Buffer) : Nil
render_to,
status_hint : String
status_hint,
width : Int32
width,
width=(width : Int32)
width=,
x : Int32
x,
x=(x : Int32)
x=,
y : Int32
y,
y=(y : Int32)
y=
Constructor methods inherited from class TUI::Widget
new(x : Int32, y : Int32, width : Int32, height : Int32)
new
Constructor Detail
Instance Method Detail
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.
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.