rosace_widgets/template/
diff.rs1use std::collections::BTreeMap;
32
33use super::{PropValue, Template, TemplateNode};
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum TemplateDiff {
38 Unchanged,
40 Swappable,
43 Escalate(EscalationReason),
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum EscalationReason {
51 KeyMismatch,
53 HoleCountChanged { old: usize, new: usize },
55 HoleSlotRetargeted { index: usize },
58}
59
60pub fn diff(old: &Template, new: &Template) -> TemplateDiff {
62 if old.key != new.key {
63 return TemplateDiff::Escalate(EscalationReason::KeyMismatch);
64 }
65 if old.root == new.root {
66 return TemplateDiff::Unchanged;
67 }
68 if old.hole_count != new.hole_count {
69 return TemplateDiff::Escalate(EscalationReason::HoleCountChanged {
70 old: old.hole_count,
71 new: new.hole_count,
72 });
73 }
74
75 let old_slots = hole_slots(old);
78 let new_slots = hole_slots(new);
79 for (index, old_site) in &old_slots {
80 if new_slots.get(index) != Some(old_site) {
81 return TemplateDiff::Escalate(EscalationReason::HoleSlotRetargeted { index: *index });
82 }
83 }
84
85 TemplateDiff::Swappable
86}
87
88fn hole_slots(t: &Template) -> BTreeMap<usize, (String, String)> {
92 let mut slots = BTreeMap::new();
93 collect_slots(&t.root, &mut slots);
94 slots
95}
96
97fn collect_slots(node: &TemplateNode, slots: &mut BTreeMap<usize, (String, String)>) {
98 for (pos, value) in node.args.iter().enumerate() {
100 if let PropValue::Hole(i) = value {
101 slots.insert(*i, (node.widget.clone(), format!("$arg{pos}")));
102 }
103 }
104 for (prop, value) in &node.props {
105 if let PropValue::Hole(i) = value {
106 slots.insert(*i, (node.widget.clone(), prop.clone()));
107 }
108 }
109 for child in &node.children {
110 collect_slots(child, slots);
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::template::{StaticValue, TemplateKey, TemplateNode};
118
119 fn key() -> TemplateKey {
120 TemplateKey::new("src/app.rs", 10, 5)
121 }
122 fn t(root: TemplateNode) -> Template {
123 Template::new(key(), root)
124 }
125
126 #[test]
127 fn identical_templates_are_unchanged() {
128 let a = t(TemplateNode::new("Column").with_static("spacing", StaticValue::Float(8.0)));
129 let b = t(TemplateNode::new("Column").with_static("spacing", StaticValue::Float(8.0)));
130 assert_eq!(diff(&a, &b), TemplateDiff::Unchanged);
131 }
132
133 #[test]
134 fn changing_a_static_literal_is_swappable() {
135 let a = t(TemplateNode::new("Text").with_static("content", StaticValue::Str("Save".into())));
137 let b = t(TemplateNode::new("Text").with_static("content", StaticValue::Str("Store".into())));
138 assert_eq!(diff(&a, &b), TemplateDiff::Swappable);
139 }
140
141 #[test]
142 fn wrapping_in_a_static_container_preserves_hole_sites_and_is_swappable() {
143 let a = t(TemplateNode::new("Column").with_child(TemplateNode::new("Text").with_hole("content", 0)));
145 let b = t(TemplateNode::new("Column")
146 .with_child(TemplateNode::new("Container").with_child(TemplateNode::new("Text").with_hole("content", 0))));
147 assert_eq!(diff(&a, &b), TemplateDiff::Swappable);
148 }
149
150 #[test]
151 fn adding_a_hole_escalates_on_count() {
152 let a = t(TemplateNode::new("Column").with_hole("spacing", 0));
153 let b = t(TemplateNode::new("Column")
154 .with_hole("spacing", 0)
155 .with_child(TemplateNode::new("Text").with_hole("content", 1)));
156 assert_eq!(
157 diff(&a, &b),
158 TemplateDiff::Escalate(EscalationReason::HoleCountChanged { old: 1, new: 2 })
159 );
160 }
161
162 #[test]
163 fn removing_a_hole_escalates_on_count() {
164 let a = t(TemplateNode::new("Column").with_hole("spacing", 0).with_hole("cross", 1));
165 let b = t(TemplateNode::new("Column").with_hole("spacing", 0));
166 assert_eq!(
167 diff(&a, &b),
168 TemplateDiff::Escalate(EscalationReason::HoleCountChanged { old: 2, new: 1 })
169 );
170 }
171
172 #[test]
173 fn retargeting_a_hole_to_a_different_prop_escalates_even_at_same_count() {
174 let a = t(TemplateNode::new("Row")
177 .with_child(TemplateNode::new("Column").with_hole("spacing", 0))
178 .with_child(TemplateNode::new("Text").with_static("content", StaticValue::Str("x".into()))));
179 let b = t(TemplateNode::new("Row")
180 .with_child(TemplateNode::new("Column").with_static("spacing", StaticValue::Float(5.0)))
181 .with_child(TemplateNode::new("Text").with_hole("content", 0)));
182 assert_eq!(
183 diff(&a, &b),
184 TemplateDiff::Escalate(EscalationReason::HoleSlotRetargeted { index: 0 })
185 );
186 }
187
188 #[test]
189 fn same_count_same_sites_but_reordered_static_neighbours_is_swappable() {
190 let a = t(TemplateNode::new("Column").with_child(TemplateNode::new("Text").with_hole("content", 0)));
193 let b = t(TemplateNode::new("Column")
194 .with_child(TemplateNode::new("Text").with_static("content", StaticValue::Str("header".into())))
195 .with_child(TemplateNode::new("Text").with_hole("content", 0)));
196 assert_eq!(diff(&a, &b), TemplateDiff::Swappable);
197 }
198
199 #[test]
200 fn different_site_keys_are_a_caller_bug() {
201 let a = Template::new(TemplateKey::new("src/a.rs", 1, 1), TemplateNode::new("Column"));
202 let b = Template::new(TemplateKey::new("src/b.rs", 2, 2), TemplateNode::new("Column"));
203 assert_eq!(diff(&a, &b), TemplateDiff::Escalate(EscalationReason::KeyMismatch));
204 }
205}