class

Tryst::UI::Realizer

Inherits Reference < Object

Walks a Document and realizes it into a live Tryst::App - two passes:

  1. create - creates every widget, allocates a hierarchical/ meaningful Tk path per node, fills each node's realized slot.
  2. link - applies layout (plain top-to-bottom pack, for now - see this class's own doc comment for what's deferred) and wires event bindings, resolving target: references by name. Runs after create has finished the WHOLE tree, so a target declared later in the build already has a live path by the time it's looked up - that ordering is what makes forward references work.

Every widget creation and mutation goes through Tryst::App#command, so tryst's interceptor/leak-cleanup layer applies automatically.

@app is AppContract (see app_contract.cr), not the concrete Tryst::App - dispatched dynamically, so a Realizer built against FakeApp (spec/support/fake_app.cr) runs the exact same code with no real Tk interpreter involved, which is what makes this class headless-testable at all.

The generic create/link passes, plain top-to-bottom pack, and the auto-scrollbar a natively_scrollable type gets for free (#create_native_scrollable) are what this class owns directly. Everything type-specific - column/row flow layout, grid layout, ui.scrollable's own embedded-viewport case, menu_bar/context_menu's bespoke traversal, :window/:pane/:tab's own post_create setup - lives on the owning type's own WidgetType subclass instead (see widget_type.cr's own doc comment), reaching back into this class only through the small public service surface those bodies actually need: #app, #pack_plain, #create_children, #allocate_path, #filtered_opts, #wire_scrollbars, #add_bindtag, #arrange_flow.

Constants

RESERVED_OPTIONS = Set {:title, :geometry, :resizable, :transient, :modal, :x, :y, :scroll, :tab_label, :pane_weight}

Opts keys that are read on this side rather than handed to Tk, so they never reach a widget-creation call as a bogus -option. Every entry is one a type's own realize step consumes out of node.opts: :window's wm setup (title/geometry/resizable/transient/modal), :scroll (#resolve_scroll) and :x/:y (#scroll_axis?, which scrollbars a scrolling widget gets), :tab_label (a page's label), and :pane_weight (how much of a split's leftover space one pane takes).

Which types may legitimately carry each of these is enforced at the declaration - see WidgetDSL#validate_reserved_opts!. Adding a key here means teaching that check about it too, or it becomes an option that silently does nothing on every type but one.

An intent with a typed Node slot is not listed here - it never entered opts to begin with. See WidgetDSL#extract_dsl_opts. A Set, not a list: #filtered_opts asks about every option on every node.

Constructors

new(app : AppContract, document : Document, default_scroll : Bool | Nil = nil)
Source

Instance methods

add_bindtag(path : String, tag : String) : Nil

Appends tag to path's bindtags, keeping the ones Tk already gave it (its own path, its widget class, its toplevel, "all") - a bare bindtags <path> <tag> would REPLACE them, silently costing the widget every class binding that makes it behave like itself. Shared by #adopt_content_bindtag above (generic infra) and ScrollableType's own wheel-binding setup (widget_types/ scrollable.cr) - public for the latter to reach from outside this class.

Source
allocate_path(node : Node, parent_path : String) : String

Claims this node's own hierarchical/meaningful Tk path segment under parent_path and records it, so a later rebuild of the same subtree gets the same segment back (Handle#destroy!'s own unlink!). Public - MenuHostType's own relocated create/link replacement (widget_types/menu_host_type.cr) needs this from outside this class; #create still uses it directly.

Source
app

The live app this realizer drives Tk through - the one thing every relocated WidgetType hook body needs and none of their signatures pass directly (only #post_create's does). Public so a subclass's #arrange/#custom_children/#custom_create/#post_create override can reach it via the realizer parameter each is handed.

Source
arrange_flow(node : Node, children : Array(Node), flow : FlowConfig) : Nil

Runs a column/row's flow-pack layout - gap:/align:/pad: driven, with each child's own grow: absorbing leftover space along the container's main axis. Public (not private, unlike ruby's own version, which reaches it via realizer.send(:arrange_flow, ...) from a WidgetType's computed arrange: hook - Crystal has no private-bypass equivalent to send) - generic layout STRATEGY any type can opt into via the flow: constructor field (WidgetType# arrange's own default body dispatches here automatically), not any one type's own bespoke logic, so it stays here rather than moving into column.cr/row.cr specifically.

Source
create_children(node : Node, path : String) : Nil

The generic "create every child under this node's own path" step

  • also WidgetType#custom_children's own default body, so a type with nothing custom to do here gets this for free. Public for the same reason #pack_plain is: a WidgetType subclass whose override only wants to run this unconditionally (rather than override #custom_children at all) can call it directly.
Source
filtered_opts(node : Node) : Hash(String, TclArgValue)

node.opts, keyed by String (App#command's Hash overload) with every RESERVED_OPTIONS key stripped - none of those are real Tk options. Public - MenuHostType's own relocated create/link replacement (widget_types/menu_host_type.cr) needs this from outside this class; #create_native_scrollable and #create still use it directly since they're both defined here.

Source
pack_plain(children : Array(Node)) : Nil

Plain top-to-bottom pack, with none of a flow container's own gap:/align:/pad: options - the fallback every unregistered node (:root) gets, and also WidgetType#arrange's own default body for a type with no #flow and nothing else overridden. Public for the same reason #create_children is.

grow: still means what it does in a flow: the child takes all the leftover space. That's what a window's single body column wants (a toplevel is plain-packed, having no flow of its own), and with nothing honoring it here no DSL-built window could ever resize its content.

Source
realize
Source
realize_subtree(node : Node, parent_node : Node) : Nil

Realize a single already-built (but not-yet-realized) node - and its descendants - into an already-running app, scoped under a parent that's realized already. Reuses the exact same create/link machinery #realize uses for the initial tree, just entered at an arbitrary node instead of the document root - for adding widgets to an already-running app (Session#add) or realizing an on-demand lazy: true subtree.

Source
wire_scrollbars(path : String, target_path : String, x : Bool, y : Bool) : Nil

Builds a vertical and/or horizontal ttk::scrollbar around target_path, gridded into path (the wrapper frame), and wires each to auto-hide when its content fits. Shared by #create_native_scrollable above (generic infra) and ScrollableType#custom_children (widget_types/scrollable.cr, the arbitrary-content case) - public for the latter to reach from outside this class.

Source