class TUI::Grid

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

Constructors

Instance Method Summary

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

def self.new(x : Int32, y : Int32, width : Int32, height : Int32, col_weights : Array(Int32), row_height : Int32 = 1, bordered : Bool = false) #

[View source]

Instance Method Detail

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


[View source]
def attachments : Array(TUI::Grid::Attachment) #

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


[View source]
def border_style : Style #

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


[View source]
def border_style=(border_style : Style) #

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


[View source]
def bordered=(bordered : Bool) #

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


[View source]
def bordered? : Bool #

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


[View source]
def 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.


[View source]
def 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.


[View source]
def render : Nil #
Description copied from class TUI::Widget

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.


[View source]
def 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.


[View source]
def scrollbar_style=(style : Style) : Nil #

[View source]
def status_hint : String #
Description copied from class TUI::Widget

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.


[View source]