module

Tryst::UI::WidgetDSL

The build surface: ui.<widget> methods that APPEND nodes to the Document tree. They never touch Tk - widgets become live only when the realizer runs at realize.

Included classes must provide @document (a Document) and @stack (an Array(Node), current-parent stack seeded with @document.root) - the real Session sets these up in its own #initialize, same as ruby's version. They must also provide #build_open? (a predicate the tree-mutating methods below check via #raise_if_closed! - true before the initial realize and again for the duration of an #add block, false otherwise).

@vars and @images default to empty arrays here (like @stack below), so Session needs no separate initialization for them - Session#realize reads both directly to realize every declared Var and Image before the widget tree itself realizes. @scope_stack is the naming-scope stack #component pushes and pops (seeded with Scope::TOP_LEVEL, so a build that never calls #component is one flat namespace) - see #component and #current_scope.

Only the generic leaf/container append machinery and the widget types built up across the tryst-ui epic's phases are ported here - see widget_type.cr's own doc comment for what's deferred and why. Every ui.<type> method below returns a Handle, matching ruby's own DSL exactly - most are one-line leaf_widget/container_widget macro calls (see those macros' own doc comments), left hand-written only where a type's real signature doesn't fit that plain shape (table's show: default, window's on_close:, tab/pane's positional label/ weight params, split's orientation:, spacer/cell/stretch/overlay's own bespoke shapes).

Instance methods

[](name : Symbol) : Handle

Look up a named widget declared in the current scope, raising KeyError if there's no such name - Crystal's convention throughout, where #[] raises and #[]? is the one that hands back nil.

Raising is what most lookups want, and reads far better where a handle is being kept: @board = ui[:board] types as a Handle, so a class can hold its widgets without a nil check per field. Reach for #[]? when a name legitimately might not be there.

Source
[]?(name : Symbol) : Handle | Nil

Look up a named widget declared in the current scope. nil if nothing by that name exists (yet, or ever).

