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