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};
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
33fn 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
158pub 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
188pub 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
273pub 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
297fn 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 None => Arc::new(ImageFile::with_id(id, f.data.clone())?),
308 };
309 out.insert(id, decoded);
310 }
311 Ok(out)
312}
313
314fn 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 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
333fn 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
360pub 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#[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 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 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;