1use crate::id::NodeId;
7use crate::model::{NodeKind, Paint, SceneGraph, SceneNode, Style};
8use std::collections::HashMap;
9
10pub fn dedup_use_styles(graph: &mut SceneGraph) {
16 let indices: Vec<_> = graph.graph.node_indices().collect();
17 for idx in indices {
18 let node = &mut graph.graph[idx];
19 dedup_use_on_node(node);
20 }
21 for edge in &mut graph.edges {
22 let mut seen = std::collections::HashSet::new();
23 edge.use_styles.retain(|id| seen.insert(*id));
24 }
25}
26
27fn dedup_use_on_node(node: &mut SceneNode) {
28 let mut seen = std::collections::HashSet::new();
29 node.use_styles.retain(|id| seen.insert(*id));
30}
31
32pub fn hoist_styles(graph: &mut SceneGraph) {
43 let mut fp_map: HashMap<String, (Vec<petgraph::graph::NodeIndex>, Style)> = HashMap::new();
45
46 for idx in graph.graph.node_indices() {
47 let node = &graph.graph[idx];
48 if is_style_empty(&node.style) {
49 continue;
50 }
51 let fp = style_fingerprint(&node.style);
52 let entry = fp_map
53 .entry(fp)
54 .or_insert_with(|| (Vec::new(), node.style.clone()));
55 entry.0.push(idx);
56 }
57
58 let mut counter = 0u32;
59 for (indices, prototype_style) in fp_map.values() {
60 if indices.len() < 2 {
61 continue;
62 }
63
64 counter += 1;
65 let style_name = NodeId::intern(&format!("_auto_{counter}"));
66 graph.styles.insert(style_name, prototype_style.clone());
67
68 for &idx in indices {
69 let node = &mut graph.graph[idx];
70 node.style = Style::default();
71 if !node.use_styles.contains(&style_name) {
72 node.use_styles.insert(0, style_name);
73 }
74 }
75 }
76}
77
78fn style_fingerprint(style: &Style) -> String {
82 let mut parts = Vec::new();
83
84 if let Some(ref fill) = style.fill {
85 parts.push(format!("fill={}", paint_key(fill)));
86 }
87 if let Some(ref stroke) = style.stroke {
88 parts.push(format!(
89 "stroke={},{}",
90 paint_key(&stroke.paint),
91 stroke.width
92 ));
93 }
94 if let Some(ref font) = style.font {
95 parts.push(format!(
96 "font={},{},{}",
97 font.family, font.weight, font.size
98 ));
99 }
100 if let Some(r) = style.corner_radius {
101 parts.push(format!("corner={r}"));
102 }
103 if let Some(o) = style.opacity {
104 parts.push(format!("opacity={o}"));
105 }
106 if let Some(ref sh) = style.shadow {
107 parts.push(format!(
108 "shadow={},{},{},{}",
109 sh.offset_x,
110 sh.offset_y,
111 sh.blur,
112 sh.color.to_hex()
113 ));
114 }
115
116 parts.join("|")
117}
118
119fn paint_key(paint: &Paint) -> String {
120 match paint {
121 Paint::Solid(c) => c.to_hex(),
122 Paint::LinearGradient { angle, stops } => {
123 let stops_str: String = stops
124 .iter()
125 .map(|s| format!("{}/{}", s.color.to_hex(), s.offset))
126 .collect::<Vec<_>>()
127 .join(",");
128 format!("linear({angle}deg,{stops_str})")
129 }
130 Paint::RadialGradient { stops } => {
131 let stops_str: String = stops
132 .iter()
133 .map(|s| format!("{}/{}", s.color.to_hex(), s.offset))
134 .collect::<Vec<_>>()
135 .join(",");
136 format!("radial({stops_str})")
137 }
138 }
139}
140
141fn is_style_empty(style: &Style) -> bool {
142 style.fill.is_none()
143 && style.stroke.is_none()
144 && style.font.is_none()
145 && style.corner_radius.is_none()
146 && style.opacity.is_none()
147 && style.shadow.is_none()
148}
149
150fn kind_priority(kind: &NodeKind) -> u8 {
155 match kind {
156 NodeKind::Root => 0,
157 NodeKind::Group { .. } | NodeKind::Frame { .. } => 1,
158 NodeKind::Rect { .. } => 2,
159 NodeKind::Ellipse { .. } => 3,
160 NodeKind::Text { .. } => 4,
161 NodeKind::Path { .. } => 5,
162 NodeKind::Generic => 6,
163 }
164}
165
166pub fn sort_nodes(graph: &mut SceneGraph) {
175 let root = graph.root;
176 let mut children = graph.children(root);
177
178 if children.len() < 2 {
179 return;
180 }
181
182 children.sort_by_key(|&idx| kind_priority(&graph.graph[idx].kind));
184
185 graph.sorted_child_order.insert(root, children);
187}
188
189#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::id::NodeId;
195 use crate::parser::parse_document;
196
197 #[test]
198 fn dedup_use_removes_duplicates() {
199 let input = r#"
200style card {
201 fill: #FFF
202}
203rect @box {
204 w: 100 h: 50
205 use: card
206 use: card
207}
208"#;
209 let mut graph = parse_document(input).unwrap();
210 dedup_use_styles(&mut graph);
211 let node = graph.get_by_id(NodeId::intern("box")).unwrap();
212 assert_eq!(node.use_styles.len(), 1, "duplicate use: should be removed");
213 }
214
215 #[test]
216 fn dedup_use_preserves_order() {
217 let input = r#"
218style a { fill: #111111 }
219style b { fill: #222222 }
220rect @box {
221 w: 100 h: 50
222 use: a
223 use: b
224 use: a
225}
226"#;
227 let mut graph = parse_document(input).unwrap();
228 dedup_use_styles(&mut graph);
229 let node = graph.get_by_id(NodeId::intern("box")).unwrap();
230 assert_eq!(node.use_styles.len(), 2);
231 assert_eq!(node.use_styles[0].as_str(), "a");
232 assert_eq!(node.use_styles[1].as_str(), "b");
233 }
234
235 #[test]
236 fn hoist_creates_shared_style_for_identical_nodes() {
237 let input = r#"
238rect @box_a {
239 w: 100 h: 50
240 fill: #FF0000
241 corner: 8
242}
243rect @box_b {
244 w: 200 h: 100
245 fill: #FF0000
246 corner: 8
247}
248"#;
249 let mut graph = parse_document(input).unwrap();
250 hoist_styles(&mut graph);
251
252 assert!(
254 !graph.styles.is_empty(),
255 "hoist should create a style block"
256 );
257
258 let box_a = graph.get_by_id(NodeId::intern("box_a")).unwrap();
259 let box_b = graph.get_by_id(NodeId::intern("box_b")).unwrap();
260
261 assert!(
263 !box_a.use_styles.is_empty(),
264 "box_a should reference the hoisted style"
265 );
266 assert!(
267 !box_b.use_styles.is_empty(),
268 "box_b should reference the hoisted style"
269 );
270 assert_eq!(
271 box_a.use_styles[0], box_b.use_styles[0],
272 "both should reference same style"
273 );
274
275 assert!(
277 box_a.style.fill.is_none(),
278 "inline fill should be cleared after hoist"
279 );
280 assert!(
281 box_b.style.fill.is_none(),
282 "inline fill should be cleared after hoist"
283 );
284 }
285
286 #[test]
287 fn sort_nodes_reorders_by_kind() {
288 let input = r#"
289text @label "Hello" {
290 font: "Inter" regular 14
291}
292rect @box {
293 w: 100 h: 50
294}
295group @container {
296 rect @inner {
297 w: 50 h: 50
298 }
299}
300"#;
301 let mut graph = parse_document(input).unwrap();
302 sort_nodes(&mut graph);
303 let children = graph.children(graph.root);
304 assert_eq!(
306 graph.graph[children[0]].id.as_str(),
307 "container",
308 "group should be first"
309 );
310 assert_eq!(
311 graph.graph[children[1]].id.as_str(),
312 "box",
313 "rect should be second"
314 );
315 assert_eq!(
316 graph.graph[children[2]].id.as_str(),
317 "label",
318 "text should be third"
319 );
320 }
321
322 #[test]
323 fn sort_nodes_preserves_relative_order() {
324 let input = r#"
325rect @second {
326 w: 200 h: 100
327}
328rect @first {
329 w: 100 h: 50
330}
331"#;
332 let mut graph = parse_document(input).unwrap();
333 sort_nodes(&mut graph);
334 let children = graph.children(graph.root);
335 assert_eq!(graph.graph[children[0]].id.as_str(), "second");
337 assert_eq!(graph.graph[children[1]].id.as_str(), "first");
338 }
339
340 #[test]
341 fn sort_nodes_only_top_level() {
342 let input = r#"
343group @outer {
344 text @label "Hi" {
345 font: "Inter" regular 14
346 }
347 rect @inner {
348 w: 50 h: 50
349 }
350}
351"#;
352 let mut graph = parse_document(input).unwrap();
353 sort_nodes(&mut graph);
354 let outer_idx = graph.index_of(NodeId::intern("outer")).unwrap();
355 let children = graph.children(outer_idx);
356 assert_eq!(
358 graph.graph[children[0]].id.as_str(),
359 "label",
360 "nested text should stay first"
361 );
362 assert_eq!(
363 graph.graph[children[1]].id.as_str(),
364 "inner",
365 "nested rect should stay second"
366 );
367 }
368}