1#[derive(Clone, Debug, PartialEq)]
18pub enum StaticValue {
19 Bool(bool),
20 Int(i64),
23 Float(f64),
24 Str(String),
25}
26
27#[derive(Clone, Debug, PartialEq)]
37pub enum PropValue {
38 Static(StaticValue),
39 Hole(usize),
40}
41
42#[derive(Clone, Debug, PartialEq)]
48pub struct TemplateNode {
49 pub widget: String,
51 pub args: Vec<PropValue>,
55 pub props: Vec<(String, PropValue)>,
57 pub children: Vec<TemplateNode>,
59}
60
61impl TemplateNode {
62 pub fn new(widget: impl Into<String>) -> Self {
64 Self { widget: widget.into(), args: Vec::new(), props: Vec::new(), children: Vec::new() }
65 }
66
67 pub fn with_arg_static(mut self, value: StaticValue) -> Self {
69 self.args.push(PropValue::Static(value));
70 self
71 }
72
73 pub fn with_arg_hole(mut self, index: usize) -> Self {
75 self.args.push(PropValue::Hole(index));
76 self
77 }
78
79 pub fn with_static(mut self, key: impl Into<String>, value: StaticValue) -> Self {
81 self.props.push((key.into(), PropValue::Static(value)));
82 self
83 }
84
85 pub fn with_hole(mut self, key: impl Into<String>, index: usize) -> Self {
87 self.props.push((key.into(), PropValue::Hole(index)));
88 self
89 }
90
91 pub fn with_child(mut self, child: TemplateNode) -> Self {
93 self.children.push(child);
94 self
95 }
96
97 fn hole_extent(&self) -> usize {
101 let hole_idx = |v: &PropValue| match v {
102 PropValue::Hole(i) => Some(i + 1),
103 PropValue::Static(_) => None,
104 };
105 let in_args = self.args.iter().filter_map(hole_idx).max().unwrap_or(0);
106 let in_props = self.props.iter().filter_map(|(_, v)| hole_idx(v)).max().unwrap_or(0);
107 let below = self.children.iter().map(TemplateNode::hole_extent).max().unwrap_or(0);
108 in_args.max(in_props).max(below)
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq, Hash)]
116pub struct TemplateKey {
117 pub file: String,
119 pub line: u32,
121 pub col: u32,
123}
124
125impl TemplateKey {
126 pub fn new(file: impl Into<String>, line: u32, col: u32) -> Self {
127 Self { file: file.into(), line, col }
128 }
129}
130
131impl std::fmt::Display for TemplateKey {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 write!(f, "{}:{}:{}", self.file, self.line, self.col)
134 }
135}
136
137#[derive(Clone, Debug, PartialEq)]
145pub struct Template {
146 pub key: TemplateKey,
147 pub root: TemplateNode,
148 pub hole_count: usize,
149}
150
151impl Template {
152 pub fn new(key: TemplateKey, root: TemplateNode) -> Self {
155 let hole_count = root.hole_extent();
156 Self { key, root, hole_count }
157 }
158
159 pub fn hole_signature_matches(&self, other: &Template) -> bool {
164 self.hole_count == other.hole_count
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn key() -> TemplateKey {
173 TemplateKey::new("src/app.rs", 12, 5)
174 }
175
176 #[test]
177 fn leaf_node_has_no_props_or_children() {
178 let n = TemplateNode::new("Text");
179 assert_eq!(n.widget, "Text");
180 assert!(n.props.is_empty());
181 assert!(n.children.is_empty());
182 }
183
184 #[test]
185 fn builder_records_statics_holes_and_children_in_order() {
186 let n = TemplateNode::new("Button")
187 .with_static("label", StaticValue::Str("Save".into()))
188 .with_hole("on_press", 0)
189 .with_child(TemplateNode::new("Icon"));
190 assert_eq!(n.props.len(), 2);
191 assert_eq!(n.props[0], ("label".to_string(), PropValue::Static(StaticValue::Str("Save".into()))));
192 assert_eq!(n.props[1], ("on_press".to_string(), PropValue::Hole(0)));
193 assert_eq!(n.children.len(), 1);
194 assert_eq!(n.children[0].widget, "Icon");
195 }
196
197 #[test]
198 fn hole_count_is_zero_for_a_fully_static_tree() {
199 let root = TemplateNode::new("Column")
200 .with_static("spacing", StaticValue::Int(12))
201 .with_child(TemplateNode::new("Text").with_static("content", StaticValue::Str("Hi".into())));
202 let t = Template::new(key(), root);
203 assert_eq!(t.hole_count, 0);
204 }
205
206 #[test]
207 fn hole_count_is_max_index_plus_one_across_the_whole_tree() {
208 let root = TemplateNode::new("Column")
210 .with_child(TemplateNode::new("Button").with_hole("on_press", 0))
211 .with_child(TemplateNode::new("Button").with_hole("on_press", 1));
212 let t = Template::new(key(), root);
213 assert_eq!(t.hole_count, 2);
214 }
215
216 #[test]
217 fn hole_count_uses_the_highest_index_even_when_sparse() {
218 let root = TemplateNode::new("Text").with_hole("content", 3);
221 let t = Template::new(key(), root);
222 assert_eq!(t.hole_count, 4);
223 }
224
225 #[test]
226 fn signature_matches_only_when_hole_counts_are_equal() {
227 let a = Template::new(
229 key(),
230 TemplateNode::new("Text").with_static("content", StaticValue::Str("A".into())),
231 );
232 let b = Template::new(
233 key(),
234 TemplateNode::new("Text").with_static("content", StaticValue::Str("B".into())),
235 );
236 assert!(a.hole_signature_matches(&b));
237
238 let c = Template::new(key(), TemplateNode::new("Text").with_hole("content", 0));
240 assert!(!a.hole_signature_matches(&c));
241 }
242
243 #[test]
244 fn key_displays_as_file_line_col() {
245 assert_eq!(TemplateKey::new("src/app.rs", 12, 5).to_string(), "src/app.rs:12:5");
246 }
247
248 #[test]
249 fn key_equality_and_hashing_identify_a_view_site() {
250 use std::collections::HashSet;
251 let mut seen = HashSet::new();
252 seen.insert(TemplateKey::new("src/app.rs", 12, 5));
253 assert!(seen.contains(&TemplateKey::new("src/app.rs", 12, 5)));
254 assert!(!seen.contains(&TemplateKey::new("src/app.rs", 12, 6)));
255 }
256}