//! The node arena: what the reconciler patches, and what `view` reads. //! //! Plain data with no libcosmic in it, so the edit half of the ABI is testable //! without a window, a GPU or a display. libvidya and libjolttui keep the same //! shape; what is different here is who reads it. There, the caller's thread //! paints the tree; here iced's thread does, so the arena sits behind a mutex //! and `view` works from a snapshot taken at the last commit. use std::collections::BTreeMap; use std::fmt::Write; #[derive(Clone, Debug, PartialEq)] pub enum Prop { Str(String), Num(f64), Bool(bool), } #[derive(Clone, Debug)] pub struct Node { pub tag: String, pub props: BTreeMap, pub children: Vec, pub parent: i32, } impl Node { pub fn str(&self, key: &str) -> &str { match self.props.get(key) { Some(Prop::Str(s)) => s, _ => "", } } pub fn num(&self, key: &str) -> Option { match self.props.get(key) { Some(Prop::Num(n)) => Some(*n), _ => None, } } pub fn bool(&self, key: &str) -> Option { match self.props.get(key) { Some(Prop::Bool(b)) => Some(*b), _ => None, } } /// The label a text-shaped widget shows: `label`, or `text` failing that. pub fn label(&self) -> &str { match self.str("label") { "" => self.str("text"), s => s, } } } /// `hbox` and `vbox` are one node here, as in libvidya; the tag only implies /// an orientation, and the jolt side writes it as a prop. fn canonical(tag: &str) -> &str { match tag { "hbox" | "vbox" => "box", "checkbox" => "checkbutton", other => other, } } #[derive(Clone, Debug, Default)] pub struct Tree { /// Slot `i` holds handle `i + 1`; `None` is a freed handle waiting for reuse. slots: Vec>, free: Vec, root: i32, } impl Tree { pub fn get(&self, id: i32) -> Option<&Node> { if id <= 0 { return None; } self.slots.get(id as usize - 1)?.as_ref() } fn get_mut(&mut self, id: i32) -> Option<&mut Node> { if id <= 0 { return None; } self.slots.get_mut(id as usize - 1)?.as_mut() } pub fn exists(&self, id: i32) -> bool { self.get(id).is_some() } pub fn root(&mut self) -> i32 { if !self.exists(self.root) { self.root = self.new_node("window"); } self.root } /// The root without creating it, for a reader holding a snapshot. pub fn root_id(&self) -> i32 { self.root } pub fn new_node(&mut self, tag: &str) -> i32 { let node = Node { tag: canonical(tag).to_owned(), props: BTreeMap::new(), children: Vec::new(), parent: 0, }; match self.free.pop() { Some(id) => { self.slots[id as usize - 1] = Some(node); id } None => { self.slots.push(Some(node)); self.slots.len() as i32 } } } /// Free `id` and everything under it, detaching it from its parent first. pub fn free(&mut self, id: i32) { let Some(node) = self.get(id) else { return }; let parent = node.parent; if let Some(p) = self.get_mut(parent) { p.children.retain(|c| *c != id); } self.free_subtree(id); } fn free_subtree(&mut self, id: i32) { let Some(node) = self.slots.get_mut(id as usize - 1).and_then(Option::take) else { return; }; for child in node.children { self.free_subtree(child); } if id == self.root { self.root = 0; } self.free.push(id); } pub fn set(&mut self, id: i32, key: &str, value: Prop) { if let Some(n) = self.get_mut(id) { n.props.insert(key.to_owned(), value); } } pub fn clear_props(&mut self, id: i32) { if let Some(n) = self.get_mut(id) { n.props.clear(); } } fn detach(&mut self, child: i32) { let parent = match self.get(child) { Some(n) => n.parent, None => return, }; if let Some(p) = self.get_mut(parent) { p.children.retain(|c| *c != child); } if let Some(c) = self.get_mut(child) { c.parent = 0; } } /// A node may not become its own ancestor; the reconciler never asks, but /// a cycle would make `view` recurse forever, so it is refused here. fn is_ancestor(&self, ancestor: i32, mut id: i32) -> bool { while id > 0 { if id == ancestor { return true; } id = self.get(id).map_or(0, |n| n.parent); } false } pub fn append(&mut self, parent: i32, child: i32) -> bool { if !self.exists(parent) || !self.exists(child) || self.is_ancestor(child, parent) { return false; } self.detach(child); self.get_mut(parent).unwrap().children.push(child); self.get_mut(child).unwrap().parent = parent; true } /// Unparents and frees `child`, under `parent` only. pub fn remove(&mut self, parent: i32, child: i32) { if self.get(child).is_some_and(|n| n.parent == parent) { self.free(child); } } /// Move `child` after `sibling`; `sibling` 0 is the first position. pub fn insert_after(&mut self, parent: i32, child: i32, sibling: i32) -> bool { if !self.exists(parent) || !self.exists(child) || self.is_ancestor(child, parent) { return false; } if sibling != 0 && self.get(sibling).is_none_or(|n| n.parent != parent) { return false; } self.detach(child); let p = self.get_mut(parent).unwrap(); let at = if sibling == 0 { 0 } else { p.children.iter().position(|c| *c == sibling).unwrap() + 1 }; p.children.insert(at, child); self.get_mut(child).unwrap().parent = parent; true } /// Put `new` where `old` was under `parent`, and free `old`. pub fn replace(&mut self, parent: i32, old: i32, new: i32) -> bool { if !self.exists(new) || self.get(old).is_none_or(|n| n.parent != parent) { return false; } if self.is_ancestor(new, parent) { return false; } self.detach(new); let p = self.get_mut(parent).unwrap(); let at = p.children.iter().position(|c| *c == old).unwrap(); p.children[at] = new; self.get_mut(new).unwrap().parent = parent; // `old` is no longer among the parent's children, so free only its subtree. self.get_mut(old).unwrap().parent = 0; self.free_subtree(old); true } /// The subtree at `id` as hiccup, one node to a line, props sorted — so two /// dumps of the same tree compare as text. pub fn dump(&self, id: i32) -> String { let mut out = String::new(); self.dump_into(id, 0, &mut out); out } fn dump_into(&self, id: i32, depth: usize, out: &mut String) { let Some(n) = self.get(id) else { return }; let _ = write!(out, "{}[:{} {{", " ".repeat(depth), n.tag); for (i, (k, v)) in n.props.iter().enumerate() { if i > 0 { out.push(' '); } let _ = match v { Prop::Str(s) => write!(out, ":{k} {s:?}"), Prop::Num(x) => write!(out, ":{k} {x}"), Prop::Bool(b) => write!(out, ":{k} {b}"), }; } out.push('}'); for child in &n.children { out.push('\n'); self.dump_into(*child, depth + 1, out); } out.push(']'); } } #[cfg(test)] mod tests { use super::*; #[test] fn a_removed_subtree_frees_every_node_under_it() { let mut t = Tree::default(); let root = t.root(); let card = t.new_node("card"); let label = t.new_node("label"); assert!(t.append(root, card)); assert!(t.append(card, label)); t.remove(root, card); assert!(!t.exists(card)); assert!(!t.exists(label)); assert!(t.get(root).unwrap().children.is_empty()); } #[test] fn insert_after_zero_moves_to_the_front() { let mut t = Tree::default(); let root = t.root(); let (a, b, c) = ( t.new_node("label"), t.new_node("label"), t.new_node("label"), ); for n in [a, b, c] { t.append(root, n); } assert!(t.insert_after(root, c, 0)); assert_eq!(t.get(root).unwrap().children, vec![c, a, b]); assert!(t.insert_after(root, c, b)); assert_eq!(t.get(root).unwrap().children, vec![a, b, c]); } #[test] fn replace_keeps_the_position_and_frees_the_old_node() { let mut t = Tree::default(); let root = t.root(); let (a, b, c) = ( t.new_node("label"), t.new_node("button"), t.new_node("label"), ); t.append(root, a); t.append(root, b); assert!(t.replace(root, a, c)); assert_eq!(t.get(root).unwrap().children, vec![c, b]); assert!(!t.exists(a)); } #[test] fn a_node_cannot_be_appended_under_itself() { let mut t = Tree::default(); let root = t.root(); let card = t.new_node("card"); t.append(root, card); assert!(!t.append(card, root)); } #[test] fn dump_reads_back_as_hiccup() { let mut t = Tree::default(); let root = t.root(); let row = t.new_node("hbox"); t.set(row, "orientation", Prop::Str("horizontal".into())); let b = t.new_node("button"); t.set(b, "label", Prop::Str("+ 1".into())); t.set(b, "kind", Prop::Str("primary".into())); t.append(root, row); t.append(row, b); assert_eq!( t.dump(root), "[:window {}\n [:box {:orientation \"horizontal\"}\n [:button {:kind \"primary\" :label \"+ 1\"}]]]" ); } }