1use std::collections::BTreeSet;
7use std::sync::Arc;
8
9use crate::fonts::{FontFile, FontFiles, FontId};
10use crate::proto;
11use crate::resolve::{
12 Align, Color, Fill, Image, ImageFile, ImageFiles, ImageId, Kind, Layout, Node, Origin, Rgba,
13 Scene, Shadow, Size, TextAlign, TextStyle, Vector,
14};
15
16fn word(c: Rgba) -> u32 {
17 u32::from_be_bytes([c.r, c.g, c.b, c.a])
18}
19
20fn unword(v: u32) -> Rgba {
21 let [r, g, b, a] = v.to_be_bytes();
22 Rgba { r, g, b, a }
23}
24
25fn rgba(c: Color) -> proto::Color {
26 proto::Color {
27 light: word(c.light),
28 dark: word(c.dark),
29 }
30}
31
32fn color(c: Option<proto::Color>) -> Color {
34 let c = c.unwrap_or_default();
35 Color {
36 light: unword(c.light),
37 dark: unword(c.dark),
38 }
39}
40
41fn size(s: Size) -> f32 {
42 match s {
43 Size::Px(v) => v,
44 Size::Hug => -1.0,
45 Size::Fill => -2.0,
46 }
47}
48
49fn unsize(v: f32) -> Size {
50 if v == -1.0 {
51 Size::Hug
52 } else if v == -2.0 {
53 Size::Fill
54 } else {
55 Size::Px(v)
56 }
57}
58
59fn align(a: Align) -> proto::Align {
60 match a {
61 Align::Start => proto::Align::Start,
62 Align::Center => proto::Align::Center,
63 Align::End => proto::Align::End,
64 Align::SpaceBetween => proto::Align::SpaceBetween,
65 }
66}
67
68fn unalign(a: i32) -> Align {
69 match proto::Align::try_from(a).unwrap_or(proto::Align::Start) {
70 proto::Align::Start => Align::Start,
71 proto::Align::Center => Align::Center,
72 proto::Align::End => Align::End,
73 proto::Align::SpaceBetween => Align::SpaceBetween,
74 }
75}
76
77pub fn encode(n: &Node) -> proto::Node {
78 proto::Node {
79 kind: match n.kind {
80 Kind::Frame => proto::Kind::Frame,
81 Kind::Text => proto::Kind::Text,
82 Kind::Rect => proto::Kind::Rect,
83 Kind::Ellipse => proto::Kind::Ellipse,
84 Kind::Vector => proto::Kind::Vector,
85 Kind::Image => proto::Kind::Image,
86 } as i32,
87 name: n.name.clone(),
88 x: n.x,
89 y: n.y,
90 width: size(n.width),
91 height: size(n.height),
92 opacity: n.opacity,
93 on_click: n.on_click.clone().unwrap_or_default(),
94 fill: match &n.fill {
95 Some(Fill::Solid(c)) => Some(rgba(*c)),
96 _ => None,
97 },
98 gradient: match &n.fill {
99 Some(Fill::Linear { angle, stops }) => Some(proto::Gradient {
100 angle: *angle,
101 stops: stops
102 .iter()
103 .map(|(c, at)| proto::Stop {
104 color: Some(rgba(*c)),
105 at: *at,
106 })
107 .collect(),
108 }),
109 _ => None,
110 },
111 vector: n.vector.as_ref().map(|v| proto::Vector {
112 d: v.d.clone(),
113 view_box: v.view_box.to_vec(),
114 }),
115 image: n.image.as_ref().map(|i| proto::Image {
116 src: i.src.clone(),
117 id: i.id.0.to_vec(),
118 }),
119 stroke: n.stroke.map(rgba),
120 stroke_width: n.stroke_width,
121 radius: n.radius.to_vec(),
122 shadow: n.shadow.as_ref().map(|s| proto::Shadow {
123 x: s.x,
124 y: s.y,
125 blur: s.blur,
126 spread: s.spread,
127 color: Some(rgba(s.color)),
128 }),
129 clip: n.clip,
130 layout: match n.layout {
131 Layout::None => proto::Layout::None,
132 Layout::Row => proto::Layout::Row,
133 Layout::Column => proto::Layout::Column,
134 } as i32,
135 gap: n.gap,
136 padding: n.padding.to_vec(),
137 align: align(n.align) as i32,
138 justify: align(n.justify) as i32,
139 text: n.text.as_ref().map(|t| proto::Text {
140 font: t.font.map(|id| id.0.to_vec()).unwrap_or_default(),
141 content: t.content.clone(),
142 size: t.size,
143 weight: t.weight as u32,
144 color: Some(rgba(t.color)),
145 line_height: t.line_height,
146 align: match t.align {
147 TextAlign::Left => proto::TextAlign::Left,
148 TextAlign::Center => proto::TextAlign::Center,
149 TextAlign::Right => proto::TextAlign::Right,
150 } as i32,
151 }),
152 children: n.children.iter().map(encode).collect(),
153 }
154}
155
156fn image_id(bytes: &[u8]) -> Result<ImageId, String> {
157 bytes
158 .try_into()
159 .map(ImageId)
160 .map_err(|_| format!("an image file id of {} bytes, not 32", bytes.len()))
161}
162
163fn font_id(bytes: &[u8]) -> Result<FontId, String> {
164 bytes
165 .try_into()
166 .map(FontId)
167 .map_err(|_| format!("a font file id of {} bytes, not 32", bytes.len()))
168}
169
170pub fn decode(n: &proto::Node) -> Result<Node, String> {
172 let four = |v: &[f32]| [0, 1, 2, 3].map(|i| v.get(i).copied().unwrap_or(0.0));
173 Ok(Node {
174 kind: match proto::Kind::try_from(n.kind).unwrap_or(proto::Kind::Frame) {
175 proto::Kind::Frame => Kind::Frame,
176 proto::Kind::Text => Kind::Text,
177 proto::Kind::Rect => Kind::Rect,
178 proto::Kind::Ellipse => Kind::Ellipse,
179 proto::Kind::Vector => Kind::Vector,
180 proto::Kind::Image => Kind::Image,
181 },
182 name: n.name.clone(),
183 x: n.x,
184 y: n.y,
185 width: unsize(n.width),
186 height: unsize(n.height),
187 opacity: n.opacity,
188 on_click: Some(n.on_click.clone()).filter(|s| !s.is_empty()),
189 fill: match (&n.fill, &n.gradient) {
190 (Some(c), _) => Some(Fill::Solid(color(Some(*c)))),
191 (None, Some(g)) => Some(Fill::Linear {
192 angle: g.angle,
193 stops: g.stops.iter().map(|s| (color(s.color), s.at)).collect(),
194 }),
195 (None, None) => None,
196 },
197 stroke: n.stroke.map(|c| color(Some(c))),
198 stroke_width: n.stroke_width,
199 radius: four(&n.radius),
200 shadow: n.shadow.as_ref().map(|s| Shadow {
201 x: s.x,
202 y: s.y,
203 blur: s.blur,
204 spread: s.spread,
205 color: color(s.color),
206 at: Origin::default(),
207 }),
208 clip: n.clip,
209 layout: match proto::Layout::try_from(n.layout).unwrap_or(proto::Layout::None) {
210 proto::Layout::None => Layout::None,
211 proto::Layout::Row => Layout::Row,
212 proto::Layout::Column => Layout::Column,
213 },
214 gap: n.gap,
215 padding: four(&n.padding),
216 align: unalign(n.align),
217 justify: unalign(n.justify),
218 text: match &n.text {
219 Some(t) => Some(TextStyle {
220 font: if t.font.is_empty() {
221 None
222 } else {
223 Some(font_id(&t.font)?)
224 },
225 content: t.content.clone(),
226 size: t.size,
227 weight: t.weight as u16,
228 color: color(t.color),
229 line_height: t.line_height,
230 align: match proto::TextAlign::try_from(t.align).unwrap_or(proto::TextAlign::Left) {
231 proto::TextAlign::Left => TextAlign::Left,
232 proto::TextAlign::Center => TextAlign::Center,
233 proto::TextAlign::Right => TextAlign::Right,
234 },
235 }),
236 None => None,
237 },
238 vector: n.vector.as_ref().map(|v| Vector {
239 d: v.d.clone(),
240 view_box: four(&v.view_box),
241 }),
242 image: match &n.image {
243 Some(i) => Some(Image {
244 src: i.src.clone(),
245 id: image_id(&i.id)?,
246 at: Origin::default(),
247 }),
248 None => None,
249 },
250 children: n.children.iter().map(decode).collect::<Result<_, _>>()?,
251 })
252}
253
254pub fn scene(scene: &Scene) -> proto::Scene {
256 proto::Scene {
257 root: Some(encode(&scene.root)),
258 images: scene.images.values().map(|f| file(f)).collect(),
259 fonts: scene.fonts.values().map(|f| font_file(f)).collect(),
260 themed: scene.themed,
261 }
262}
263
264fn font_file(f: &FontFile) -> proto::FontFile {
265 proto::FontFile {
266 id: f.id.0.to_vec(),
267 data: f.bytes.to_vec(),
268 }
269}
270
271fn file(f: &ImageFile) -> proto::ImageFile {
272 proto::ImageFile {
273 id: f.id.0.to_vec(),
274 data: f.bytes.clone(),
275 }
276}
277
278pub fn patch(old: &Scene, new: &Scene) -> proto::Patch {
281 proto::Patch {
282 replace: diff(&old.root, &new.root),
283 images: new
284 .images
285 .values()
286 .filter(|f| !old.images.contains_key(&f.id))
287 .map(|f| file(f))
288 .collect(),
289 fonts: new
290 .fonts
291 .values()
292 .filter(|f| !old.fonts.contains_key(&f.id))
293 .map(|f| font_file(f))
294 .collect(),
295 themed: new.themed,
296 }
297}
298
299fn files(files: &[proto::ImageFile], held: &ImageFiles) -> Result<ImageFiles, String> {
303 let mut out = ImageFiles::new();
304 for f in files {
305 let id = image_id(&f.id)?;
306 let decoded = match held.get(&id) {
307 Some(known) => known.clone(),
308 None => Arc::new(ImageFile::with_id(id, f.data.clone())?),
310 };
311 out.insert(id, decoded);
312 }
313 Ok(out)
314}
315
316fn font_files(files: &[proto::FontFile], held: &FontFiles) -> Result<FontFiles, String> {
320 let mut out = FontFiles::new();
321 for f in files {
322 let id = font_id(&f.id)?;
323 let checked = match held.get(&id) {
324 Some(known) => known.clone(),
325 None => Arc::new(
327 FontFile::with_id(id, f.data.clone()).map_err(|e| format!("a font file {e}"))?,
328 ),
329 };
330 out.insert(id, checked);
331 }
332 Ok(out)
333}
334
335fn settle(root: &Node, images: &mut ImageFiles, fonts: &mut FontFiles) -> Result<(), String> {
338 fn used(n: &Node, images: &mut BTreeSet<ImageId>, fonts: &mut BTreeSet<FontId>) {
339 if let Some(i) = &n.image {
340 images.insert(i.id);
341 }
342 if let Some(font) = n.text.as_ref().and_then(|t| t.font) {
343 fonts.insert(font);
344 }
345 for c in &n.children {
346 used(c, images, fonts);
347 }
348 }
349 let (mut image_ids, mut font_ids) = (BTreeSet::new(), BTreeSet::new());
350 used(root, &mut image_ids, &mut font_ids);
351 if let Some(missing) = image_ids.iter().find(|id| !images.contains_key(id)) {
352 return Err(format!("the stream never sent the image file {missing:?}"));
353 }
354 if let Some(missing) = font_ids.iter().find(|id| !fonts.contains_key(id)) {
355 return Err(format!("the stream never sent the font file {missing:?}"));
356 }
357 images.retain(|id, _| image_ids.contains(id));
358 fonts.retain(|id, _| font_ids.contains(id));
359 Ok(())
360}
361
362pub fn decode_scene(
365 message: &proto::Scene,
366 held: (&ImageFiles, &FontFiles),
367) -> Result<Scene, String> {
368 let root = decode(message.root.as_ref().ok_or("a scene with no root")?)?;
369 let mut images = files(&message.images, held.0)?;
370 let mut fonts = font_files(&message.fonts, held.1)?;
371 settle(&root, &mut images, &mut fonts)?;
372 Ok(Scene {
373 root,
374 images,
375 fonts,
376 files: Vec::new(),
377 themed: message.themed,
378 })
379}
380
381pub fn diff(old: &Node, new: &Node) -> Vec<proto::Replace> {
384 let mut out = Vec::new();
385 walk(old, new, "0", &mut out);
386 out
387}
388
389fn walk(old: &Node, new: &Node, id: &str, out: &mut Vec<proto::Replace>) {
390 if old == new {
391 return;
392 }
393 let same_shell = old.children.len() == new.children.len() && {
394 let (mut a, mut b) = (old.clone(), new.clone());
395 a.children.clear();
396 b.children.clear();
397 a == b
398 };
399 if !same_shell {
400 out.push(proto::Replace {
401 id: id.to_string(),
402 node: Some(encode(new)),
403 });
404 return;
405 }
406 for (i, (o, n)) in old.children.iter().zip(&new.children).enumerate() {
407 walk(o, n, &format!("{id}.{i}"), out);
408 }
409}
410
411pub fn apply(scene: &mut Scene, patch: &proto::Patch) -> Result<(), String> {
414 let mut next = scene.clone();
415 let added = files(&patch.images, &scene.images)?;
416 next.images.extend(added);
417 let added = font_files(&patch.fonts, &scene.fonts)?;
418 next.fonts.extend(added);
419 for r in &patch.replace {
420 let node = r.node.as_ref().ok_or("a replacement with no node")?;
421 let at = find(&mut next.root, &r.id).ok_or_else(|| format!("no node {}", r.id))?;
422 *at = decode(node)?;
423 }
424 settle(&next.root, &mut next.images, &mut next.fonts)?;
425 next.themed = patch.themed;
426 *scene = next;
427 Ok(())
428}
429
430fn find<'a>(root: &'a mut Node, id: &str) -> Option<&'a mut Node> {
431 let mut at = root;
432 for step in id.split('.').skip(1) {
433 at = at.children.get_mut(step.parse::<usize>().ok()?)?;
434 }
435 Some(at)
436}
437
438#[cfg(test)]
439mod tests {
440 use std::collections::HashMap;
441
442 use prost::Message;
443
444 use super::*;
445 use crate::Files;
446
447 fn scene(src: &str) -> Scene {
448 let files: HashMap<String, String> = [("a.design".to_string(), src.to_string())].into();
449 crate::resolve(&files, "a.design").unwrap()
450 }
451
452 #[test]
453 fn a_patch_carries_only_what_changed() {
454 let old = scene(
455 r##"<Frame layout="column"><Text>One</Text><Frame><Text>Two</Text></Frame></Frame>"##,
456 );
457 let new = scene(
458 r##"<Frame layout="column"><Text>One</Text><Frame><Text>Three</Text></Frame></Frame>"##,
459 );
460 assert_eq!(decode(&encode(&old.root)).unwrap(), old.root);
461 let patch = patch(&old, &new);
462 assert_eq!(
463 patch
464 .replace
465 .iter()
466 .map(|r| r.id.as_str())
467 .collect::<Vec<_>>(),
468 vec!["0.1.0"]
469 );
470 let mut applied =
471 decode_scene(&super::scene(&old), (&ImageFiles::new(), &FontFiles::new())).unwrap();
472 apply(&mut applied, &patch).unwrap();
473 assert_eq!(applied.root, new.root);
474 }
475
476 #[test]
479 fn a_scene_carries_both_themes() {
480 let themed = |fill: &str| {
481 let files: HashMap<String, String> = [
482 (
483 "themes.yml".to_string(),
484 "kind: themes\nlight:\n a: \"#ffffff\"\ndark:\n a: \"#000000\"\n".to_string(),
485 ),
486 (
487 "a.design".to_string(),
488 format!(
489 "<Frame fill={{{fill}}} stroke={{{fill}}} shadow={{{{ color: {fill} }}}}><Text color={{{fill}}}>Hi</Text><Rect fill=\"linear-gradient(0deg, theme.a 0%, #f00 100%)\" /></Frame>"
490 ),
491 ),
492 ]
493 .into();
494 crate::resolve(&files, "a.design").unwrap()
495 };
496 let light_only = themed("\"#ffffff\"");
497 let both = themed("theme.a");
498 assert!(both.themed);
499 assert_eq!(decode(&encode(&both.root)).unwrap(), both.root);
500 let whole = decode_scene(
501 &super::scene(&light_only),
502 (&ImageFiles::new(), &FontFiles::new()),
503 )
504 .unwrap();
505 assert!(whole.themed, "the gradient names theme.a");
506 let mut applied = whole;
507 apply(&mut applied, &patch(&light_only, &both)).unwrap();
508 assert_eq!(applied.root, both.root);
509 let plain = scene("<Frame />");
510 apply(&mut applied, &patch(&both, &plain)).unwrap();
511 assert!(!applied.themed);
512 }
513
514 struct Mem(HashMap<String, Vec<u8>>);
516
517 impl Files for Mem {
518 fn bytes(&self, path: &str) -> Option<Vec<u8>> {
519 self.0.get(path).cloned()
520 }
521 fn list(&self, path: &str) -> Option<Vec<String>> {
522 crate::resolve::listed(self.0.keys(), path)
523 }
524 }
525
526 fn png(w: u32, h: u32) -> Vec<u8> {
527 let mut out = Vec::new();
528 let mut e = ::png::Encoder::new(&mut out, w, h);
529 e.set_color(::png::ColorType::Rgba);
530 let mut writer = e.write_header().unwrap();
531 writer
532 .write_image_data(&vec![200; (4 * w * h) as usize])
533 .unwrap();
534 drop(writer);
535 out
536 }
537
538 fn shirt() -> Vec<u8> {
539 std::fs::read(concat!(
540 env!("CARGO_MANIFEST_DIR"),
541 "/../docs/designs/checkout/shirt.jpg"
542 ))
543 .unwrap()
544 }
545
546 fn cards(n: usize, extra: &str) -> Mem {
547 let mut page = String::from("import Card from \"./card.design\"\n<Frame layout=\"row\">");
548 for _ in 0..n {
549 page.push_str("<Card />");
550 }
551 page.push_str(extra);
552 page.push_str("</Frame>");
553 Mem([
554 (
555 "card.design".to_string(),
556 br#"<Frame><Image src="shirt.jpg" width={100} height={100} /></Frame>"#.to_vec(),
557 ),
558 ("page.design".to_string(), page.into_bytes()),
559 ("shirt.jpg".to_string(), shirt()),
560 ("copy.jpg".to_string(), shirt()),
561 ]
562 .into())
563 }
564
565 #[test]
569 fn an_image_file_travels_once() {
570 let fifty = crate::resolve(&cards(50, ""), "page.design").unwrap();
571 assert_eq!(fifty.images.len(), 1);
572 let message = super::scene(&fifty).encode_to_vec().len();
573 let jpeg = shirt().len();
574 assert!(
575 message < jpeg + 50 * 200,
576 "a scene of fifty uses of a {jpeg}-byte file is {message} bytes"
577 );
578 let more = crate::resolve(
581 &cards(51, r#"<Image src="copy.jpg" width={10} height={10} />"#),
582 "page.design",
583 )
584 .unwrap();
585 assert_eq!(more.images.len(), 1, "copy.jpg is shirt.jpg's bytes");
586 let p = patch(&fifty, &more);
587 assert!(p.images.is_empty());
588 let mut browser = decode_scene(
589 &super::scene(&fifty),
590 (&ImageFiles::new(), &FontFiles::new()),
591 )
592 .unwrap();
593 let held = browser.images.values().next().unwrap().clone();
594 apply(&mut browser, &p).unwrap();
595 assert_eq!(browser.root, more.root);
596 assert!(Arc::ptr_eq(browser.images.values().next().unwrap(), &held));
598 let mut fs = cards(1, r#"<Image src="other.png" width={10} height={10} />"#);
600 fs.0.insert("other.png".into(), png(2, 2));
601 let other = crate::resolve(&fs, "page.design").unwrap();
602 let p = patch(&more, &other);
603 assert_eq!(p.images.len(), 1);
604 apply(&mut browser, &p).unwrap();
605 assert_eq!(browser.images.len(), 2);
606 let mut lacking = p.clone();
608 lacking.images.clear();
609 let mut kept = decode_scene(
610 &super::scene(&more),
611 (&ImageFiles::new(), &FontFiles::new()),
612 )
613 .unwrap();
614 assert!(
615 apply(&mut kept, &lacking)
616 .unwrap_err()
617 .contains("never sent")
618 );
619 assert_eq!(kept.root, more.root, "a refused patch leaves the scene");
620 }
621
622 fn font(name: &str) -> Vec<u8> {
623 std::fs::read(format!(
624 "{}/fonts/Inter-{name}.ttf",
625 env!("CARGO_MANIFEST_DIR")
626 ))
627 .unwrap()
628 }
629
630 fn texts(body: &str) -> Scene {
631 let fs = Mem([
632 ("f/Inter-Regular.ttf".to_string(), font("Regular")),
633 ("f/Inter-Bold.ttf".to_string(), font("Bold")),
634 (
635 "themes.yml".to_string(),
636 b"kind: themes\nfonts:\n inter: ./f\nlight: {}\ndark: {}\n".to_vec(),
637 ),
638 (
639 "a.design".to_string(),
640 format!("<Frame layout=\"column\">{body}</Frame>").into_bytes(),
641 ),
642 ]
643 .into());
644 crate::resolve(&fs, "a.design").unwrap()
645 }
646
647 #[test]
651 fn a_font_file_travels_once() {
652 let many = texts(&r#"<Text font="inter">a</Text>"#.repeat(20));
653 assert_eq!(many.fonts.len(), 1);
654 let message = super::scene(&many).encode_to_vec().len();
655 let size = font("Regular").len();
656 assert!(
657 message < size + 20 * 200,
658 "{message} bytes for one {size}-byte font file"
659 );
660 assert_eq!(decode(&encode(&many.root)).unwrap(), many.root);
661 let mut browser = decode_scene(
662 &super::scene(&many),
663 (&ImageFiles::new(), &FontFiles::new()),
664 )
665 .unwrap();
666 let held = browser.fonts.values().next().unwrap().clone();
667 let more = texts(&r#"<Text font="inter">a</Text>"#.repeat(21));
668 let p = patch(&many, &more);
669 assert!(p.fonts.is_empty());
670 apply(&mut browser, &p).unwrap();
671 assert!(Arc::ptr_eq(browser.fonts.values().next().unwrap(), &held));
672 let bold = texts(r#"<Text font="inter">a</Text><Text font="inter" weight={700}>b</Text>"#);
673 let p = patch(&more, &bold);
674 assert_eq!(p.fonts.len(), 1);
675 apply(&mut browser, &p).unwrap();
676 assert_eq!(browser.root, bold.root);
677 assert_eq!(browser.fonts.len(), 2);
678 let mut lacking = patch(&more, &bold);
679 lacking.fonts.clear();
680 let mut kept = decode_scene(
681 &super::scene(&more),
682 (&ImageFiles::new(), &FontFiles::new()),
683 )
684 .unwrap();
685 assert!(
686 apply(&mut kept, &lacking)
687 .unwrap_err()
688 .contains("never sent the font file")
689 );
690 let plain = texts("<Text>a</Text>");
692 apply(&mut browser, &patch(&bold, &plain)).unwrap();
693 assert!(browser.fonts.is_empty());
694 }
695}