1use 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
34fn 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
159pub 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
189pub 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
274pub 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
298fn 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 None => Arc::new(ImageFile::with_id(id, f.data.clone())?),
309 };
310 out.insert(id, decoded);
311 }
312 Ok(out)
313}
314
315fn 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 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
334fn 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
361pub 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;