1use crate::id::NodeId;
7use crate::model::{NodeKind, Paint, Properties, SceneGraph, SceneNode};
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>, Properties)> = HashMap::new();
45
46 for idx in graph.graph.node_indices() {
47 let node = &graph.graph[idx];
48 if is_style_empty(&node.props) {
49 continue;
50 }
51 let fp = style_fingerprint(&node.props);
52 let entry = fp_map
53 .entry(fp)
54 .or_insert_with(|| (Vec::new(), node.props.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.props = Properties::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: &Properties) -> 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: &Properties) -> 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::Image { .. } => 6,
163 NodeKind::Generic => 7,
164 }
165}
166
167pub fn sort_nodes(graph: &mut SceneGraph) {
176 let root = graph.root;
177 let mut children = graph.children(root);
178
179 if children.len() < 2 {
180 return;
181 }
182
183 children.sort_by_key(|&idx| kind_priority(&graph.graph[idx].kind));
185
186 graph.sorted_child_order.insert(root, children);
188}
189
190#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::id::NodeId;
196 use crate::parser::parse_document;
197
198 #[test]
199 fn dedup_use_removes_duplicates() {
200 let input = r#"
201style card {
202 fill: #FFF
203}
204rect @box {
205 w: 100 h: 50
206 use: card
207 use: card
208}
209"#;
210 let mut graph = parse_document(input).unwrap();
211 dedup_use_styles(&mut graph);
212 let node = graph.get_by_id(NodeId::intern("box")).unwrap();
213 assert_eq!(node.use_styles.len(), 1, "duplicate use: should be removed");
214 }
215
216 #[test]
217 fn dedup_use_preserves_order() {
218 let input = r#"
219style a { fill: #111111 }
220style b { fill: #222222 }
221rect @box {
222 w: 100 h: 50
223 use: a
224 use: b
225 use: a
226}
227"#;
228 let mut graph = parse_document(input).unwrap();
229 dedup_use_styles(&mut graph);
230 let node = graph.get_by_id(NodeId::intern("box")).unwrap();
231 assert_eq!(node.use_styles.len(), 2);
232 assert_eq!(node.use_styles[0].as_str(), "a");
233 assert_eq!(node.use_styles[1].as_str(), "b");
234 }
235
236 #[test]
237 fn hoist_creates_shared_style_for_identical_nodes() {
238 let input = r#"
239rect @box_a {
240 w: 100 h: 50
241 fill: #FF0000
242 corner: 8
243}
244rect @box_b {
245 w: 200 h: 100
246 fill: #FF0000
247 corner: 8
248}
249"#;
250 let mut graph = parse_document(input).unwrap();
251 hoist_styles(&mut graph);
252
253 assert!(
255 !graph.styles.is_empty(),
256 "hoist should create a style block"
257 );
258
259 let box_a = graph.get_by_id(NodeId::intern("box_a")).unwrap();
260 let box_b = graph.get_by_id(NodeId::intern("box_b")).unwrap();
261
262 assert!(
264 !box_a.use_styles.is_empty(),
265 "box_a should reference the hoisted style"
266 );
267 assert!(
268 !box_b.use_styles.is_empty(),
269 "box_b should reference the hoisted style"
270 );
271 assert_eq!(
272 box_a.use_styles[0], box_b.use_styles[0],
273 "both should reference same style"
274 );
275
276 assert!(
278 box_a.props.fill.is_none(),
279 "inline fill should be cleared after hoist"
280 );
281 assert!(
282 box_b.props.fill.is_none(),
283 "inline fill should be cleared after hoist"
284 );
285 }
286
287 #[test]
288 fn sort_nodes_reorders_by_kind() {
289 let input = r#"
290text @label "Hello" {
291 font: "Inter" regular 14
292}
293rect @box {
294 w: 100 h: 50
295}
296group @container {
297 rect @inner {
298 w: 50 h: 50
299 }
300}
301"#;
302 let mut graph = parse_document(input).unwrap();
303 sort_nodes(&mut graph);
304 let children = graph.children(graph.root);
305 assert_eq!(
307 graph.graph[children[0]].id.as_str(),
308 "container",
309 "group should be first"
310 );
311 assert_eq!(
312 graph.graph[children[1]].id.as_str(),
313 "box",
314 "rect should be second"
315 );
316 assert_eq!(
317 graph.graph[children[2]].id.as_str(),
318 "label",
319 "text should be third"
320 );
321 }
322
323 #[test]
324 fn sort_nodes_preserves_relative_order() {
325 let input = r#"
326rect @second {
327 w: 200 h: 100
328}
329rect @first {
330 w: 100 h: 50
331}
332"#;
333 let mut graph = parse_document(input).unwrap();
334 sort_nodes(&mut graph);
335 let children = graph.children(graph.root);
336 assert_eq!(graph.graph[children[0]].id.as_str(), "second");
338 assert_eq!(graph.graph[children[1]].id.as_str(), "first");
339 }
340
341 #[test]
342 fn sort_nodes_only_top_level() {
343 let input = r#"
344group @outer {
345 text @label "Hi" {
346 font: "Inter" regular 14
347 }
348 rect @inner {
349 w: 50 h: 50
350 }
351}
352"#;
353 let mut graph = parse_document(input).unwrap();
354 sort_nodes(&mut graph);
355 let outer_idx = graph.index_of(NodeId::intern("outer")).unwrap();
356 let children = graph.children(outer_idx);
357 assert_eq!(
359 graph.graph[children[0]].id.as_str(),
360 "label",
361 "nested text should stay first"
362 );
363 assert_eq!(
364 graph.graph[children[1]].id.as_str(),
365 "inner",
366 "nested rect should stay second"
367 );
368 }
369}