use crate::compat::HashMap;
use crate::widget::capability::CapabilityValue;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Host {
#[default]
Declared,
Overlay,
}
#[derive(Clone)]
pub struct Node {
pub widget: String,
pub key: Option<String>,
pub props: HashMap<String, CapabilityValue>,
pub host: Host,
pub on_mount: Option<crate::compat::Rc<dyn Fn(crate::core::ObjectId)>>,
pub on_unmount: Option<crate::compat::Rc<dyn Fn(crate::core::ObjectId)>>,
pub children: Vec<Node>,
}
impl PartialEq for Node {
fn eq(&self, other: &Self) -> bool {
self.widget == other.widget
&& self.key == other.key
&& self.props == other.props
&& self.children == other.children
}
}
impl core::fmt::Debug for Node {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Node")
.field("widget", &self.widget)
.field("key", &self.key)
.field("props", &self.props)
.field("host", &self.host)
.field("on_mount", &self.on_mount.is_some())
.field("on_unmount", &self.on_unmount.is_some())
.field("children", &self.children)
.finish()
}
}
impl Node {
pub fn new(widget: impl Into<String>) -> Self {
Self {
widget: widget.into(),
key: None,
props: HashMap::new(),
host: Host::default(),
on_mount: None,
on_unmount: None,
children: Vec::new(),
}
}
pub fn on_mount(mut self, f: impl Fn(crate::core::ObjectId) + 'static) -> Self {
self.on_mount = Some(crate::compat::Rc::new(f));
self
}
pub fn on_unmount(mut self, f: impl Fn(crate::core::ObjectId) + 'static) -> Self {
self.on_unmount = Some(crate::compat::Rc::new(f));
self
}
pub fn portal(mut self) -> Self {
self.host = Host::Overlay;
self
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn prop(mut self, name: impl Into<String>, value: CapabilityValue) -> Self {
self.props.insert(name.into(), value);
self
}
pub fn child(mut self, child: Node) -> Self {
self.children.push(child);
self
}
pub fn children_of(mut self, children: impl IntoIterator<Item = Node>) -> Self {
self.children.extend(children);
self
}
pub fn child_if(mut self, condition: bool, child: Node) -> Self {
if condition {
self.children.push(child);
}
self
}
pub fn child_if_else(self, condition: bool, then_child: Node, else_child: Node) -> Self {
if condition {
self.child(then_child)
} else {
self.child(else_child)
}
}
pub fn children_keyed<T>(
mut self,
items: &[T],
key_of: impl Fn(&T) -> String,
make: impl Fn(&T) -> Node,
) -> Self {
for item in items {
self.children.push(make(item).key(key_of(item)));
}
self
}
pub fn prop_value(&self, name: &str) -> Option<&CapabilityValue> {
self.props.get(name)
}
pub fn key_str(&self) -> Option<&str> {
self.key.as_deref()
}
pub fn node_count(&self) -> usize {
let mut count = 0usize;
let mut stack = vec![self];
while let Some(node) = stack.pop() {
count += 1;
stack.extend(node.children.iter());
}
count
}
pub fn duplicate_sibling_keys(&self) -> Vec<(&str, usize)> {
let mut counts: HashMap<&str, usize> = HashMap::new();
for child in &self.children {
if let Some(k) = child.key_str() {
*counts.entry(k).or_insert(0) += 1;
}
}
let mut dups: Vec<(&str, usize)> = counts.into_iter().filter(|(_, n)| *n > 1).collect();
dups.sort_by(|a, b| a.0.cmp(b.0));
dups
}
pub fn walk(&self) -> Vec<(&Node, usize)> {
let mut out = Vec::new();
let mut stack = vec![(self, 0usize)];
while let Some((node, depth)) = stack.pop() {
out.push((node, depth));
for child in node.children.iter().rev() {
stack.push((child, depth + 1));
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &str) -> CapabilityValue {
CapabilityValue::String(v.to_string())
}
#[test]
fn new_node_is_a_keyless_leaf() {
let n = Node::new("label");
assert_eq!(n.widget, "label");
assert_eq!(n.key, None);
assert!(n.props.is_empty());
assert!(n.children.is_empty());
assert_eq!(n.node_count(), 1);
}
#[test]
fn builder_chain_sets_key_and_props() {
let n = Node::new("label").key("title").prop("text", s("Hi"));
assert_eq!(n.key_str(), Some("title"));
assert_eq!(n.prop_value("text"), Some(&s("Hi")));
}
#[test]
fn setting_a_property_twice_keeps_the_last_write() {
let n = Node::new("label").prop("text", s("first")).prop("text", s("second"));
assert_eq!(n.prop_value("text"), Some(&s("second")));
}
#[test]
fn children_keep_insertion_order() {
let n = Node::new("vbox").child(Node::new("a")).child(Node::new("b")).child(Node::new("c"));
let names: Vec<&str> = n.children.iter().map(|c| c.widget.as_str()).collect();
assert_eq!(names, ["a", "b", "c"]);
}
#[test]
fn children_of_appends_rather_than_replaces() {
let n =
Node::new("vbox").child(Node::new("a")).children_of([Node::new("b"), Node::new("c")]);
assert_eq!(n.children.len(), 3);
assert_eq!(n.children[2].widget, "c");
}
#[test]
fn node_count_covers_the_whole_subtree() {
let n = Node::new("vbox")
.child(Node::new("a").child(Node::new("a1")).child(Node::new("a2")))
.child(Node::new("b"));
assert_eq!(n.node_count(), 5);
}
#[test]
fn walk_is_depth_first_and_reports_depth() {
let n =
Node::new("vbox").child(Node::new("a").child(Node::new("a1"))).child(Node::new("b"));
let seen: Vec<(&str, usize)> =
n.walk().into_iter().map(|(node, d)| (node.widget.as_str(), d)).collect();
assert_eq!(seen, [("vbox", 0), ("a", 1), ("a1", 2), ("b", 1)]);
}
#[test]
fn child_if_includes_or_omits_the_node() {
let shown = Node::new("row").child_if(true, Node::new("badge").key("b"));
assert_eq!(shown.children.len(), 1);
assert_eq!(shown.children[0].key_str(), Some("b"));
let hidden = Node::new("row").child_if(false, Node::new("badge").key("b"));
assert!(hidden.children.is_empty(), "the absent branch must contribute no node");
}
#[test]
fn child_if_else_picks_exactly_one_branch() {
let loading =
Node::new("body").child_if_else(true, Node::new("spinner"), Node::new("content"));
assert_eq!(loading.children.len(), 1);
assert_eq!(loading.children[0].widget, "spinner");
let ready =
Node::new("body").child_if_else(false, Node::new("spinner"), Node::new("content"));
assert_eq!(ready.children.len(), 1);
assert_eq!(ready.children[0].widget, "content");
}
#[test]
fn a_conditional_group_is_expressed_with_an_if() {
let hidden = Node::new("section");
let hidden = if false { hidden.children_of([Node::new("a")]) } else { hidden };
assert!(hidden.children.is_empty(), "a false condition adds no children");
let shown = Node::new("section");
let shown = if true { shown.children_of([Node::new("a"), Node::new("b")]) } else { shown };
assert_eq!(shown.children.len(), 2);
}
#[test]
fn children_keyed_assigns_a_stable_key_per_item() {
let names = ["alpha", "beta", "gamma"];
let list = Node::new("list").children_keyed(
&names,
|n| (*n).to_string(),
|n| Node::new("label").prop("text", s(n)),
);
assert_eq!(list.children.len(), 3);
let keys: Vec<&str> = list.children.iter().filter_map(|c| c.key_str()).collect();
assert_eq!(keys, ["alpha", "beta", "gamma"]);
assert!(
list.duplicate_sibling_keys().is_empty(),
"distinct items must not collide, or the diff would refuse to move either"
);
}
#[test]
fn children_keyed_on_an_empty_slice_adds_nothing() {
let empty: [u8; 0] = [];
let list =
Node::new("list").children_keyed(&empty, |n| n.to_string(), |_| Node::new("label"));
assert!(list.children.is_empty());
}
#[test]
fn duplicate_sibling_keys_are_reported() {
let n = Node::new("vbox")
.child(Node::new("a").key("dup"))
.child(Node::new("b").key("dup"))
.child(Node::new("c").key("unique"));
assert_eq!(n.duplicate_sibling_keys(), [("dup", 2)]);
}
#[test]
fn duplicate_report_ignores_keyless_siblings() {
let n = Node::new("vbox").child(Node::new("a")).child(Node::new("b"));
assert!(n.duplicate_sibling_keys().is_empty());
}
#[test]
fn duplicate_report_covers_only_direct_children() {
let n = Node::new("vbox")
.child(Node::new("list").key("left").child(Node::new("row").key("total")))
.child(Node::new("list").key("right").child(Node::new("row").key("total")));
assert!(n.duplicate_sibling_keys().is_empty());
}
#[test]
fn duplicate_report_is_not_confused_by_grandchildren() {
let n = Node::new("vbox")
.child(Node::new("a").key("k").child(Node::new("a1").key("k")))
.child(Node::new("b").key("k"));
assert_eq!(n.duplicate_sibling_keys(), [("k", 2)]);
}
#[test]
fn props_are_compared_by_exact_value() {
let a = Node::new("x").prop("v", CapabilityValue::Float(1.0));
let b = Node::new("x").prop("v", CapabilityValue::Float(1.000_000_1));
assert_ne!(a.props, b.props);
}
#[test]
fn a_node_is_a_value_that_can_be_cloned_and_compared() {
let a = Node::new("label").key("k").prop("text", s("t"));
let b = a.clone();
assert_eq!(a, b);
}
}