Tryst::UI::Document
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
@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.
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.
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.
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.
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.
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.
@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).
@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.
@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).
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.
@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).
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.