Skip to main content

devpage_design/wire/
mod.rs

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