Skip to main content

devpage_design/wire/
mod.rs

1//! Scenes and their edits on the wire: a scene to its `proto::Scene` and
2//! back, and [`Edits`](crate::scene::Edits) to the `proto::Patch` the watch
3//! stream sends and back ([`patch`]). Each image file and font file travels
4//! once: a scene carries every one its nodes use, a patch only those the
5//! scene before it didn't have. It does no I/O.
6
7use std::collections::BTreeSet;
8use std::sync::Arc;
9
10use crate::fonts::{FontFile, FontFiles, FontId};
11use crate::proto;
12use crate::scene::{
13    Align, Color, Fill, Image, ImageFile, ImageFiles, ImageId, Kind, Layout, Node, Origin, Rgba,
14    Scene, Shadow, Size, TextAlign, TextStyle, Vector,
15};
16use crate::tree::{Children, NodeId};
17
18fn word(c: Rgba) -> u32 {
19    u32::from_be_bytes([c.r, c.g, c.b, c.a])
20}
21
22fn unword(v: u32) -> Rgba {
23    let [r, g, b, a] = v.to_be_bytes();
24    Rgba { r, g, b, a }
25}
26
27fn rgba(c: Color) -> proto::Color {
28    proto::Color {
29        light: word(c.light),
30        dark: word(c.dark),
31    }
32}
33
34/// A color from the wire; one the stream left out is transparent.
35fn color(c: Option<proto::Color>) -> Color {
36    let c = c.unwrap_or_default();
37    Color {
38        light: unword(c.light),
39        dark: unword(c.dark),
40    }
41}
42
43fn size(s: Size) -> f32 {
44    match s {
45        Size::Px(v) => v,
46        Size::Hug => -1.0,
47        Size::Fill => -2.0,
48    }
49}
50
51fn unsize(v: f32) -> Size {
52    if v == -1.0 {
53        Size::Hug
54    } else if v == -2.0 {
55        Size::Fill
56    } else {
57        Size::Px(v)
58    }
59}
60
61fn align(a: Align) -> proto::Align {
62    match a {
63        Align::Start => proto::Align::Start,
64        Align::Center => proto::Align::Center,
65        Align::End => proto::Align::End,
66        Align::SpaceBetween => proto::Align::SpaceBetween,
67    }
68}
69
70fn unalign(a: i32) -> Align {
71    match proto::Align::try_from(a).unwrap_or(proto::Align::Start) {
72        proto::Align::Start => Align::Start,
73        proto::Align::Center => Align::Center,
74        proto::Align::End => Align::End,
75        proto::Align::SpaceBetween => Align::SpaceBetween,
76    }
77}
78
79pub fn encode(n: &Node) -> proto::Node {
80    proto::Node {
81        kind: match n.kind {
82            Kind::Frame => proto::Kind::Frame,
83            Kind::Text => proto::Kind::Text,
84            Kind::Rect => proto::Kind::Rect,
85            Kind::Ellipse => proto::Kind::Ellipse,
86            Kind::Vector => proto::Kind::Vector,
87            Kind::Image => proto::Kind::Image,
88        } as i32,
89        name: n.name.clone(),
90        x: n.x,
91        y: n.y,
92        width: size(n.width),
93        height: size(n.height),
94        opacity: n.opacity,
95        on_click: n.on_click.clone().unwrap_or_default(),
96        fill: match &n.fill {
97            Some(Fill::Solid(c)) => Some(rgba(*c)),
98            _ => None,
99        },
100        gradient: match &n.fill {
101            Some(Fill::Linear { angle, stops }) => Some(proto::Gradient {
102                angle: *angle,
103                stops: stops
104                    .iter()
105                    .map(|(c, at)| proto::Stop {
106                        color: Some(rgba(*c)),
107                        at: *at,
108                    })
109                    .collect(),
110            }),
111            _ => None,
112        },
113        vector: n.vector.as_ref().map(|v| proto::Vector {
114            d: v.d.clone(),
115            view_box: v.view_box.to_vec(),
116        }),
117        image: n.image.as_ref().map(|i| proto::Image {
118            src: i.src.clone(),
119            id: i.id.0.to_vec(),
120        }),
121        stroke: n.stroke.map(rgba),
122        stroke_width: n.stroke_width,
123        radius: n.radius.to_vec(),
124        shadow: n.shadow.as_ref().map(|s| proto::Shadow {
125            x: s.x,
126            y: s.y,
127            blur: s.blur,
128            spread: s.spread,
129            color: Some(rgba(s.color)),
130        }),
131        clip: n.clip,
132        layout: match n.layout {
133            Layout::None => proto::Layout::None,
134            Layout::Row => proto::Layout::Row,
135            Layout::Column => proto::Layout::Column,
136        } as i32,
137        gap: n.gap,
138        padding: n.padding.to_vec(),
139        align: align(n.align) as i32,
140        justify: align(n.justify) as i32,
141        text: n.text.as_ref().map(|t| proto::Text {
142            font: t.font.map(|id| id.0.to_vec()).unwrap_or_default(),
143            content: t.content.clone(),
144            size: t.size,
145            weight: t.weight as u32,
146            color: Some(rgba(t.color)),
147            line_height: t.line_height,
148            align: match t.align {
149                TextAlign::Left => proto::TextAlign::Left,
150                TextAlign::Center => proto::TextAlign::Center,
151                TextAlign::Right => proto::TextAlign::Right,
152            } as i32,
153        }),
154        children: n.children.iter().map(encode).collect(),
155        id: Some(node_id(&n.id)),
156    }
157}
158
159/// A node's own attributes for the wire: its children aside.
160pub fn encode_own(n: &Node) -> proto::Node {
161    let mut own = n.clone();
162    own.children.clear();
163    encode(&own)
164}
165
166fn node_id(id: &NodeId) -> proto::NodeId {
167    proto::NodeId { path: id.0.clone() }
168}
169
170fn unnode_id(id: Option<&proto::NodeId>) -> Result<NodeId, String> {
171    id.map(|i| NodeId(i.path.clone()))
172        .ok_or_else(|| "a node with no id".to_string())
173}
174
175fn image_id(bytes: &[u8]) -> Result<ImageId, String> {
176    bytes
177        .try_into()
178        .map(ImageId)
179        .map_err(|_| format!("an image file id of {} bytes, not 32", bytes.len()))
180}
181
182fn font_id(bytes: &[u8]) -> Result<FontId, String> {
183    bytes
184        .try_into()
185        .map(FontId)
186        .map_err(|_| format!("a font file id of {} bytes, not 32", bytes.len()))
187}
188
189/// A node from the wire, or why it isn't one.
190pub fn decode(n: &proto::Node) -> Result<Node, String> {
191    let four = |v: &[f32]| [0, 1, 2, 3].map(|i| v.get(i).copied().unwrap_or(0.0));
192    Ok(Node {
193        id: unnode_id(n.id.as_ref())?,
194        kind: match proto::Kind::try_from(n.kind).unwrap_or(proto::Kind::Frame) {
195            proto::Kind::Frame => Kind::Frame,
196            proto::Kind::Text => Kind::Text,
197            proto::Kind::Rect => Kind::Rect,
198            proto::Kind::Ellipse => Kind::Ellipse,
199            proto::Kind::Vector => Kind::Vector,
200            proto::Kind::Image => Kind::Image,
201        },
202        name: n.name.clone(),
203        x: n.x,
204        y: n.y,
205        width: unsize(n.width),
206        height: unsize(n.height),
207        opacity: n.opacity,
208        on_click: Some(n.on_click.clone()).filter(|s| !s.is_empty()),
209        fill: match (&n.fill, &n.gradient) {
210            (Some(c), _) => Some(Fill::Solid(color(Some(*c)))),
211            (None, Some(g)) => Some(Fill::Linear {
212                angle: g.angle,
213                stops: g.stops.iter().map(|s| (color(s.color), s.at)).collect(),
214            }),
215            (None, None) => None,
216        },
217        stroke: n.stroke.map(|c| color(Some(c))),
218        stroke_width: n.stroke_width,
219        radius: four(&n.radius),
220        shadow: n.shadow.as_ref().map(|s| Shadow {
221            x: s.x,
222            y: s.y,
223            blur: s.blur,
224            spread: s.spread,
225            color: color(s.color),
226            at: Origin::default(),
227        }),
228        clip: n.clip,
229        layout: match proto::Layout::try_from(n.layout).unwrap_or(proto::Layout::None) {
230            proto::Layout::None => Layout::None,
231            proto::Layout::Row => Layout::Row,
232            proto::Layout::Column => Layout::Column,
233        },
234        gap: n.gap,
235        padding: four(&n.padding),
236        align: unalign(n.align),
237        justify: unalign(n.justify),
238        text: match &n.text {
239            Some(t) => Some(TextStyle {
240                font: if t.font.is_empty() {
241                    None
242                } else {
243                    Some(font_id(&t.font)?)
244                },
245                content: t.content.clone(),
246                size: t.size,
247                weight: t.weight as u16,
248                color: color(t.color),
249                line_height: t.line_height,
250                align: match proto::TextAlign::try_from(t.align).unwrap_or(proto::TextAlign::Left) {
251                    proto::TextAlign::Left => TextAlign::Left,
252                    proto::TextAlign::Center => TextAlign::Center,
253                    proto::TextAlign::Right => TextAlign::Right,
254                },
255            }),
256            None => None,
257        },
258        vector: n.vector.as_ref().map(|v| Vector {
259            d: v.d.clone(),
260            view_box: four(&v.view_box),
261        }),
262        image: match &n.image {
263            Some(i) => Some(Image {
264                src: i.src.clone(),
265                id: image_id(&i.id)?,
266                at: Origin::default(),
267            }),
268            None => None,
269        },
270        children: Children::from_vec(n.children.iter().map(decode).collect::<Result<_, _>>()?),
271    })
272}
273
274/// A whole scene for the wire: its tree and every image file it shows.
275pub fn scene(scene: &Scene) -> proto::Scene {
276    proto::Scene {
277        root: Some(encode(&scene.root)),
278        images: scene.images.values().map(|f| file(f)).collect(),
279        fonts: scene.fonts.values().map(|f| font_file(f)).collect(),
280        themed: scene.themed,
281    }
282}
283
284fn font_file(f: &FontFile) -> proto::FontFile {
285    proto::FontFile {
286        id: f.id.0.to_vec(),
287        data: f.bytes.to_vec(),
288    }
289}
290
291fn file(f: &ImageFile) -> proto::ImageFile {
292    proto::ImageFile {
293        id: f.id.0.to_vec(),
294        data: f.bytes.clone(),
295    }
296}
297
298/// The image files `files` carries, decoded — each taken from `held` instead
299/// when it is already there, so a file is decoded once however often it is
300/// sent.
301fn files(files: &[proto::ImageFile], held: &ImageFiles) -> Result<ImageFiles, String> {
302    let mut out = ImageFiles::new();
303    for f in files {
304        let id = image_id(&f.id)?;
305        let decoded = match held.get(&id) {
306            Some(known) => known.clone(),
307            // The server decoded the same bytes, so they decode here too.
308            None => Arc::new(ImageFile::with_id(id, f.data.clone())?),
309        };
310        out.insert(id, decoded);
311    }
312    Ok(out)
313}
314
315/// The font files `files` carries, checked — each taken from `held` instead
316/// when it is already there, so a file is checked once however often it is
317/// sent.
318fn font_files(files: &[proto::FontFile], held: &FontFiles) -> Result<FontFiles, String> {
319    let mut out = FontFiles::new();
320    for f in files {
321        let id = font_id(&f.id)?;
322        let checked = match held.get(&id) {
323            Some(known) => known.clone(),
324            // The server checked the same bytes, so they pass here too.
325            None => Arc::new(
326                FontFile::with_id(id, f.data.clone()).map_err(|e| format!("a font file {e}"))?,
327            ),
328        };
329        out.insert(id, checked);
330    }
331    Ok(out)
332}
333
334/// Keeps only the image and font files `root` uses, and says if it uses one
335/// that isn't there.
336fn settle(root: &Node, images: &mut ImageFiles, fonts: &mut FontFiles) -> Result<(), String> {
337    fn used(n: &Node, images: &mut BTreeSet<ImageId>, fonts: &mut BTreeSet<FontId>) {
338        if let Some(i) = &n.image {
339            images.insert(i.id);
340        }
341        if let Some(font) = n.text.as_ref().and_then(|t| t.font) {
342            fonts.insert(font);
343        }
344        for c in &n.children {
345            used(c, images, fonts);
346        }
347    }
348    let (mut image_ids, mut font_ids) = (BTreeSet::new(), BTreeSet::new());
349    used(root, &mut image_ids, &mut font_ids);
350    if let Some(missing) = image_ids.iter().find(|id| !images.contains_key(id)) {
351        return Err(format!("the stream never sent the image file {missing:?}"));
352    }
353    if let Some(missing) = font_ids.iter().find(|id| !fonts.contains_key(id)) {
354        return Err(format!("the stream never sent the font file {missing:?}"));
355    }
356    images.retain(|id, _| image_ids.contains(id));
357    fonts.retain(|id, _| font_ids.contains(id));
358    Ok(())
359}
360
361/// The scene a stream message carries; image and font files `held` already
362/// has aren't decoded or checked again.
363pub fn decode_scene(
364    message: &proto::Scene,
365    held: (&ImageFiles, &FontFiles),
366) -> Result<Scene, String> {
367    let root = decode(message.root.as_ref().ok_or("a scene with no root")?)?;
368    let mut images = files(&message.images, held.0)?;
369    let mut fonts = font_files(&message.fonts, held.1)?;
370    settle(&root, &mut images, &mut fonts)?;
371    let scene = Scene::new(root, images, fonts, message.themed);
372    if scene.ids.len() != count(&scene.root) {
373        return Err("a scene with two nodes of the same id".into());
374    }
375    Ok(scene)
376}
377
378fn count(n: &Node) -> usize {
379    1 + n.children.iter().map(count).sum::<usize>()
380}
381
382mod patch;
383pub(crate) use patch::edits;
384pub use patch::{apply, patch};
385
386#[cfg(test)]
387mod tests;