class

Tryst::UI::Document

Inherits Reference < Object

The build-phase tree: an unattached root Node plus a name index ({Scope, Symbol} -> Node). Plain Crystal, no Tk - building and traversing a Document never touches an interpreter, which is what makes the DSL headless-testable.

Document only constructs and indexes nodes; it has no opinion on tree shape (which node is whose parent) - the build surface decides that by calling Node#add_child itself, so Document stays reusable underneath whatever parent-tracking scheme the builder uses.

Constructors

Instance methods

[](name : Symbol, scope : Scope = Scope::TOP_LEVEL) : Node | Nil
Source
claim_path_segment(parent_path : String, segment : String) : String

@api private - called by Realizer#allocate_path, which gets a fresh instance for every separate realize pass (the initial realize, each Session#add, each lazily-Handle#realize!d screen) - tracking claims here instead keeps them honest across every one of those passes for this Document's whole lifetime. Two mounts of the same component requesting the same key under the same real parent (e.g. a reusable row/screen, realized more than once - see WidgetDSL#component) get distinct, disambiguated segments; the common, non-colliding case keeps its plain segment unchanged.

Tracked as the SET of currently-claimed segments per parent, not a monotonic counter - #release_path_segment frees one back into this set on destroy, and picking the lowest still-unclaimed "#N" suffix (rather than always incrementing) is what makes that safe however far out of order claims are released, not just LIFO.

Source
create(type : Symbol, name : Symbol | Nil = nil, opts : Hash(Symbol, TclArgValue) = {} of Symbol => TclArgValue, scope : Scope = Scope::TOP_LEVEL) : Node

Construct a node and register it under its name (if any), scoped to scope - the same name used in two different scopes indexes as two distinct entries, so a component's local :save never collides with another component's (or the top level's) own :save. Does NOT attach it to any parent - the caller does that with Node#add_child, so Document never needs to know about a current-parent stack.

The node's own name/key stay bare/unqualified - only this index is scope-aware. A node's real Tk path is already distinct per scope with no help needed here, since it's built from the parent chain (Realizer#allocate_path), and two components' subtrees are never siblings of themselves.

Source
each_named_node

Every named node, regardless of whether it's actually attached anywhere in the tree - see Validator's orphan check, which is exactly the reason this differs from #each_node.

Source
each_node

Depth-first, pre-order traversal of the whole tree from #root.

Source
elsewhere_hint(name : Symbol, scope : Scope) : String

A one-line hint for a lookup of name in scope that missed: where the name is declared instead, if anywhere. Empty when it exists nowhere, so a message reads the same as before in that case.

Source
find(name : Symbol, scope : Scope = Scope::TOP_LEVEL) : Node | Nil

scope must be the same Scope instance the node was #create'd with

  • a name registered inside a scope is never found by a lookup in a different one, or vice versa.
Source
find_by_path(path : String) : Node | Nil

Reverse lookup: given a real Tk path (from an error message, a winfo query, or poking around in a REPL), find which node it belongs to - the counterpart to #find's name-based lookup. Only ever matches a node's own RealizedNode#path, never its arrange_path (the scrollbar-wrapper case - the wrapper frame itself has no owning node of its own to return) or a WidgetType's addressing strategy's synthesized virtual path (a menu entry has no real Tk path at all). Backed by #register_path/#unregister_path (see Node#realized=), so this is an index lookup, not a tree walk.

Source
named_nodes

Every named node as an Array of {name, node} pairs.

Source
node_destroyed(path : String) : Nil

@api private - the single Document-side entry point a live App's <Destroy> handler calls (via Session#realize wiring App#on_widget_destroyed) with the exact Tk path Tk just destroyed, for EVERY window it destroys - both an explicit ui[:x].destroy! and an implicit one (the window manager's own close button, an ancestor's destroy recursively taking a descendant with it). Tk destroys a whole subtree window-by-window, each with its own real <Destroy> firing, so this only ever has to release what ONE node owns - no separate subtree walk needed here the way Handle#perform_destroy! used to do it manually.

A no-op for any path that isn't one of this Document's own nodes (an unrelated widget somewhere else in the same App, or a wrapper frame/scrollbar Realizer created as plumbing - see #find_by_path's own arrange_path note).

Source
nodes

The whole tree as an Array, in the order #each_node yields it.

Source
notify(event : Symbol, *args : Node | String) : Nil

@api private - see #subscribe

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

@api private - the ONLY callers are Node#realized= (keeping the index in step with whatever a node's current real Tk path is) and #node_destroyed (removing an entry once its node is confirmed gone). Never call directly from outside Node.

Source
release_path_segment(node : Node) : Nil

@api private - the release half of #claim_path_segment. Frees the segment a now-dead node claimed back into its parent's pool, so a subtree destroyed and rebuilt under the same name gets the exact same Tk path back instead of the claim table growing by one entry every cycle forever (see #claim_path_segment's own comment on the 1,000-tab-opens failure mode this exists to close off).

Called from both destroy paths that ever remove a node - #node_destroyed (an implicit Tk-driven destroy) and Handle#unlink! (an explicit Handle#destroy!, which unregisters synchronously ahead of the <Destroy> event #node_destroyed would otherwise handle for the same node - see Node#realized= and #node_destroyed's own comment). A no-op for a node that never claimed a segment (structural/:root, or one that was never realized at all).

Source
root

The tree's root - starts with no children.

Source
scopes_declaring(name : Symbol) : Array(Scope)

Every scope with a node registered under name - for a "not found" message to point out that the name DOES exist, just somewhere the lookup deliberately can't see. Only ever consulted after an exact #find has already missed, never as a fallback lookup.

Source
subscribe(event : Symbol, &block : Array(Node | String) -> Nil) : Proc(Array(Node | String), Nil)

@api private

A minimal, always-on, generic build-event hook - Node#add_child notifies :append; the build stack's own push/pop (WidgetDSL#push_stack/WidgetDSL#pop_stack) notify :push/:pop. Document has no idea what (if anything) is listening, or why - it's a plain EventBus (see that file's own doc comment - it's internal plumbing, this is its only caller), scoped to build-time instrumentation. With nothing subscribed (the overwhelmingly common case), #notify costs one hash lookup into an empty list - not something a normal build needs to think about. See TreeInspector, the one built-in subscriber (Phase F).

Source
unregister(node : Node) : Nil

Removes node from the name index, scoped exactly like #register does - a no-op if node was never named (nothing to remove) or already unregistered. Called by Handle#destroy! for a destroyed node and every named descendant of its own subtree (Tk destroys descendants recursively, so their names need to stop resolving too), so a later widget can reuse the same name in the same scope, and #find correctly reports the name as gone in the meantime.

Source
unregister_path(path : String) : Nil

@api private - see #register_path.

Source