plushie/tree
Tree operations: normalization, diffing, and search.
After view(model) produces a Node tree, normalize applies scoped
IDs and resolves a11y references. After each update cycle, diff
compares old and new normalized trees to produce PatchOp lists for
the wire protocol.
Types
Cache of memo-ized subtrees from the previous render cycle. Maps scoped memo ID to the dependency value, normalized subtree, and the widget registry entries and window IDs accumulated while normalizing that subtree. On cache hit (same dependency), the subtree and its accumulated data are restored without re-normalizing.
pub type MemoCache =
dict.Dict(String, MemoCacheEntry)
A single entry in the memo cache.
pub type MemoCacheEntry {
MemoCacheEntry(
dep: dynamic.Dynamic,
node: node.Node,
registry: dict.Dict(String, widget.RegistryEntry),
windows: set.Set(String),
)
}
Constructors
-
MemoCacheEntry( dep: dynamic.Dynamic, node: node.Node, registry: dict.Dict(String, widget.RegistryEntry), windows: set.Set(String), )
Result of a full view normalization including accumulated state.
pub type NormalizeResult {
NormalizeResult(
tree: node.Node,
memo_cache: dict.Dict(String, MemoCacheEntry),
registry: dict.Dict(String, widget.RegistryEntry),
windows: set.Set(String),
)
}
Constructors
-
NormalizeResult( tree: node.Node, memo_cache: dict.Dict(String, MemoCacheEntry), registry: dict.Dict(String, widget.RegistryEntry), windows: set.Set(String), )
Values
pub fn diff(
old: node.Node,
new: node.Node,
) -> List(patch.PatchOp)
Compare two normalized trees and return a list of patch operations
that transform old into new.
Paths are lists of integer child indices from the root. For example,
[0, 2] means “root’s first child, then that node’s third child”.
The algorithm uses O(n) set comparison for reorder detection rather than O(n^2) LCS. When children are reordered, the entire node is replaced rather than computing minimal moves, a deliberate simplicity-over-optimality tradeoff matching the reference implementation.
pub fn empty_memo_cache() -> dict.Dict(String, MemoCacheEntry)
Create an empty memo cache for the first render cycle.
pub fn exists(tree: node.Node, id: String) -> Bool
Check whether a node with the given ID exists anywhere in the tree.
pub fn find(
tree: node.Node,
id: String,
) -> option.Option(node.Node)
Find a node by its ID (depth-first). Returns the first match.
Matches against the full ID first. If no match is found and the target does not contain “/”, falls back to matching the local segment (the part after the last “/”) of each node’s ID.
pub fn find_all(
tree: node.Node,
predicate: fn(node.Node) -> Bool,
) -> List(node.Node)
Collect all nodes matching a predicate (depth-first order).
pub fn normalize(node: node.Node) -> node.Node
Normalize a node tree by applying scoped IDs and resolving a11y
references. Call this on the output of view(model) before diffing.
Scoping rules:
- Window nodes keep bare IDs. Their children get
window#child_id. - Named (non-empty-ID) non-window nodes create scope boundaries.
Deeper descendants get
window#parent/child(the#separator only appears at the window boundary). - Nodes with an empty
idare assigned an auto-ID of the formauto:<kind>:<index>and are transparent to scoping (like other auto-IDs).
pub fn normalize_view(
node: node.Node,
registry: dict.Dict(String, widget.RegistryEntry),
prev_memo_cache: dict.Dict(String, MemoCacheEntry),
) -> Result(NormalizeResult, String)
Normalize a top-level app view and enforce explicit windows.
Accepts the previous render cycle’s memo cache and returns the normalized tree, new memo cache, accumulated widget registry, and detected window IDs. The registry and windows are collected during normalization itself, eliminating separate tree walks.
The runtime wraps the list-of-windows returned by view into a
single tree root before calling this function, so the root here is
either a window node, a synthetic container holding only
window children, or an empty container representing [].
pub fn normalize_with_memo(
node: node.Node,
registry: dict.Dict(String, widget.RegistryEntry),
prev_memo_cache: dict.Dict(String, MemoCacheEntry),
) -> NormalizeResult
Normalize with a widget registry and memo cache. Returns the normalized tree, new memo cache, accumulated widget registry, and detected window IDs.
pub fn normalize_with_registry(
node: node.Node,
registry: dict.Dict(String, widget.RegistryEntry),
) -> node.Node
Normalize with a widget registry. Widget placeholders in the tree are rendered using stored state from the registry.
pub fn view_list_to_tree(windows: List(node.Node)) -> node.Node
Collapse a list of top-level window nodes into a single tree root.
[] becomes an empty container (loading, transition, error screens).
[single] promotes the single window to the root directly. Multiple
entries wrap under a synthetic root container so the diff and
normalize pipeline can treat the tree uniformly. Mirrors the
Elixir runtime’s internal wrapping at plushie-elixir/lib/plushie/tree.ex.