class

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

new(x : Int32, y : Int32, width : Int32, height : Int32, col_weights : Array(Int32), row_height : Int32 = 1, bordered : Bool = false)
Source

Instance methods

attach(child : Widget, col : Int32, row : Int32, col_span : Int32 = 1, row_span : Int32 = 1) : Nil

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.

Source
attachments

Read-only list of attached children in attachment order — the order Tab/Shift+Tab/Up/Down traverse.

Source
border_style

Applied to the border drawn by #render when #bordered? is true.

Source
border_style=(border_style : Style)

Applied to the border drawn by #render when #bordered? is true.

Source
bordered=(bordered : Bool)

Whether to draw a box border around the grid. Mirrors Window#bordered?.

Source
bordered?

Whether to draw a box border around the grid. Mirrors Window#bordered?.

Source
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. 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.

Source
handle_key(ev : KeyEvent) : Bool

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.

Source
render

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.

Source
scrollbar_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.

Source
scrollbar_style=(style : Style) : Nil
Source
status_hint

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.

Source

Nested types