Module: Plushie::Tree
- Defined in:
- lib/plushie/tree.rb,
lib/plushie/tree/diff.rb,
lib/plushie/tree/search.rb,
sig/plushie/tree.rbs,
sig/plushie/tree/diff.rbs,
sig/plushie/tree/search.rbs
Overview
Utilities for working with UI trees.
Provides normalization (Tree), search (Tree::Search), and diffing (Tree::Diff) for Node trees. Search and diff are also available directly on Tree via delegation.
Defined Under Namespace
Constant Summary collapse
- MAX_DEPTH =
Maximum tree depth before raising. Protects against infinite recursion from circular widget compositions.
256- DEPTH_WARNING =
Depth at which a warning is emitted (approaching MAX_DEPTH).
200- WIRE_READY_PROPS =
:__plushie_wire_ready_props- WIDGET_ROLE_DEFAULTS =
Post-normalize a11y pass
Mirrors the Rust SDK. After the main normalize pass produces a fully scoped tree:
- Auto-populate a11y.role from the widget type when unset. - Rewrite active_descendant and radio_group list refs through the same scope-prefix logic already applied to labelled_by, described_by, error_message. - Populate implicit a11y.radio_group when radios share a :group prop in the same enclosing scope. - Emit a11y_ref_unresolved warnings for dangling refs. - Emit missing_accessible_name warnings for interactive widgets carrying no accessible name source. { "button" => "button", "checkbox" => "check_box", "toggler" => "switch", "radio" => "radio_button", "text_input" => "text_input", "text_editor" => "multiline_text_input", "text" => "label", "rich_text" => "label", "slider" => "slider", "vertical_slider" => "slider", "pick_list" => "combo_box", "combo_box" => "combo_box", "progress_bar" => "progress_indicator", "image" => "image", "svg" => "image", "qr_code" => "image", "scrollable" => "scroll_view", "container" => "generic_container", "column" => "generic_container", "row" => "generic_container", "stack" => "generic_container", "grid" => "generic_container", "pane_grid" => "generic_container", "table" => "table", "canvas" => "canvas", "rule" => "separator" }.freeze
- A11Y_SINGLE_REF_KEYS =
%w[labelled_by described_by error_message active_descendant].freeze
- NAMED_INTERACTIVE_TYPES =
%w[button toggler checkbox pointer_area].freeze
- PLACEHOLDER_DESCRIPTION_WIDGETS =
%w[text_input text_editor combo_box pick_list].freeze
- VALIDATABLE_WIDGETS =
%w[text_input text_editor checkbox pick_list combo_box].freeze
- VALID_ID_PATTERN =
Printable ASCII range (0x21-0x7E), excludes space and control characters.
/\A[\x21-\x7e]+\z/
Class Method Summary collapse
-
.diff(old_tree, new_tree) ⇒ Array<Hash>
Diff two normalized trees, producing an array of patch operations.
- .exists?(tree, id) ⇒ Boolean
- .find(tree, id) ⇒ Node?
- .find_all(tree) {|arg0| ... } ⇒ Array[Node]
- .find_first(tree) {|arg0| ... } ⇒ Node?
- .ids(tree) ⇒ Array[String]
-
.node_to_wire(node) ⇒ Hash
Convert a Node to a plain wire-ready Hash (recursive).
-
.normalize(tree, registry: nil) ⇒ Array<Node>
Normalize a tree for wire transport.
-
.normalize_view(tree, registry: nil) ⇒ Node
Normalize a top-level app view and require explicit windows.
Class Method Details
.diff(old_tree, new_tree) ⇒ Array<Hash>
Diff two normalized trees, producing an array of patch operations.
Each op is a Hash with string keys matching the wire protocol:
`{ "op" => "replace_node", "path" => [...], "node" => {...} }`
`{ "op" => "update_props", "path" => [...], "props" => {...} }`
`{ "op" => "insert_child", "path" => [...], "index" => n, "node" => {...} }`
`{ "op" => "remove_child", "path" => [...], "index" => n }`
98 |
# File 'lib/plushie/tree.rb', line 98 def self.diff(old_tree, new_tree) = Diff.diff(old_tree, new_tree) |
.exists?(tree, id) ⇒ Boolean
22 23 |
# File 'lib/plushie/tree.rb', line 22 def self.exists?(tree, id) = Search.exists?(tree, id) # @see Tree::Search#ids |
.find(tree, id) ⇒ Node?
20 21 |
# File 'lib/plushie/tree.rb', line 20 def self.find(tree, id) = Search.find(tree, id) # @see Tree::Search#exists? |
.find_all(tree) {|arg0| ... } ⇒ Array[Node]
28 |
# File 'lib/plushie/tree.rb', line 28 def self.find_all(tree, &predicate) = Search.find_all(tree, &predicate) |
.find_first(tree) {|arg0| ... } ⇒ Node?
26 27 |
# File 'lib/plushie/tree.rb', line 26 def self.find_first(tree, &predicate) = Search.find_first(tree, &predicate) # @see Tree::Search#find_all |
.ids(tree) ⇒ Array[String]
24 25 |
# File 'lib/plushie/tree.rb', line 24 def self.ids(tree) = Search.ids(tree) # @see Tree::Search#find_first |
.node_to_wire(node) ⇒ Hash
Convert a Node to a plain wire-ready Hash (recursive).
104 105 106 107 108 109 110 111 |
# File 'lib/plushie/tree.rb', line 104 def self.node_to_wire(node) { "id" => node.id, "type" => node.type, "props" => wire_ready_props?(node) ? node.props : Encode.encode_props(node.props), "children" => node.children.map { |c| node_to_wire(c) } } end |
.normalize(tree, registry: nil) ⇒ Array<Node>
Normalize a tree for wire transport. Converts symbol prop values to strings via Encode, resolves scoped IDs, and validates tree structure.
When a canvas widget registry is provided, canvas widget placeholders are detected and rendered with stored state.
52 53 54 55 56 57 58 59 |
# File 'lib/plushie/tree.rb', line 52 def self.normalize(tree, registry: nil) return [Node.new(id: "root", type: "container")] if tree.nil? trees = (tree.is_a?(Array) ? tree : [tree]).compact normalized = trees.map { |node| normalize_node(node, "", registry, nil, 0) } check_duplicate_ids!(normalized) normalized.map { |node| post_normalize(node) } end |
.normalize_view(tree, registry: nil) ⇒ Node
Normalize a top-level app view and require explicit windows.
A nil tree is treated as "no UI": returns a root container
with no child windows so the renderer still has a structurally
valid snapshot to diff. Useful for transition, loading, or
error states where the app has nothing to display yet.
71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/plushie/tree.rb', line 71 def self.normalize_view(tree, registry: nil) return Node.new(id: "root", type: "root", children: []) if tree.nil? windows = normalize(tree, registry: registry) if windows.empty? || !windows.all? { |node| node.type == "window" } raise ArgumentError, "view must return a window node or an array of window nodes" end Node.new(id: "root", type: "root", children: windows) end |