1use std::collections::{HashMap, HashSet};
9
10use serde_json::{Map, Value};
11
12use crate::transforms::{LengthComp, LengthUnit, TransformOp};
13use crate::{IRAnimation, IRKeyframe, IRNode, IRStyle, IRWindow};
14
15pub struct IRSerializer;
17
18impl IRSerializer {
19 pub fn to_dict(windows: &[IRWindow], logic_so_path: Option<&str>) -> Value {
22 let mut root = Map::new();
23 root.insert("type".to_string(), Value::String("app".to_string()));
24 root.insert(
25 "windows".to_string(),
26 Value::Array(windows.iter().map(Self::window).collect()),
27 );
28 if let Some(path) = logic_so_path {
29 root.insert("logic_so_path".to_string(), Value::String(path.to_string()));
30 }
31 Value::Object(root)
32 }
33
34 pub fn to_json(
36 windows: &[IRWindow],
37 logic_so_path: Option<&str>,
38 ) -> Result<String, serde_json::Error> {
39 serde_json::to_string(&Self::to_dict(windows, logic_so_path))
40 }
41
42 fn window(w: &IRWindow) -> Value {
43 let mut out = Map::new();
44 out.insert("id".to_string(), Value::String(w.window_id.clone()));
45 out.insert("title".to_string(), Value::String(w.title.clone()));
46 out.insert("width".to_string(), Value::from(w.width));
47 out.insert("height".to_string(), Value::from(w.height));
48 out.insert("visible".to_string(), Value::from(w.visible));
49 out.insert("renderer".to_string(), Value::String(w.renderer.clone()));
50 out.insert("nodes".to_string(), Value::Array(w.nodes.iter().map(Self::node).collect()));
51 out.insert("startup_logs".to_string(), strings(&w.startup_logs));
52 out.insert("premain_functions".to_string(), strings(&w.premain_functions));
53 out.insert("extra_headers".to_string(), strings(&w.extra_headers));
54 out.insert(
55 "state_vars".to_string(),
56 Value::Array(w.state_vars.iter().map(string_map).collect()),
57 );
58 out.insert(
59 "effect_decls".to_string(),
60 Value::Array(w.effect_decls.iter().map(string_map).collect()),
61 );
62 out.insert(
63 "cpp_imports".to_string(),
64 Value::Array(w.cpp_imports.iter().map(string_map).collect()),
65 );
66 out.insert("keyframes".to_string(), Self::keyframes_dict(&w.keyframes));
67 if let Some(v) = w.min_width {
68 out.insert("min_width".to_string(), Value::from(v));
69 }
70 if let Some(v) = w.max_width {
71 out.insert("max_width".to_string(), Value::from(v));
72 }
73 if let Some(v) = w.min_height {
74 out.insert("min_height".to_string(), Value::from(v));
75 }
76 if let Some(v) = w.max_height {
77 out.insert("max_height".to_string(), Value::from(v));
78 }
79 Value::Object(out)
80 }
81
82 fn keyframes_dict(keyframes: &HashMap<String, Vec<IRKeyframe>>) -> Value {
83 let mut names: Vec<&String> = keyframes.keys().collect();
84 names.sort();
85 let mut out = Map::new();
86 for name in names {
87 let frames = keyframes.get(name).map_or(&[] as &[IRKeyframe], Vec::as_slice);
88 out.insert(name.clone(), Value::Array(frames.iter().map(Self::keyframe).collect()));
89 }
90 Value::Object(out)
91 }
92
93 fn keyframe(kf: &IRKeyframe) -> Value {
94 let mut out = Map::new();
95 out.insert("offset".to_string(), num(kf.offset));
96 out.insert("style".to_string(), Self::keyframe_style(kf));
97 out.insert("raw".to_string(), string_map(&kf.raw));
98 Value::Object(out)
99 }
100
101 fn keyframe_style(kf: &IRKeyframe) -> Value {
107 let full = Self::style(&kf.style);
108 let keep: HashSet<&str> = if kf.declared.is_empty() {
109 fallback_declared(&kf.style)
110 } else {
111 kf.declared.iter().map(String::as_str).collect()
112 };
113 match full {
114 Value::Object(map) => {
115 Value::Object(map.into_iter().filter(|(k, _)| keep.contains(k.as_str())).collect())
116 }
117 other => other,
118 }
119 }
120
121 fn animations(anims: &[IRAnimation]) -> Value {
122 Value::Array(
123 anims
124 .iter()
125 .map(|a| {
126 let mut out = Map::new();
127 out.insert("name".to_string(), Value::String(a.name.clone()));
128 out.insert("duration".to_string(), num(a.duration));
129 out.insert("easing".to_string(), Value::String(a.easing.clone()));
130 out.insert("delay".to_string(), num(a.delay));
131 out.insert("iterations".to_string(), num(a.iterations));
132 out.insert("direction".to_string(), Value::String(a.direction.clone()));
133 out.insert("fill_mode".to_string(), Value::String(a.fill_mode.clone()));
134 out.insert("play_state".to_string(), Value::String(a.play_state.clone()));
135 Value::Object(out)
136 })
137 .collect(),
138 )
139 }
140
141 fn style(s: &IRStyle) -> Value {
142 let mut out = Map::new();
143 out.insert("bg_color".to_string(), floats(&s.bg_color));
144 out.insert("color".to_string(), floats(&s.color));
145 out.insert("width".to_string(), opt_num(s.width));
146 out.insert("min_width".to_string(), opt_num(s.min_width));
147 out.insert("max_width".to_string(), opt_num(s.max_width));
148 out.insert("height".to_string(), opt_num(s.height));
149 out.insert("min_height".to_string(), opt_num(s.min_height));
150 out.insert("max_height".to_string(), opt_num(s.max_height));
151 out.insert("margin".to_string(), floats(&s.margin));
152 out.insert(
153 "margin_auto".to_string(),
154 Value::Array(s.margin_auto.iter().map(|b| Value::from(*b)).collect()),
155 );
156 out.insert("padding".to_string(), floats(&s.padding));
157 out.insert("border_radius".to_string(), num(s.border_radius));
158 out.insert("font_size".to_string(), num(s.font_size));
159 out.insert("font_weight".to_string(), Value::String(s.font_weight.clone()));
160 out.insert("text_align".to_string(), Value::String(s.text_align.clone()));
161 out.insert("display".to_string(), Value::String(s.display.clone()));
162 out.insert("flex_dir".to_string(), Value::String(s.flex_dir.clone()));
163 out.insert("flex_grow".to_string(), num(s.flex_grow));
164 out.insert("flex_shrink".to_string(), num(s.flex_shrink));
165 out.insert("flex_basis".to_string(), Value::String(s.flex_basis.clone()));
166 out.insert("gap".to_string(), num(s.gap));
167 out.insert("overflow".to_string(), Value::String(s.overflow.clone()));
168 out.insert("position".to_string(), Value::String(s.position.clone()));
169 out.insert("left".to_string(), opt_num(s.left));
170 out.insert("right".to_string(), opt_num(s.right));
171 out.insert("top".to_string(), opt_num(s.top));
172 out.insert("bottom".to_string(), opt_num(s.bottom));
173 out.insert("justify_content".to_string(), Value::String(s.justify_content.clone()));
174 out.insert("align_items".to_string(), Value::String(s.align_items.clone()));
175 out.insert("flex_wrap".to_string(), Value::String(s.flex_wrap.clone()));
176 out.insert("cursor".to_string(), Value::String(s.cursor.clone()));
177 out.insert("scrollbar_width".to_string(), num(s.scrollbar_width));
178 out.insert("scrollbar_track_color".to_string(), floats(&s.scrollbar_track_color));
179 out.insert("scrollbar_thumb_color".to_string(), floats(&s.scrollbar_thumb_color));
180 out.insert("scrollbar_border_radius".to_string(), num(s.scrollbar_border_radius));
181 out.insert("border_width".to_string(), num(s.border_width));
182 out.insert("border_color".to_string(), floats(&s.border_color));
183 out.insert("border_style".to_string(), Value::String(s.border_style.clone()));
184 out.insert("box_sizing".to_string(), Value::String(s.box_sizing.clone()));
185 out.insert("z_index".to_string(), s.z_index.map_or(Value::Null, Value::from));
186 out.insert("opacity".to_string(), num(s.opacity));
187 out.insert(
188 "transform_ops".to_string(),
189 match s.transform_ops.as_deref().unwrap_or(&[]) {
190 [] => Value::Null,
191 ops => Value::Array(ops.iter().map(Self::transform_op).collect()),
192 },
193 );
194 out.insert(
195 "transform_matrix".to_string(),
196 s.transform_matrix.map_or(Value::Null, |m| floats(&m)),
197 );
198 out.insert(
199 "transform_origin".to_string(),
200 s.transform_origin_resolved
201 .map_or(Value::Null, |(x, y)| Value::Array(vec![num(x), num(y)])),
202 );
203 out.insert(
204 "transform_origin_raw".to_string(),
205 s.transform_origin.map_or(Value::Null, |((x, x_pct), (y, y_pct))| {
206 Value::Array(vec![
207 Value::Array(vec![num(x), Value::from(x_pct)]),
208 Value::Array(vec![num(y), Value::from(y_pct)]),
209 ])
210 }),
211 );
212 Value::Object(out)
213 }
214
215 fn transform_op(op: &TransformOp) -> Value {
218 match op {
219 TransformOp::Matrix(m) => {
220 Value::Array(vec![Value::String("matrix".to_string()), floats(m)])
221 }
222 TransformOp::Matrix3d(m) => {
223 Value::Array(vec![Value::String("matrix3d".to_string()), floats(m)])
224 }
225 TransformOp::Perspective(v) => {
226 Value::Array(vec![Value::String("perspective".to_string()), num(*v)])
227 }
228 TransformOp::Rotate(d) => {
229 Value::Array(vec![Value::String("rotate".to_string()), num(*d)])
230 }
231 TransformOp::RotateX(d) => {
232 Value::Array(vec![Value::String("rotatex".to_string()), num(*d)])
233 }
234 TransformOp::RotateY(d) => {
235 Value::Array(vec![Value::String("rotatey".to_string()), num(*d)])
236 }
237 TransformOp::RotateZ(d) => {
238 Value::Array(vec![Value::String("rotatez".to_string()), num(*d)])
239 }
240 TransformOp::Rotate3d(x, y, z, d) => {
241 Value::Array(vec![Value::String("rotate3d".to_string()), floats(&[*x, *y, *z, *d])])
242 }
243 TransformOp::Translate(a, b) => Value::Array(vec![
244 Value::String("translate".to_string()),
245 Value::Array(vec![length_comp(*a), length_comp(*b)]),
246 ]),
247 TransformOp::Translate3d(a, b, c) => Value::Array(vec![
248 Value::String("translate3d".to_string()),
249 Value::Array(vec![length_comp(*a), length_comp(*b), length_comp(*c)]),
250 ]),
251 TransformOp::Scale(x, y) => Value::Array(vec![
252 Value::String("scale".to_string()),
253 Value::Array(vec![num(*x), num(*y)]),
254 ]),
255 TransformOp::Scale3d(x, y, z) => Value::Array(vec![
256 Value::String("scale3d".to_string()),
257 Value::Array(vec![num(*x), num(*y), num(*z)]),
258 ]),
259 TransformOp::Skew(x, y) => Value::Array(vec![
260 Value::String("skew".to_string()),
261 Value::Array(vec![num(*x), num(*y)]),
262 ]),
263 }
264 }
265
266 fn node(n: &IRNode) -> Value {
267 let mut out = Map::new();
268 out.insert("id".to_string(), Value::String(n.node_id.clone()));
269 out.insert("type".to_string(), Value::String(n.node_type.clone()));
270 out.insert("x".to_string(), num(n.x));
271 out.insert("y".to_string(), num(n.y));
272 out.insert("w".to_string(), num(n.w));
273 out.insert("h".to_string(), num(n.h));
274 out.insert("text".to_string(), Value::String(n.text_content.clone()));
275 out.insert("attrs".to_string(), string_map(&n.attrs));
276 out.insert("style".to_string(), Self::style(&n.style));
277 out.insert(
278 "children".to_string(),
279 Value::Array(n.children.iter().map(Self::node).collect()),
280 );
281 out.insert(
282 "events".to_string(),
283 Value::Array(
284 n.events
285 .iter()
286 .map(|e| {
287 let mut ev = Map::new();
288 ev.insert("trigger".to_string(), Value::String(e.trigger.clone()));
289 ev.insert("action".to_string(), Value::String(e.action.clone()));
290 ev.insert("target".to_string(), Value::String(e.target.clone()));
291 Value::Object(ev)
292 })
293 .collect(),
294 ),
295 );
296 if !n.reactive_text.is_empty() {
297 out.insert("reactive_text".to_string(), Value::String(n.reactive_text.clone()));
298 }
299 if !n.reactive_class.is_empty() {
300 out.insert("reactive_class".to_string(), Value::String(n.reactive_class.clone()));
301 }
302 if !n.reactive_style.is_empty() {
303 out.insert("reactive_style".to_string(), string_map(&n.reactive_style));
304 }
305 if !n.reactive_attrs.is_empty() {
306 out.insert("reactive_attrs".to_string(), string_map(&n.reactive_attrs));
307 }
308 if !n.class_conditional_effects.is_empty() {
309 out.insert(
310 "class_conditional_effects".to_string(),
311 Value::Array(
312 n.class_conditional_effects
313 .iter()
314 .map(|fx| {
315 Value::Array(vec![
316 Value::String(fx.condition.clone()),
317 string_map(&fx.on_styles),
318 string_map(&fx.off_styles),
319 ])
320 })
321 .collect(),
322 ),
323 );
324 }
325 if !n.condition_expr.is_empty() {
326 out.insert("condition_expr".to_string(), Value::String(n.condition_expr.clone()));
327 out.insert(
328 "then_nodes".to_string(),
329 Value::Array(n.then_nodes.iter().map(Self::node).collect()),
330 );
331 out.insert(
332 "else_nodes".to_string(),
333 Value::Array(n.else_nodes.iter().map(Self::node).collect()),
334 );
335 }
336 if !n.list_expr.is_empty() {
337 out.insert("list_expr".to_string(), Value::String(n.list_expr.clone()));
338 out.insert("list_key_expr".to_string(), Value::String(n.list_key_expr.clone()));
339 }
340 if let Some(tmpl) = n.item_template.as_deref() {
341 out.insert("item_template".to_string(), Self::node(tmpl));
342 }
343 if let Some(hover) = n.hover_style.as_ref() {
344 out.insert("hover_style".to_string(), Self::style(hover));
345 }
346 if let Some(active) = n.active_style.as_ref() {
347 out.insert("active_style".to_string(), Self::style(active));
348 }
349 if n.transition_duration > 0.0 {
350 out.insert("transition_duration".to_string(), num(n.transition_duration));
351 out.insert("transition_easing".to_string(), Value::String(n.transition_easing.clone()));
352 }
353 if !n.animations.is_empty() {
354 out.insert("animations".to_string(), Self::animations(&n.animations));
355 }
356 if !n.hover_animations.is_empty() {
357 out.insert("hover_animations".to_string(), Self::animations(&n.hover_animations));
358 }
359 Value::Object(out)
360 }
361}
362
363fn num(v: f32) -> Value {
365 if v.is_finite() { Value::from(v) } else { Value::Null }
366}
367
368fn opt_num(v: Option<f32>) -> Value {
369 v.map_or(Value::Null, num)
370}
371
372fn floats(values: &[f32]) -> Value {
373 Value::Array(values.iter().map(|v| num(*v)).collect())
374}
375
376fn strings(values: &[String]) -> Value {
377 Value::Array(values.iter().map(|s| Value::String(s.clone())).collect())
378}
379
380fn string_map(map: &HashMap<String, String>) -> Value {
381 let mut keys: Vec<&String> = map.keys().collect();
382 keys.sort();
383 Value::Object(
384 keys.into_iter()
385 .map(|k| {
386 let v = map.get(k).map_or("", String::as_str);
387 (k.clone(), Value::String(v.to_string()))
388 })
389 .collect(),
390 )
391}
392
393fn length_comp(comp: LengthComp) -> Value {
394 let unit = match comp.1 {
395 LengthUnit::Px => "px",
396 LengthUnit::Pct => "%",
397 };
398 Value::Array(vec![num(comp.0), Value::String(unit.to_string())])
399}
400
401fn fallback_declared(style: &IRStyle) -> HashSet<&'static str> {
404 let mut keep = HashSet::new();
405 if style.opacity != 1.0 {
406 keep.insert("opacity");
407 }
408 if style.bg_color != [0.0, 0.0, 0.0, 0.0] {
409 keep.insert("bg_color");
410 }
411 if style.color != [0.0, 0.0, 0.0, 1.0] {
412 keep.insert("color");
413 }
414 if style.border_radius != 0.0 {
415 keep.insert("border_radius");
416 }
417 if style.font_size != 16.0 {
418 keep.insert("font_size");
419 }
420 if style.width.is_some() {
421 keep.insert("width");
422 }
423 if style.height.is_some() {
424 keep.insert("height");
425 }
426 if style.left.is_some() {
427 keep.insert("left");
428 }
429 if style.top.is_some() {
430 keep.insert("top");
431 }
432 keep
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 fn sample_window() -> IRWindow {
440 let mut window = IRWindow {
441 window_id: "win_0".to_string(),
442 title: "Test".to_string(),
443 width: 800,
444 height: 600,
445 visible: true,
446 renderer: String::new(),
447 ..Default::default()
448 };
449 let mut node = IRNode {
450 node_id: "node_0000".to_string(),
451 node_type: "div".to_string(),
452 text_content: "hi".to_string(),
453 x: 1.0,
454 y: 2.0,
455 w: 3.0,
456 h: 4.0,
457 ..Default::default()
458 };
459 node.style.bg_color = [1.0, 0.0, 0.0, 1.0];
460 node.attrs.insert("data-x".to_string(), "1".to_string());
461 window.nodes.push(node);
462 window
463 }
464
465 #[test]
466 fn root_shape_windows_and_type() {
467 let value = IRSerializer::to_dict(&[sample_window()], None);
468 assert_eq!(value["type"], Value::String("app".to_string()));
469 assert_eq!(value["windows"][0]["id"], Value::String("win_0".to_string()));
470 assert_eq!(value["windows"][0]["title"], Value::String("Test".to_string()));
471 assert!(value.get("logic_so_path").is_none());
472 }
473
474 #[test]
475 fn logic_so_path_attached_at_root() {
476 let value = IRSerializer::to_dict(&[sample_window()], Some("/tmp/logic.so"));
477 assert_eq!(value["logic_so_path"], Value::String("/tmp/logic.so".to_string()));
478 }
479
480 #[test]
481 fn node_shape_matches_dev_protocol() {
482 let value = IRSerializer::to_dict(&[sample_window()], None);
483 let node = &value["windows"][0]["nodes"][0];
484 assert_eq!(node["id"], Value::String("node_0000".to_string()));
485 assert_eq!(node["type"], Value::String("div".to_string()));
486 assert_eq!(node["text"], Value::String("hi".to_string()));
487 assert_eq!(node["x"], Value::from(1.0f32));
488 assert_eq!(node["attrs"]["data-x"], Value::String("1".to_string()));
489 assert_eq!(
490 node["style"]["bg_color"],
491 Value::Array(vec![
492 Value::from(1.0f32),
493 Value::from(0.0f32),
494 Value::from(0.0f32),
495 Value::from(1.0f32),
496 ])
497 );
498 assert!(node.get("reactive_text").is_none());
499 assert!(node.get("hover_style").is_none());
500 assert!(node.get("animations").is_none());
501 }
502
503 #[test]
504 fn unset_optionals_serialize_as_null() {
505 let value = IRSerializer::to_dict(&[sample_window()], None);
506 let style = &value["windows"][0]["nodes"][0]["style"];
507 assert_eq!(style["width"], Value::Null);
508 assert_eq!(style["z_index"], Value::Null);
509 assert_eq!(style["transform_ops"], Value::Null);
510 assert_eq!(style["transform_matrix"], Value::Null);
511 assert_eq!(style["transform_origin"], Value::Null);
512 }
513
514 #[test]
515 fn non_finite_floats_become_null() {
516 let mut window = sample_window();
517 window.nodes[0].style.opacity = f32::INFINITY;
518 let value = IRSerializer::to_dict(&[window], None);
519 assert_eq!(value["windows"][0]["nodes"][0]["style"]["opacity"], Value::Null);
520 }
521
522 #[test]
523 fn transform_serialization_shape() {
524 use crate::transforms::TransformOp;
525 let mut window = sample_window();
526 window.nodes[0].style.transform_ops =
527 Some(vec![TransformOp::Rotate(45.0), TransformOp::Scale(2.0, 3.0)]);
528 window.nodes[0].style.transform_matrix = Some([1.0; 16]);
529 window.nodes[0].style.transform_origin_resolved = Some((0.0, 1.0));
530 window.nodes[0].style.transform_origin = Some(((0.0, false), (100.0, true)));
531 let value = IRSerializer::to_dict(&[window], None);
532 let style = &value["windows"][0]["nodes"][0]["style"];
533 assert_eq!(
534 style["transform_ops"],
535 serde_json::json!([["rotate", 45.0], ["scale", [2.0, 3.0]]])
536 );
537 assert_eq!(style["transform_matrix"][0], Value::from(1.0f32));
538 assert_eq!(style["transform_origin"], serde_json::json!([0.0, 1.0]));
539 assert_eq!(style["transform_origin_raw"], serde_json::json!([[0.0, false], [100.0, true]]));
540 }
541
542 #[test]
543 fn keyframes_keep_only_declared_fields() {
544 let mut window = sample_window();
545 let mut kf = IRKeyframe {
546 offset: 0.5,
547 style: IRStyle::new(),
548 declared: vec!["opacity".to_string()],
549 raw: HashMap::new(),
550 };
551 kf.style.opacity = 0.25;
552 kf.style.bg_color = [1.0, 1.0, 1.0, 1.0];
553 window.keyframes.insert("fade".to_string(), vec![kf]);
554 let value = IRSerializer::to_dict(&[window], None);
555 let frame = &value["windows"][0]["keyframes"]["fade"][0];
556 assert_eq!(frame["offset"], Value::from(0.5f32));
557 assert_eq!(frame["style"]["opacity"], Value::from(0.25f32));
558 assert!(frame["style"].get("bg_color").is_none());
559 }
560
561 #[test]
562 fn to_json_round_trips() {
563 let text = IRSerializer::to_json(&[sample_window()], None).unwrap();
564 let parsed: Value = serde_json::from_str(&text).unwrap();
565 assert_eq!(parsed["type"], Value::String("app".to_string()));
566 }
567}