1use std::collections::BTreeSet;
6use std::sync::Arc;
7
8use crate::proto;
9use crate::resolve::{
10 Align, Color, Fill, Image, ImageFile, ImageFiles, ImageId, Kind, Layout, Node, Origin, Rgba,
11 Scene, Shadow, Size, TextAlign, TextStyle, Vector,
12};
13
14fn word(c: Rgba) -> u32 {
15 u32::from_be_bytes([c.r, c.g, c.b, c.a])
16}
17
18fn unword(v: u32) -> Rgba {
19 let [r, g, b, a] = v.to_be_bytes();
20 Rgba { r, g, b, a }
21}
22
23fn rgba(c: Color) -> proto::Color {
24 proto::Color {
25 light: word(c.light),
26 dark: word(c.dark),
27 }
28}
29
30fn color(c: Option<proto::Color>) -> Color {
32 let c = c.unwrap_or_default();
33 Color {
34 light: unword(c.light),
35 dark: unword(c.dark),
36 }
37}
38
39fn size(s: Size) -> f32 {
40 match s {
41 Size::Px(v) => v,
42 Size::Hug => -1.0,
43 Size::Fill => -2.0,
44 }
45}
46
47fn unsize(v: f32) -> Size {
48 if v == -1.0 {
49 Size::Hug
50 } else if v == -2.0 {
51 Size::Fill
52 } else {
53 Size::Px(v)
54 }
55}
56
57fn align(a: Align) -> proto::Align {
58 match a {
59 Align::Start => proto::Align::Start,
60 Align::Center => proto::Align::Center,
61 Align::End => proto::Align::End,
62 Align::SpaceBetween => proto::Align::SpaceBetween,
63 }
64}
65
66fn unalign(a: i32) -> Align {
67 match proto::Align::try_from(a).unwrap_or(proto::Align::Start) {
68 proto::Align::Start => Align::Start,
69 proto::Align::Center => Align::Center,
70 proto::Align::End => Align::End,
71 proto::Align::SpaceBetween => Align::SpaceBetween,
72 }
73}
74
75pub fn encode(n: &Node) -> proto::Node {
76 proto::Node {
77 kind: match n.kind {
78 Kind::Frame => proto::Kind::Frame,
79 Kind::Text => proto::Kind::Text,
80 Kind::Rect => proto::Kind::Rect,
81 Kind::Ellipse => proto::Kind::Ellipse,
82 Kind::Vector => proto::Kind::Vector,
83 Kind::Image => proto::Kind::Image,
84 } as i32,
85 name: n.name.clone(),
86 x: n.x,
87 y: n.y,
88 width: size(n.width),
89 height: size(n.height),
90 opacity: n.opacity,
91 on_click: n.on_click.clone().unwrap_or_default(),
92 fill: match &n.fill {
93 Some(Fill::Solid(c)) => Some(rgba(*c)),
94 _ => None,
95 },
96 gradient: match &n.fill {
97 Some(Fill::Linear { angle, stops }) => Some(proto::Gradient {
98 angle: *angle,
99 stops: stops
100 .iter()
101 .map(|(c, at)| proto::Stop {
102 color: Some(rgba(*c)),
103 at: *at,
104 })
105 .collect(),
106 }),
107 _ => None,
108 },
109 vector: n.vector.as_ref().map(|v| proto::Vector {
110 d: v.d.clone(),
111 view_box: v.view_box.to_vec(),
112 }),
113 image: n.image.as_ref().map(|i| proto::Image {
114 src: i.src.clone(),
115 id: i.id.0.to_vec(),
116 }),
117 stroke: n.stroke.map(rgba),
118 stroke_width: n.stroke_width,
119 radius: n.radius.to_vec(),
120 shadow: n.shadow.as_ref().map(|s| proto::Shadow {
121 x: s.x,
122 y: s.y,
123 blur: s.blur,
124 spread: s.spread,
125 color: Some(rgba(s.color)),
126 }),
127 clip: n.clip,
128 layout: match n.layout {
129 Layout::None => proto::Layout::None,
130 Layout::Row => proto::Layout::Row,
131 Layout::Column => proto::Layout::Column,
132 } as i32,
133 gap: n.gap,
134 padding: n.padding.to_vec(),
135 align: align(n.align) as i32,
136 justify: align(n.justify) as i32,
137 text: n.text.as_ref().map(|t| proto::Text {
138 content: t.content.clone(),
139 size: t.size,
140 weight: t.weight as u32,
141 color: Some(rgba(t.color)),
142 line_height: t.line_height,
143 align: match t.align {
144 TextAlign::Left => proto::TextAlign::Left,
145 TextAlign::Center => proto::TextAlign::Center,
146 TextAlign::Right => proto::TextAlign::Right,
147 } as i32,
148 }),
149 children: n.children.iter().map(encode).collect(),
150 }
151}
152
153fn image_id(bytes: &[u8]) -> Result<ImageId, String> {
154 bytes
155 .try_into()
156 .map(ImageId)
157 .map_err(|_| format!("an image file id of {} bytes, not 32", bytes.len()))
158}
159
160pub fn decode(n: &proto::Node) -> Result<Node, String> {
162 let four = |v: &[f32]| [0, 1, 2, 3].map(|i| v.get(i).copied().unwrap_or(0.0));
163 Ok(Node {
164 kind: match proto::Kind::try_from(n.kind).unwrap_or(proto::Kind::Frame) {
165 proto::Kind::Frame => Kind::Frame,
166 proto::Kind::Text => Kind::Text,
167 proto::Kind::Rect => Kind::Rect,
168 proto::Kind::Ellipse => Kind::Ellipse,
169 proto::Kind::Vector => Kind::Vector,
170 proto::Kind::Image => Kind::Image,
171 },
172 name: n.name.clone(),
173 x: n.x,
174 y: n.y,
175 width: unsize(n.width),
176 height: unsize(n.height),
177 opacity: n.opacity,
178 on_click: Some(n.on_click.clone()).filter(|s| !s.is_empty()),
179 fill: match (&n.fill, &n.gradient) {
180 (Some(c), _) => Some(Fill::Solid(color(Some(*c)))),
181 (None, Some(g)) => Some(Fill::Linear {
182 angle: g.angle,
183 stops: g.stops.iter().map(|s| (color(s.color), s.at)).collect(),
184 }),
185 (None, None) => None,
186 },
187 stroke: n.stroke.map(|c| color(Some(c))),
188 stroke_width: n.stroke_width,
189 radius: four(&n.radius),
190 shadow: n.shadow.as_ref().map(|s| Shadow {
191 x: s.x,
192 y: s.y,
193 blur: s.blur,
194 spread: s.spread,
195 color: color(s.color),
196 at: Origin::default(),
197 }),
198 clip: n.clip,
199 layout: match proto::Layout::try_from(n.layout).unwrap_or(proto::Layout::None) {
200 proto::Layout::None => Layout::None,
201 proto::Layout::Row => Layout::Row,
202 proto::Layout::Column => Layout::Column,
203 },
204 gap: n.gap,
205 padding: four(&n.padding),
206 align: unalign(n.align),
207 justify: unalign(n.justify),
208 text: n.text.as_ref().map(|t| TextStyle {
209 content: t.content.clone(),
210 size: t.size,
211 weight: t.weight as u16,
212 color: color(t.color),
213 line_height: t.line_height,
214 align: match proto::TextAlign::try_from(t.align).unwrap_or(proto::TextAlign::Left) {
215 proto::TextAlign::Left => TextAlign::Left,
216 proto::TextAlign::Center => TextAlign::Center,
217 proto::TextAlign::Right => TextAlign::Right,
218 },
219 }),
220 vector: n.vector.as_ref().map(|v| Vector {
221 d: v.d.clone(),
222 view_box: four(&v.view_box),
223 }),
224 image: match &n.image {
225 Some(i) => Some(Image {
226 src: i.src.clone(),
227 id: image_id(&i.id)?,
228 at: Origin::default(),
229 }),
230 None => None,
231 },
232 children: n.children.iter().map(decode).collect::<Result<_, _>>()?,
233 })
234}
235
236pub fn scene(scene: &Scene) -> proto::Scene {
238 proto::Scene {
239 root: Some(encode(&scene.root)),
240 images: scene.images.values().map(|f| file(f)).collect(),
241 themed: scene.themed,
242 }
243}
244
245fn file(f: &ImageFile) -> proto::ImageFile {
246 proto::ImageFile {
247 id: f.id.0.to_vec(),
248 data: f.bytes.clone(),
249 }
250}
251
252pub fn patch(old: &Scene, new: &Scene) -> proto::Patch {
255 proto::Patch {
256 replace: diff(&old.root, &new.root),
257 images: new
258 .images
259 .values()
260 .filter(|f| !old.images.contains_key(&f.id))
261 .map(|f| file(f))
262 .collect(),
263 themed: new.themed,
264 }
265}
266
267fn files(files: &[proto::ImageFile], held: &ImageFiles) -> Result<ImageFiles, String> {
271 let mut out = ImageFiles::new();
272 for f in files {
273 let id = image_id(&f.id)?;
274 let decoded = match held.get(&id) {
275 Some(known) => known.clone(),
276 None => Arc::new(ImageFile::with_id(id, f.data.clone())?),
278 };
279 out.insert(id, decoded);
280 }
281 Ok(out)
282}
283
284fn settle(root: &Node, images: &mut ImageFiles) -> Result<(), String> {
287 fn shown(n: &Node, out: &mut BTreeSet<ImageId>) {
288 if let Some(i) = &n.image {
289 out.insert(i.id);
290 }
291 for c in &n.children {
292 shown(c, out);
293 }
294 }
295 let mut ids = BTreeSet::new();
296 shown(root, &mut ids);
297 if let Some(missing) = ids.iter().find(|id| !images.contains_key(id)) {
298 return Err(format!("the stream never sent the image file {missing:?}"));
299 }
300 images.retain(|id, _| ids.contains(id));
301 Ok(())
302}
303
304pub fn decode_scene(message: &proto::Scene, held: &ImageFiles) -> Result<Scene, String> {
307 let root = decode(message.root.as_ref().ok_or("a scene with no root")?)?;
308 let mut images = files(&message.images, held)?;
309 settle(&root, &mut images)?;
310 Ok(Scene {
311 root,
312 images,
313 files: Vec::new(),
314 themed: message.themed,
315 })
316}
317
318pub fn diff(old: &Node, new: &Node) -> Vec<proto::Replace> {
321 let mut out = Vec::new();
322 walk(old, new, "0", &mut out);
323 out
324}
325
326fn walk(old: &Node, new: &Node, id: &str, out: &mut Vec<proto::Replace>) {
327 if old == new {
328 return;
329 }
330 let same_shell = old.children.len() == new.children.len() && {
331 let (mut a, mut b) = (old.clone(), new.clone());
332 a.children.clear();
333 b.children.clear();
334 a == b
335 };
336 if !same_shell {
337 out.push(proto::Replace {
338 id: id.to_string(),
339 node: Some(encode(new)),
340 });
341 return;
342 }
343 for (i, (o, n)) in old.children.iter().zip(&new.children).enumerate() {
344 walk(o, n, &format!("{id}.{i}"), out);
345 }
346}
347
348pub fn apply(scene: &mut Scene, patch: &proto::Patch) -> Result<(), String> {
351 let mut next = scene.clone();
352 let added = files(&patch.images, &scene.images)?;
353 next.images.extend(added);
354 for r in &patch.replace {
355 let node = r.node.as_ref().ok_or("a replacement with no node")?;
356 let at = find(&mut next.root, &r.id).ok_or_else(|| format!("no node {}", r.id))?;
357 *at = decode(node)?;
358 }
359 settle(&next.root, &mut next.images)?;
360 next.themed = patch.themed;
361 *scene = next;
362 Ok(())
363}
364
365fn find<'a>(root: &'a mut Node, id: &str) -> Option<&'a mut Node> {
366 let mut at = root;
367 for step in id.split('.').skip(1) {
368 at = at.children.get_mut(step.parse::<usize>().ok()?)?;
369 }
370 Some(at)
371}
372
373#[cfg(test)]
374mod tests {
375 use std::collections::HashMap;
376
377 use prost::Message;
378
379 use super::*;
380 use crate::Files;
381
382 fn scene(src: &str) -> Scene {
383 let files: HashMap<String, String> = [("a.design".to_string(), src.to_string())].into();
384 crate::resolve(&files, "a.design").unwrap()
385 }
386
387 #[test]
388 fn a_patch_carries_only_what_changed() {
389 let old = scene(
390 r##"<Frame layout="column"><Text>One</Text><Frame><Text>Two</Text></Frame></Frame>"##,
391 );
392 let new = scene(
393 r##"<Frame layout="column"><Text>One</Text><Frame><Text>Three</Text></Frame></Frame>"##,
394 );
395 assert_eq!(decode(&encode(&old.root)).unwrap(), old.root);
396 let patch = patch(&old, &new);
397 assert_eq!(
398 patch
399 .replace
400 .iter()
401 .map(|r| r.id.as_str())
402 .collect::<Vec<_>>(),
403 vec!["0.1.0"]
404 );
405 let mut applied = decode_scene(&super::scene(&old), &ImageFiles::new()).unwrap();
406 apply(&mut applied, &patch).unwrap();
407 assert_eq!(applied.root, new.root);
408 }
409
410 #[test]
413 fn a_scene_carries_both_themes() {
414 let themed = |fill: &str| {
415 let files: HashMap<String, String> = [
416 (
417 "themes.yml".to_string(),
418 "kind: themes\nlight:\n a: \"#ffffff\"\ndark:\n a: \"#000000\"\n".to_string(),
419 ),
420 (
421 "a.design".to_string(),
422 format!(
423 "<Frame fill={{{fill}}} stroke={{{fill}}} shadow={{{{ color: {fill} }}}}><Text color={{{fill}}}>Hi</Text><Rect fill=\"linear-gradient(0deg, theme.a 0%, #f00 100%)\" /></Frame>"
424 ),
425 ),
426 ]
427 .into();
428 crate::resolve(&files, "a.design").unwrap()
429 };
430 let light_only = themed("\"#ffffff\"");
431 let both = themed("theme.a");
432 assert!(both.themed);
433 assert_eq!(decode(&encode(&both.root)).unwrap(), both.root);
434 let whole = decode_scene(&super::scene(&light_only), &ImageFiles::new()).unwrap();
435 assert!(whole.themed, "the gradient names theme.a");
436 let mut applied = whole;
437 apply(&mut applied, &patch(&light_only, &both)).unwrap();
438 assert_eq!(applied.root, both.root);
439 let plain = scene("<Frame />");
440 apply(&mut applied, &patch(&both, &plain)).unwrap();
441 assert!(!applied.themed);
442 }
443
444 struct Mem(HashMap<String, Vec<u8>>);
446
447 impl Files for Mem {
448 fn bytes(&self, path: &str) -> Option<Vec<u8>> {
449 self.0.get(path).cloned()
450 }
451 }
452
453 fn png(w: u32, h: u32) -> Vec<u8> {
454 let mut out = Vec::new();
455 let mut e = ::png::Encoder::new(&mut out, w, h);
456 e.set_color(::png::ColorType::Rgba);
457 let mut writer = e.write_header().unwrap();
458 writer
459 .write_image_data(&vec![200; (4 * w * h) as usize])
460 .unwrap();
461 drop(writer);
462 out
463 }
464
465 fn shirt() -> Vec<u8> {
466 std::fs::read(concat!(
467 env!("CARGO_MANIFEST_DIR"),
468 "/../docs/designs/checkout/shirt.jpg"
469 ))
470 .unwrap()
471 }
472
473 fn cards(n: usize, extra: &str) -> Mem {
474 let mut page = String::from("import Card from \"./card.design\"\n<Frame layout=\"row\">");
475 for _ in 0..n {
476 page.push_str("<Card />");
477 }
478 page.push_str(extra);
479 page.push_str("</Frame>");
480 Mem([
481 (
482 "card.design".to_string(),
483 br#"<Frame><Image src="shirt.jpg" width={100} height={100} /></Frame>"#.to_vec(),
484 ),
485 ("page.design".to_string(), page.into_bytes()),
486 ("shirt.jpg".to_string(), shirt()),
487 ("copy.jpg".to_string(), shirt()),
488 ]
489 .into())
490 }
491
492 #[test]
496 fn an_image_file_travels_once() {
497 let fifty = crate::resolve(&cards(50, ""), "page.design").unwrap();
498 assert_eq!(fifty.images.len(), 1);
499 let message = super::scene(&fifty).encode_to_vec().len();
500 let jpeg = shirt().len();
501 assert!(
502 message < jpeg + 50 * 200,
503 "a scene of fifty uses of a {jpeg}-byte file is {message} bytes"
504 );
505 let more = crate::resolve(
508 &cards(51, r#"<Image src="copy.jpg" width={10} height={10} />"#),
509 "page.design",
510 )
511 .unwrap();
512 assert_eq!(more.images.len(), 1, "copy.jpg is shirt.jpg's bytes");
513 let p = patch(&fifty, &more);
514 assert!(p.images.is_empty());
515 let mut browser = decode_scene(&super::scene(&fifty), &ImageFiles::new()).unwrap();
516 let held = browser.images.values().next().unwrap().clone();
517 apply(&mut browser, &p).unwrap();
518 assert_eq!(browser.root, more.root);
519 assert!(Arc::ptr_eq(browser.images.values().next().unwrap(), &held));
521 let mut fs = cards(1, r#"<Image src="other.png" width={10} height={10} />"#);
523 fs.0.insert("other.png".into(), png(2, 2));
524 let other = crate::resolve(&fs, "page.design").unwrap();
525 let p = patch(&more, &other);
526 assert_eq!(p.images.len(), 1);
527 apply(&mut browser, &p).unwrap();
528 assert_eq!(browser.images.len(), 2);
529 let mut lacking = p.clone();
531 lacking.images.clear();
532 let mut kept = decode_scene(&super::scene(&more), &ImageFiles::new()).unwrap();
533 assert!(
534 apply(&mut kept, &lacking)
535 .unwrap_err()
536 .contains("never sent")
537 );
538 assert_eq!(kept.root, more.root, "a refused patch leaves the scene");
539 }
540}