Source
build_open?
Source
button(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle
Source
canvas(name : Symbol | Nil = nil, **opts, & : self -> Nil) : Handle
Source
canvas(name : Symbol | Nil = nil, **opts) : Handle
Source
cell(row : Int32, col : Int32, colspan : Int32 = 1, rowspan : Int32 = 1, sticky : Symbol | String | Nil = nil, padx : Int32 | Nil = nil, pady : Int32 | Nil = nil, ipadx : Int32 | Nil = nil, ipady : Int32 | Nil = nil, & : self -> Nil) : Nil

Position the single widget declared in the block at (row, col) in the enclosing ui.grid. Only valid directly inside a grid's block.

colspan/rowspan widen the cell across neighbouring columns/rows. sticky/padx/pady/ipadx/ipady override this one cell's placement; left alone, the grid's own defaults apply (see Realizer#arrange_grid).

Source
cell(row : Int32, col : Int32, colspan : Int32 = 1, rowspan : Int32 = 1, sticky : Symbol | String | Nil = nil, padx : Int32 | Nil = nil, pady : Int32 | Nil = nil, ipadx : Int32 | Nil = nil, ipady : Int32 | Nil = nil) : Nil

Raises ArgumentError if this cell's block builds anything other than exactly one widget.

Source
checkbox(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle
Source
column(name : Symbol | Nil = nil, **opts, & : self -> Nil) : Handle

Stack children top to bottom (#column) or left to right (#row). gap: is the pixels between children, pad: the pixels between the container's edge and its content, align: where each child sits on the cross axis (:start/:center/:end, or :stretch to fill it); a child declared grow: true takes the leftover space on the main axis. #panel/#group/#tab/#pane/#window stack like a column and take the same options; anywhere else these are rejected at validation - see LayoutIntentValidator for what honours what.

Source
column(index : Int32, weight : Int32 | Nil = nil, min_size : Int32 | Nil = nil) : Nil

Precise, single-column control over grid columnconfigure - weight: and min_size: (-minsize), independently of #stretch's flat every-listed-column-gets-weight-1 shape. Only valid directly inside a grid's block.

Overloads ui.column (the flow container widget, defined above via container_widget) rather than colliding with it: that one's name: is Symbol?, so a call passing an Int32 index here - grid.column(0, ...) - only ever matches this overload.

Source
column(name : Symbol | Nil = nil, **opts) : Handle

Stack children top to bottom (#column) or left to right (#row). gap: is the pixels between children, pad: the pixels between the container's edge and its content, align: where each child sits on the cross axis (:start/:center/:end, or :stretch to fill it); a child declared grow: true takes the leftover space on the main axis. #panel/#group/#tab/#pane/#window stack like a column and take the same options; anywhere else these are rejected at validation - see LayoutIntentValidator for what honours what.

Source
component(label : Symbol | String | Nil = nil, & : self -> Nil) : Nil

Opens an isolated naming scope for the duration of the block, so a reusable subtree (a card, a settings tab, a row template) can name its own widgets freely without colliding with another copy of itself, or with anything else in the document:

ui.component(:gamepad) { |comp| comp.button(:reset, text: "Reset") }
ui.component(:achievements) { |comp| comp.button(:reset, text: "Reset") }

Purely a naming boundary - no node of its own. Whatever the block declares attaches to the enclosing container exactly as it would without the wrapper, and realizes to the same Tk paths (two mounts requesting the same path segment under one parent get disambiguated - see Document#claim_path_segment). Every call opens a genuinely fresh Scope, even with the same label: label is for error messages and debugging, never identity.

Names are exactly as visible as the scope they were declared in. Inside the block, #[]/#[]? and an event's target: see only names declared inside this same component - NOT the enclosing scope's or the top level's, there is no lexical fallback up the chain - and from outside, a component's names are invisible. Handles are the way across the boundary: every ui.<widget> call still returns one, and a component's caller keeps whichever it needs, same as any other build.

Ordinary containers (row/column/panel/tab/...) never scope - a name declared inside a plain row is addressable from outside it. Only this method opens a scope, deliberately: the flat namespace stays the default for any build that doesn't ask for more.

Source
context_menu(name : Symbol | Nil = nil, **opts, & : MenuBuilder -> Nil) : Handle

A standalone popup menu - built the same declarative way as a menu_bar's dropdowns, but not attached to anything automatically. Wire it to a widget with handle.on_right_click(this).

Source
context_menu(name : Symbol | Nil = nil, **opts) : Handle
Source
current_path

The current build-parent ancestry, as a readable breadcrumb (e.g. "column > row") - derived from @stack, the one thing only the builder (not the Document) knows: which containers are currently open. Useful in a build-time error message or just to orient yourself while poking around mid-build. Returns "(top level)" when nothing is currently open.

Source
divider(name : Symbol | Nil = nil, orientation : SplitOrientation = :horizontal, bind : Var | Nil = nil, **opts) : Handle

A rule between sections. orientation: mirrors #split's own param name and values for API consistency.

Source
font(family : String | Nil = nil, size : Int32 | Nil = nil, bold : Bool = false, italic : Bool = false, underline : Bool = false) : Font

A Font value, buildable and inspectable with no interpreter at all (a plain record - see Font) - the canonical way to spell font: anywhere a widget or ui.style takes it, instead of Tk's own font-list syntax and brace quoting.

ui.font(size: 24)                          # default family at 24
ui.font("Helvetica", 18)                   # positional family, size
ui.font("Comic Sans MS", 14, bold: true)   # spaces safe, modifiers as bools
Source
grid(name : Symbol | Nil = nil, **opts, & : self -> Nil) : Handle
Source
grid(name : Symbol | Nil = nil, **opts) : Handle
Source
group(name : Symbol | Nil = nil, **opts, & : self -> Nil) : Handle

A titled container: a panel with a caption drawn into its border, passed as text:. Stacks its children like #panel does, with the same spacing options.

Source
group(name : Symbol | Nil = nil, **opts) : Handle

A titled container: a panel with a caption drawn into its border, passed as text:. Stacks its children like #panel does, with the same spacing options.

Source
image(path : String, width : Int32 | Nil = nil, height : Int32 | Nil = nil, format : String | Nil = nil, palette : String | Nil = nil, gamma : Float64 | Nil = nil, subsample : Int32 | Nil = nil) : Image

Declare an image loaded from a file - any format Tk's own image create photo -file accepts (PNG, GIF, ...). Its Tcl image name is allocated now (no interpreter needed - it's just a string), so a widget can name it as an image: option straight away; the backing Tryst::Photo and the file load itself only happen at realize (Session#realize runs Image#realize on every declared Image before the widget tree itself realizes).

Pass it along as image: img.name - see Image on why an Image can't be an option value directly the way ruby-tryst's can.

The remaining arguments are forwarded to Tryst::Photo.new. Ruby forwards an opts Hash; they're spelled out here because Crystal can't splat one into a method with named parameters.

Owned by whichever container is currently open (the top of @stack) - Handle#destroy! releases every image a destroyed subtree owns this way, so a thumbnail declared inside a row that later gets destroyed and rebuilt doesn't accumulate a live Tk photo per cycle. See Node#images.

Source
label(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle
Source
list(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle
Source
number_box(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle

A numeric stepper: from:/to: bound the range, increment: sets the step, and bind: a Var to read or set the value.

Source
on_key(spec : Symbol | String, &block : Array(String), CallbackSignal -> Nil) : Nil

An application-wide key binding, attached to the root window so it fires wherever the focus happens to be - the keyboard counterpart to a menu entry, and what actually makes a menu's shortcut: label true (that label draws the accelerator text and nothing more).

Takes the same spec as Handle#on_key: a friendly Symbol (:f2, :enter, :escape) or a raw Tk sequence ("<Control-s>").

Source
overlay(at : Symbol, & : self -> Nil) : Nil

Floats the single widget declared in the block on top of the enclosing ui.canvas, positioned at a fixed corner/edge/center anchor via Tk's place geometry manager - a "use sparingly" escape valve for the one legitimate absolute-position case (a status readout or button bar layered over canvas content), not a general-purpose layout mode. Stays correctly positioned across a canvas resize with nothing to redo by hand - place's relative coordinates are fractions of the canvas's current size, recomputed live by Tk on every resize. Only valid directly inside a ui.canvas block.

Source
overlay(at : Symbol) : Nil

Raises ArgumentError if this overlay's block builds anything other than exactly one widget.

Source
pane(name : Symbol | Nil = nil, weight : Int32 | Nil = nil, **opts, & : self -> Nil) : Handle

One region of a #split. weight: is how much of the leftover space this pane takes when the split is resized, relative to its siblings' weights; left unset, ttk::panedwindow's own default applies - a pane that keeps its size until the sash is dragged.

Only valid directly inside a ui.split block; raises ArgumentError anywhere else.

Source
pane(name : Symbol | Nil = nil, weight : Int32 | Nil = nil, **opts) : Handle
Source
panel(name : Symbol | Nil = nil, **opts, & : self -> Nil) : Handle

A plain frame. Stacks its children top to bottom exactly as #column does - the same gap:/pad:/align: apply, and a child's grow: takes the leftover height - so it needs no column inside for the common case; put a row/grid inside for any other shape.

Source
panel(name : Symbol | Nil = nil, **opts) : Handle

A plain frame. Stacks its children top to bottom exactly as #column does - the same gap:/pad:/align: apply, and a child's grow: takes the leftover height - so it needs no column inside for the common case; put a row/grid inside for any other shape.

Source
progress(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle

A progress bar: mode: (determinate/indeterminate), maximum: and value:, or bind: a Var to drive the position from code.

Source
radio(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle
Source
raw

The build-time escape hatch. A widget has no Tk path yet during build, so acting on it directly mid-build can't work - #raw defers the block instead, running it at realize with the live app in scope. It's a closure, so it can still reference sibling widgets by name even if they're declared later - by the time any raw block runs, the whole tree has already been realized once over (same forward-reference guarantee an event target: gets).

Source
row(name : Symbol | Nil = nil, **opts, & : self -> Nil) : Handle
Source
row(index : Int32, weight : Int32 | Nil = nil, min_size : Int32 | Nil = nil) : Nil

Ditto, for a grid row.

Source
row(name : Symbol | Nil = nil, **opts) : Handle
Source
scrollable(name : Symbol | Nil = nil, **opts, & : self -> Nil) : Handle

A scrolling region for ORDINARY widgets - a long column of them, a form taller than its window. Only needed for content Tk can't scroll on its own: a list/text_area/tree/table/canvas already attaches its own scrollbar with no wrapper at all, driven by its own scroll: - so wrapping one in a ui.scrollable would nest two scrolling regions, not improve the one.

y: (default true) and x: (default false) pick which scrollbars it gets, and each auto-hides while its content fits. Wheel scrolling works anywhere over the region, including over a nested child.

With x: false, content is held at the visible width rather than its natural one, so it never ends up narrower than the region.

Source
scrollable(name : Symbol | Nil = nil, **opts) : Handle

A scrolling region for ORDINARY widgets - a long column of them, a form taller than its window. Only needed for content Tk can't scroll on its own: a list/text_area/tree/table/canvas already attaches its own scrollbar with no wrapper at all, driven by its own scroll: - so wrapping one in a ui.scrollable would nest two scrolling regions, not improve the one.

y: (default true) and x: (default false) pick which scrollbars it gets, and each auto-hides while its content fits. Wheel scrolling works anywhere over the region, including over a nested child.

With x: false, content is held at the visible width rather than its natural one, so it never ends up narrower than the region.

Source
slider(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle
Source
spacer

A flexible gap - the named replacement for the "invisible spring row" trick (an empty row/column given all the leftover weight).

Source
split(name : Symbol | Nil = nil, orientation : SplitOrientation = :horizontal, **opts, & : self -> Nil) : Handle

A draggable split - two or more #pane regions with a sash between them to resize by. orientation: :horizontal puts the panes side by side, so the sash is vertical; :vertical stacks them.

Source
split(name : Symbol | Nil = nil, orientation : SplitOrientation = :horizontal, **opts) : Handle
Source
stretch(columns : Array(Int32) = [] of Int32, rows : Array(Int32) = [] of Int32, min_size : Int32 | Nil = nil) : Nil

Mark which columns/rows of the enclosing ui.grid absorb leftover space - the named replacement for grid columnconfigure -weight. min_size:, if given, applies the same minimum pixel width/height (-minsize) to every listed column AND row - the common "these N columns share a floor" case. For a floor that differs per column, or a weight that isn't the flat 1 every listed index gets here, use #column/#row instead. Only valid directly inside a grid's block.

Source
style(type : Symbol, name : String, *, hover = nil, pressed = nil, disabled = nil, focused = nil, **opts) : StyleRef

The named variant: configures "name.TWidgetClass" and returns a StyleRef to pass as a widget's own style: (ui.button(style: calc)), so nothing downstream ever has to know the ttk class or spell out the Prefix.TWidgetClass convention by hand. Same hover:/pressed:/ disabled:/focused: as the app-wide overload above.

Source
style(name : String, **opts) : Nil

Configure a ttk style, for the options ttk keeps on a style rather than on the widget - a ttk::button has no -font of its own, so a bigger label means a named style and style: on the widget. The raw ttk name overload - name is exactly what ttk::style configure takes, "Calc.TButton" lore and all. Stays working unchanged as the escape hatch; #style(type:, name:) below is the DSL-language spelling that doesn't require knowing ttk's own naming convention.

Deferred to realize like #raw, since it's a live-interpreter call.

Source
style(type : Symbol, *, hover = nil, pressed = nil, disabled = nil, focused = nil, **opts) : Nil

Restyle a DSL widget type app-wide, by its type: (:button, :label, ...) - see TtkStyleNames for the full vocabulary - instead of ttk's own Prefix.TWidgetClass spelling. Every widget of that type picks up the change immediately; there's no name involved, and nothing to pass to a widget's own style: - see the two-arg overload below for a named variant instead.

hover:/pressed:/disabled:/focused: are each a NamedTuple of the same options, translated into a ttk::style map state entry (ttk's own per-state color mechanism, the one feature every ttk app needs and nobody remembers the syntax for) - e.g. style(:button, background: "#a00", hover: {background: "#c00"}).

Source
tab(label : String, name : Symbol | Nil = nil, **opts, & : self -> Nil) : Handle

One page of a #tabs notebook. label is positional and required - it's the text on the tab itself, and a page with none is meaningless. Give a name too to address the page later, including as what #on_tab_changed reports when this one is selected.

Only valid directly inside a ui.tabs block; raises ArgumentError anywhere else.

Source
tab(label : String, name : Symbol | Nil = nil, **opts) : Handle
Source
table(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle

The same widget as #tree, for rows of fields rather than a hierarchy: name the fields with columns:. Scrolls itself unless scroll: false.

Source
tabs(name : Symbol | Nil = nil, **opts, & : self -> Nil) : Handle

A tabbed notebook. Declare its pages with #tab inside the block.

Source
tabs(name : Symbol | Nil = nil, **opts) : Handle

A tabbed notebook. Declare its pages with #tab inside the block.

Source
text_area(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle

A multi-line text widget. Scrolls itself unless scroll: false - see #scrollable, which is for the widgets that can't.

Source
text_box(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle
Source
tree(name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle

A hierarchical treeview, showing the tree column Tk displays by default. Scrolls itself unless scroll: false.

Source
var(initial : VarValue) : Var

Declare a reactive variable. Its Tcl variable name is allocated now (no interpreter needed - it's just a string); the variable itself only becomes real at realize (Session#realize runs Var#realize on every declared Var before the widget tree itself realizes). Bind it to a widget with bind:.

Owned by whichever container is currently open (the top of @stack) - Handle#destroy! releases every var a destroyed subtree owns this way, same as #image. See Node#vars.

Source
widget(type : Symbol, name : Symbol | Nil = nil, bind : Var | Nil = nil, **opts) : Handle

Declares a widget of any registered type, by type name. The way to use a type registered from outside this library - every type built in here has its own method above, which reads better and is worth preferring where one exists.

Takes the same name:/bind:/**opts as those methods, and a leaf or a container depending on what the type registered itself as; pass a block for a container's children.

WidgetTypes.register(WidgetType.new(type: :gauge, tk_command: "ttk::progressbar"))
ui.widget(:gauge, :cpu, maximum: 100)

A shard wanting ui.gauge(...) instead can define it: WidgetDSL is a module, so reopening it puts a method alongside the built-in ones, calling the same #widget.

Source
widget(type : Symbol, name : Symbol | Nil = nil, **opts, & : self -> Nil) : Handle

ditto, for a container with children.

Source
window(name : Symbol | Nil = nil, on_close : CloseHandler | Nil = nil, **opts, & : self -> Nil) : Handle

A separate toplevel window. Configure it with title:, geometry: ("WxH+X+Y", or just "+X+Y"), resizable: (one Bool for both axes, {width:, height:}, or [width, height]), min_size:/max_size: ({width, height} Tuples, wm minsize/maxsize), transient: false to make it independent of its parent rather than subordinate to it, and modal: true to have #show grab input when it opens.

on_close: runs when the window manager's close button is used; handle.on_close { } does the same thing after the fact. It is its own parameter rather than one of **opts for the same reason bind: is: a value that has to arrive with its type intact, not flattened into TclArgValue and recovered.

Created withdrawn, so a build can declare every window the app will ever need without them all appearing at realize - reveal one with handle.show, hide it again with handle.hide.

Source
window(name : Symbol | Nil = nil, on_close : CloseHandler | Nil = nil, **opts) : Handle
Source

Macros

container_widget(type)

ditto, for a container - both the with-block and without-block overloads every built-in container method below has.

module Tryst::UI::WidgetDSL
  container_widget panel_deck
end
ui.panel_deck(:cards) { |dsl| dsl.button(:ok, text: "OK") }
Source
leaf_widget(type)

Stamps a leaf ui.<type> method with the same name:/bind:/**opts shape every built-in leaf method below has - the macro version of what #widget's own doc comment describes doing by hand. See CUSTOM_WIDGETS.md at the repo root for the full guide; in short:

module Tryst::UI::WidgetDSL
  leaf_widget gauge
end
ui.gauge(:cpu, maximum: 100)

reads exactly like a built-in #progress/#label/etc call, once type: :gauge is registered (see WidgetType's own doc comment for how). Defined before every method below so they can use it too - Crystal requires a macro to be defined before its call site.

Source