1use parley::{
6 Alignment, AlignmentOptions, FontContext, FontFamily, FontWeight, Layout as Text,
7 LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
8};
9use std::collections::HashMap;
10use taffy::prelude::*;
11
12use vello_cpu::kurbo::{
13 Affine, BezPath, Ellipse as KEllipse, Rect as KRect, RoundedRect, Shape, Vec2,
14};
15use vello_cpu::peniko::{
16 BlendMode, Color as PColor, Compose, FontData, Gradient, ImageBrush, ImageData, Mix,
17};
18use vello_cpu::{Glyph, Pixmap, RenderContext, Resources};
19
20use crate::resolve::{
21 self, Align, Color, Fill, ImageFiles, ImageId, Kind, Layout, Node, Origin, Scene,
22 Size as DSize, TextAlign,
23};
24use crate::shadow::{self, Outline};
25
26const INTER_ASCENT: f64 = 0.969;
29const INTER_DESCENT: f64 = 0.242;
30
31pub const MAX_SIDE: f32 = 16384.0;
34
35const FONTS: [&[u8]; 4] = [
36 include_bytes!("../fonts/Inter-Regular.ttf"),
37 include_bytes!("../fonts/Inter-Medium.ttf"),
38 include_bytes!("../fonts/Inter-SemiBold.ttf"),
39 include_bytes!("../fonts/Inter-Bold.ttf"),
40];
41
42pub struct Renderer {
44 fonts: FontContext,
45 layouts: LayoutContext<()>,
46 blobs: HashMap<ImageId, ImageData>,
50 painted: Vec<(Painted, Origin)>,
51}
52
53#[derive(Clone)]
56struct Painted {
57 data: ImageData,
58 file: Option<ImageId>,
59}
60
61impl Painted {
62 fn same(&self, other: &Painted) -> bool {
64 match (self.file, other.file) {
65 (Some(a), Some(b)) => a == b,
66 (None, None) => {
67 (self.data.width, self.data.height) == (other.data.width, other.data.height)
68 && self.data.data.data() == other.data.data.data()
69 }
70 _ => false,
71 }
72 }
73}
74
75impl Default for Renderer {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq)]
83pub struct Frame {
84 pub x: f32,
85 pub y: f32,
86 pub width: f32,
87 pub height: f32,
88}
89
90pub enum Paint {
92 Solid(PColor),
93 Gradient(Gradient),
95 Image(ImageData, Affine),
97}
98
99pub enum Draw {
103 Fill(BezPath, Paint),
104 Stroke(BezPath, f32, PColor),
105 Glyphs(FontData, f32, PColor, Vec<Glyph>),
106 PushClip(BezPath),
107 PushOpacity(f32, Option<BezPath>),
109 PushAdd(BezPath),
114 Pop,
115}
116
117#[derive(Debug, Clone, PartialEq)]
120pub struct Undrawable {
121 pub message: String,
122 pub at: Option<Origin>,
123}
124
125impl std::fmt::Display for Undrawable {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 f.write_str(&self.message)
128 }
129}
130
131pub struct Drawing {
134 pub frames: Vec<Frame>,
135 pub area: Frame,
136 pub list: Vec<Draw>,
137 pub images: Vec<ImageData>,
141 shows: Vec<Painted>,
143}
144
145impl Renderer {
146 pub fn new() -> Renderer {
147 let mut fonts = FontContext {
148 collection: parley::fontique::Collection::new(parley::fontique::CollectionOptions {
149 shared: false,
150 system_fonts: false,
151 }),
152 source_cache: parley::fontique::SourceCache::default(),
153 };
154 for font in FONTS {
155 fonts
156 .collection
157 .register_fonts(parley::fontique::Blob::new(std::sync::Arc::new(font)), None);
158 }
159 Renderer {
160 fonts,
161 layouts: LayoutContext::new(),
162 blobs: HashMap::new(),
163 painted: Vec::new(),
164 }
165 }
166
167 fn text_layout(&mut self, text: &resolve::TextStyle, width: Option<f32>) -> Text<()> {
168 let mut builder = self
169 .layouts
170 .ranged_builder(&mut self.fonts, &text.content, 1.0, true);
171 builder.push_default(StyleProperty::FontFamily(FontFamily::named("Inter")));
172 builder.push_default(StyleProperty::FontSize(text.size));
173 builder.push_default(StyleProperty::FontWeight(FontWeight::new(
174 text.weight as f32,
175 )));
176 builder.push_default(StyleProperty::LineHeight(LineHeight::FontSizeRelative(
177 text.line_height,
178 )));
179 let mut layout = builder.build(&text.content);
180 layout.break_all_lines(width);
181 let align = match text.align {
182 TextAlign::Left => Alignment::Left,
183 TextAlign::Center => Alignment::Center,
184 TextAlign::Right => Alignment::Right,
185 };
186 layout.align(align, AlignmentOptions::default());
187 layout
188 }
189
190 pub fn layout(&mut self, scene: &Scene) -> Vec<Frame> {
192 let mut tree: TaffyTree<Option<Leaf>> = TaffyTree::new();
193 let mut texts = Vec::new();
194 let mut hugs = Vec::new();
195 let root = build(
196 &mut tree,
197 &scene.root,
198 None,
199 &mut texts,
200 &mut hugs,
201 &scene.images,
202 );
203 for _ in 0..=hugs.len() {
209 self.compute(&mut tree, root, &texts);
210 let mut changed = false;
211 for &(id, w, h) in &hugs {
212 let (mut ew, mut eh) = (0.0f32, 0.0f32);
213 for child in tree.children(id).expect("children") {
214 let l = tree.layout(child).expect("laid out");
215 ew = ew.max(l.location.x + l.size.width);
216 eh = eh.max(l.location.y + l.size.height);
217 }
218 let mut style = tree.style(id).expect("style").clone();
219 let mut set = |d: &mut Dimension, v: f32| {
220 if *d != Dimension::length(v) {
221 *d = Dimension::length(v);
222 changed = true;
223 }
224 };
225 if w {
226 set(&mut style.size.width, ew);
227 }
228 if h {
229 set(&mut style.size.height, eh);
230 }
231 tree.set_style(id, style).expect("style");
232 }
233 if !changed {
234 break;
235 }
236 }
237 let mut frames = Vec::new();
238 collect(&tree, root, 0.0, 0.0, &mut frames);
239 frames
240 }
241
242 fn compute(
243 &mut self,
244 tree: &mut TaffyTree<Option<Leaf>>,
245 root: NodeId,
246 texts: &[(resolve::TextStyle, bool)],
247 ) {
248 tree.compute_layout_with_measure(root, Size::MAX_CONTENT, |input, _id, ctx, style| {
249 let known = input.known_dimensions;
250 let fixed = |d: Dimension| (!d.is_auto()).then(|| d.value());
251 let known = Size {
252 width: known.width.or(fixed(style.size.width)),
253 height: known.height.or(fixed(style.size.height)),
254 };
255 let size = match ctx {
256 Some(Some(Leaf::Intrinsic(w, h))) => {
257 let (w, h) = (*w, *h);
259 match (known.width, known.height) {
260 (Some(kw), None) => Size {
261 width: kw,
262 height: kw * h / w,
263 },
264 (None, Some(kh)) => Size {
265 width: kh * w / h,
266 height: kh,
267 },
268 (kw, kh) => Size {
269 width: kw.unwrap_or(w),
270 height: kh.unwrap_or(h),
271 },
272 }
273 }
274 Some(Some(Leaf::Text(key))) => {
275 let (text, wraps): &(resolve::TextStyle, bool) = &texts[*key];
276 let width = known.width.or(match input.available_space.width {
277 AvailableSpace::Definite(w) if *wraps => Some(w),
278 _ => None,
279 });
280 let layout = self.text_layout(text, width);
281 Size {
282 width: known.width.unwrap_or(layout.width().ceil()),
283 height: known.height.unwrap_or(layout.height()),
284 }
285 }
286 _ => Size {
287 width: known.width.unwrap_or(0.0),
288 height: known.height.unwrap_or(0.0),
289 },
290 };
291 taffy::LayoutOutput::from_outer_size(size)
292 })
293 .expect("layout");
294 }
295
296 pub fn display_list(&mut self, scene: &Scene) -> (Frame, Vec<Draw>) {
301 let frames = self.layout(scene);
302 self.list_for(scene, &frames)
303 }
304
305 pub fn drawing(
316 &mut self,
317 scene: &Scene,
318 after: Option<&Drawing>,
319 ) -> Result<Drawing, Undrawable> {
320 let frames = self.layout(scene);
321 let (area, list) = self.list_for(scene, &frames);
322 let painted = std::mem::take(&mut self.painted);
323 if area.width > MAX_SIDE || area.height > MAX_SIDE {
324 return Err(Undrawable {
325 message: format!(
326 "the drawing is {}×{} pixels; a design draws at most {} a side",
327 area.width, area.height, MAX_SIDE
328 ),
329 at: None,
330 });
331 }
332 let sizes: Vec<(u32, u32)> = painted
333 .iter()
334 .map(|(p, _)| (p.data.width, p.data.height))
335 .collect();
336 if let Err(n) = crate::atlas::place(&sizes) {
337 let (Painted { data, .. }, at) = &painted[n];
338 let what = if at.file.is_empty() {
339 "an image"
340 } else {
341 "this"
342 };
343 return Err(Undrawable {
344 message: format!(
345 "{what} doesn't fit: the images a drawing paints — its image files, \
346 at most {side} pixels a side, and its shadows — share the GPU's one \
347 {atlas}×{atlas} atlas, and the {n} before this {w}×{h} fill it",
348 side = resolve::MAX_IMAGE_SIDE,
349 atlas = crate::atlas::SIDE,
350 w = data.width,
351 h = data.height,
352 ),
353 at: Some(at.clone()),
354 });
355 }
356 let mut list = list;
357 let mut shows: Vec<Painted> = painted.into_iter().map(|(p, _)| p).collect();
358 if let Some(last) = after
359 && last.shows.len() == shows.len()
360 && last.shows.iter().zip(&shows).all(|(a, b)| a.same(b))
361 {
362 let last_of: HashMap<u64, &ImageData> = shows
363 .iter()
364 .zip(&last.shows)
365 .map(|(new, old)| (new.data.data.id(), &old.data))
366 .collect();
367 for draw in &mut list {
368 if let Draw::Fill(_, Paint::Image(data, _)) = draw {
369 *data = last_of[&data.data.id()].clone();
370 }
371 }
372 shows = last.shows.clone();
373 }
374 let images = shows.iter().map(|p| p.data.clone()).collect();
375 Ok(Drawing {
376 frames,
377 area,
378 list,
379 images,
380 shows,
381 })
382 }
383
384 fn list_for(&mut self, scene: &Scene, frames: &[Frame]) -> (Frame, Vec<Draw>) {
386 let root = frames[0];
387 let mut area = KRect::new(
388 root.x as f64,
389 root.y as f64,
390 (root.x + root.width) as f64,
391 (root.y + root.height) as f64,
392 );
393 let mut list = Vec::new();
394 let mut at = 0;
395 self.blobs.clear();
399 self.painted.clear();
400 self.node(
401 scene,
402 &scene.root,
403 frames,
404 &mut at,
405 &mut list,
406 None,
407 &mut area,
408 );
409 self.blobs.clear();
410 const SLIVER: f64 = 0.01;
413 let area = KRect::new(
414 (area.x0 + SLIVER).floor(),
415 (area.y0 + SLIVER).floor(),
416 (area.x1 - SLIVER).ceil(),
417 (area.y1 - SLIVER).ceil(),
418 );
419 list.insert(0, Draw::PushClip(area.to_path(0.1)));
420 list.push(Draw::Pop);
421 let area = Frame {
422 x: area.x0 as f32,
423 y: area.y0 as f32,
424 width: area.width() as f32,
425 height: area.height() as f32,
426 };
427 (area, list)
428 }
429
430 pub fn paint(&mut self, scene: &Scene, scale: f32) -> Pixmap {
434 let (area, list) = self.display_list(scene);
435 paint_list(area, &list, scale)
436 }
437
438 #[allow(clippy::too_many_arguments)]
441 fn node(
442 &mut self,
443 scene: &Scene,
444 node: &Node,
445 frames: &[Frame],
446 at: &mut usize,
447 list: &mut Vec<Draw>,
448 clip: Option<KRect>,
449 area: &mut KRect,
450 ) {
451 let f = frames[*at];
452 *at += 1;
453 let mut cover = |r: KRect| {
454 let r = clip.map_or(r, |c| r.intersect(c));
455 if r.width() > 0.0 && r.height() > 0.0 {
456 *area = area.union(r);
457 }
458 };
459 let opacity = node.opacity < 1.0;
460 if opacity {
461 list.push(Draw::PushOpacity(node.opacity, None));
462 }
463 let rect = KRect::new(
464 f.x as f64,
465 f.y as f64,
466 (f.x + f.width) as f64,
467 (f.y + f.height) as f64,
468 );
469 let shape = |inset: f64| -> BezPath {
470 let r = rect.inset(-inset);
471 if node.kind == Kind::Ellipse {
472 KEllipse::from_rect(r).to_path(0.1)
473 } else {
474 let [tl, tr, br, bl] = node.radius.map(|v| (v as f64 - inset).max(0.0));
475 RoundedRect::from_rect(r, (tl, tr, br, bl)).to_path(0.1)
476 }
477 };
478 if let Some(s) = &node.shadow {
479 let spread = s.spread as f64;
482 let r = rect
483 .inflate(spread, spread)
484 .with_origin((rect.x0 - spread + s.x as f64, rect.y0 - spread + s.y as f64));
485 let outline = if node.kind == Kind::Ellipse {
486 Outline::ellipse(r)
487 } else {
488 Outline::rounded(r, node.radius.map(|v| (v as f64 + spread).max(0.0)))
489 };
490 let std_dev = (s.blur as f64 / 2.0).max(0.0);
491 if r.width() > 0.0 && r.height() > 0.0 {
492 cover(r.inflate(3.0 * std_dev, 3.0 * std_dev));
494 }
495 let mut images = Vec::new();
496 shadow::shadow(outline, std_dev, s.color, list, &mut images);
497 self.painted.extend(
498 images
499 .into_iter()
500 .map(|data| (Painted { data, file: None }, s.at.clone())),
501 );
502 }
503 let vector = node.vector.as_ref().map(|v| {
506 let [vx, vy, vw, vh] = v.view_box.map(|n| n as f64);
507 let s = (f.width as f64 / vw).min(f.height as f64 / vh);
508 let tx = f.x as f64 + (f.width as f64 - vw * s) / 2.0 - vx * s;
509 let ty = f.y as f64 + (f.height as f64 - vh * s) / 2.0 - vy * s;
510 let mut path = BezPath::from_svg(&v.d).unwrap_or_default();
511 path.apply_affine(Affine::translate((tx, ty)) * Affine::scale(s));
512 path
513 });
514 if let Some(fill) = &node.fill {
515 let paint = match fill {
516 Fill::Solid(c) => Paint::Solid(color(*c)),
517 Fill::Linear { angle, stops } => Paint::Gradient(gradient(rect, *angle, stops)),
518 };
519 let path = vector.clone().unwrap_or_else(|| shape(0.0));
520 cover(path.bounding_box());
521 list.push(Draw::Fill(path, paint));
522 }
523 if let Some(image) = &node.image {
524 let file = &scene.images[&image.id];
525 let data = match self.blobs.get(&image.id) {
526 Some(data) => data.clone(),
527 None => {
528 let data = file.image_data();
529 self.blobs.insert(image.id, data.clone());
530 self.painted.push((
531 Painted {
532 data: data.clone(),
533 file: Some(image.id),
534 },
535 image.at.clone(),
536 ));
537 data
538 }
539 };
540 let (iw, ih) = (file.width as f64, file.height as f64);
544 let s = (f.width as f64 / iw).max(f.height as f64 / ih);
545 let place = Affine::translate((
546 f.x as f64 + (f.width as f64 - iw * s) / 2.0,
547 f.y as f64 + (f.height as f64 - ih * s) / 2.0,
548 )) * Affine::scale(s)
549 * Affine::scale_non_uniform(
550 iw / file.pixels_width as f64,
551 ih / file.pixels_height as f64,
552 );
553 cover(rect);
554 list.push(Draw::Fill(shape(0.0), Paint::Image(data, place)));
555 }
556 if let Some(c) = node.stroke {
557 let path = match &vector {
558 Some(path) => path.clone(),
559 None => shape(node.stroke_width as f64 / 2.0),
560 };
561 let half = node.stroke_width as f64 / 2.0;
562 cover(path.bounding_box().inflate(half, half));
563 list.push(Draw::Stroke(path, node.stroke_width, color(c)));
564 }
565 if let Some(text) = &node.text {
566 let layout = self.text_layout(text, Some(f.width + 0.5));
567 for line in layout.lines() {
568 let m = line.metrics();
569 let size = text.size as f64;
570 let baseline = f.y as f64 + m.baseline as f64;
571 cover(KRect::new(
572 f.x as f64 + m.inline_min_coord as f64,
573 (f.y as f64 + m.block_min_coord as f64).min(baseline - INTER_ASCENT * size),
574 f.x as f64 + m.inline_max_coord as f64,
575 (f.y as f64 + m.block_max_coord as f64).max(baseline + INTER_DESCENT * size),
576 ));
577 for item in line.items() {
578 if let PositionedLayoutItem::GlyphRun(run) = item {
579 let glyphs = run
580 .positioned_glyphs()
581 .map(|g| Glyph {
582 id: g.id,
583 x: f.x + g.x,
584 y: f.y + g.y,
585 })
586 .collect();
587 list.push(Draw::Glyphs(
588 run.run().font().clone(),
589 run.run().font_size(),
590 color(text.color),
591 glyphs,
592 ));
593 }
594 }
595 }
596 }
597 let inner = if node.clip {
598 list.push(Draw::PushClip(shape(0.0)));
599 Some(clip.map_or(rect, |c| rect.intersect(c)))
600 } else {
601 clip
602 };
603 for child in &node.children {
604 self.node(scene, child, frames, at, list, inner, area);
605 }
606 if node.clip {
607 list.push(Draw::Pop);
608 }
609 if opacity {
610 list.push(Draw::Pop);
611 }
612 }
613}
614
615pub fn paint_list(area: Frame, list: &[Draw], scale: f32) -> Pixmap {
618 let side = |v: f32| ((v.min(MAX_SIDE) * scale).ceil().clamp(1.0, u16::MAX as f32)) as u16;
619 let (w, h) = (side(area.width), side(area.height));
620 let mut ctx = RenderContext::new(w, h);
621 let mut resources = Resources::default();
622 let mut sources: HashMap<u64, vello_cpu::ImageSource> = HashMap::new();
624 ctx.set_transform(
625 Affine::scale(scale as f64) * Affine::translate((-area.x as f64, -area.y as f64)),
626 );
627 for draw in list {
628 match draw {
629 Draw::Fill(path, paint) => {
630 match paint {
631 Paint::Solid(c) => ctx.set_paint(*c),
632 Paint::Gradient(g) => ctx.set_paint(g.clone()),
633 Paint::Image(data, place) => {
634 let image = sources
635 .entry(data.data.id())
636 .or_insert_with(|| vello_cpu::ImageSource::from_peniko_image_data(data))
637 .clone();
638 ctx.set_paint(ImageBrush {
639 image,
640 sampler: Default::default(),
641 });
642 ctx.set_paint_transform(*place);
643 }
644 }
645 ctx.fill_path(path);
646 ctx.reset_paint_transform();
647 }
648 Draw::Stroke(path, width, c) => {
649 ctx.set_paint(*c);
650 ctx.set_stroke(vello_cpu::kurbo::Stroke::new(*width as f64));
651 ctx.stroke_path(path);
652 }
653 Draw::Glyphs(font, size, c, glyphs) => {
654 ctx.set_paint(*c);
655 let _ = ctx
656 .glyph_run(&mut resources, font)
657 .font_size(*size)
658 .hint(true)
659 .fill_glyphs(glyphs.iter().copied());
660 }
661 Draw::PushClip(path) => ctx.push_clip_layer(path),
662 Draw::PushOpacity(o, clip) => ctx.push_layer(clip.as_ref(), None, Some(*o), None, None),
663 Draw::PushAdd(clip) => ctx.push_layer(
664 Some(clip),
665 Some(BlendMode::new(Mix::Normal, Compose::Plus)),
666 None,
667 None,
668 None,
669 ),
670 Draw::Pop => ctx.pop_layer(),
671 }
672 }
673 ctx.flush();
674 let mut pixmap = Pixmap::new(w, h);
675 ctx.render(&mut pixmap, &mut resources);
676 pixmap
677}
678
679enum Leaf {
682 Text(usize),
683 Intrinsic(f32, f32),
684}
685
686fn color(c: Color) -> PColor {
687 PColor::from_rgba8(c.r, c.g, c.b, c.a)
688}
689
690fn gradient(rect: KRect, angle: f32, stops: &[(Color, f32)]) -> Gradient {
693 let a = (angle as f64).to_radians();
694 let dir = Vec2::new(a.sin(), -a.cos());
695 let half = (rect.width() * a.sin().abs() + rect.height() * a.cos().abs()) / 2.0;
696 let center = rect.center();
697 let stops: Vec<(f32, PColor)> = stops.iter().map(|(c, at)| (*at, color(*c))).collect();
698 Gradient::new_linear(center - dir * half, center + dir * half).with_stops(stops.as_slice())
699}
700
701pub const EDGE: u8 = 8;
713pub const SHARE: f64 = 0.0005;
715pub const CLUSTER: usize = 4;
717
718#[derive(Debug, Clone, Copy, PartialEq)]
720pub struct Difference {
721 pub share: f64,
723 pub cluster: usize,
725 pub max: u8,
727}
728
729impl Difference {
730 pub fn within_tolerance(&self) -> bool {
732 self.share <= SHARE && self.cluster <= CLUSTER
733 }
734}
735
736pub fn compare(a: &[u8], b: &[u8], width: usize) -> Option<Difference> {
739 if a.len() != b.len() || width == 0 || !a.len().is_multiple_of(4 * width) {
740 return None;
741 }
742 let count = a.len() / 4;
743 let height = count / width;
744 let neighbours = |i: usize| {
745 let (x, y) = (i % width, i / width);
746 let xs = x.saturating_sub(1)..=(x + 1).min(width - 1);
747 (y.saturating_sub(1)..=(y + 1).min(height - 1))
748 .flat_map(move |ny| xs.clone().map(move |nx| ny * width + nx))
749 };
750 let outside = |p: &[u8], q: &[u8], i: usize| {
752 (0..4).any(|c| {
753 let v = p[4 * i + c];
754 let (lo, hi) = neighbours(i).fold((u8::MAX, 0), |(lo, hi), n| {
755 (lo.min(q[4 * n + c]), hi.max(q[4 * n + c]))
756 });
757 v.saturating_add(EDGE) < lo || v > hi.saturating_add(EDGE)
758 })
759 };
760 let max = a
761 .iter()
762 .zip(b)
763 .map(|(x, y)| x.abs_diff(*y))
764 .max()
765 .unwrap_or(0);
766 let unexplained: Vec<bool> = (0..count)
767 .map(|i| outside(a, b, i) || outside(b, a, i))
768 .collect();
769 let mut seen = vec![false; count];
770 let (mut total, mut cluster) = (0, 0);
771 let mut stack = Vec::new();
772 for start in 0..count {
773 if !unexplained[start] || seen[start] {
774 continue;
775 }
776 seen[start] = true;
777 stack.push(start);
778 let mut size = 0;
779 while let Some(i) = stack.pop() {
780 size += 1;
781 for n in neighbours(i) {
782 if unexplained[n] && !seen[n] {
783 seen[n] = true;
784 stack.push(n);
785 }
786 }
787 }
788 total += size;
789 cluster = cluster.max(size);
790 }
791 Some(Difference {
792 share: total as f64 / count.max(1) as f64,
793 cluster,
794 max,
795 })
796}
797
798pub fn matches(a: &[u8], b: &[u8], width: usize) -> Result<(), Option<Difference>> {
801 match compare(a, b, width) {
802 Some(d) if d.within_tolerance() => Ok(()),
803 other => Err(other),
804 }
805}
806
807pub fn pixels(data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
810 let image = resolve::decode(data).ok()?;
811 Some((image.data.data().to_vec(), image.width, image.height))
812}
813
814fn build(
815 tree: &mut TaffyTree<Option<Leaf>>,
816 node: &Node,
817 parent: Option<&Node>,
818 texts: &mut Vec<(resolve::TextStyle, bool)>,
819 hugs: &mut Vec<(NodeId, bool, bool)>,
820 images: &ImageFiles,
821) -> NodeId {
822 let mut style = Style::default();
823 let dir = parent.map(|p| p.layout);
824 let dim = |s: DSize| match s {
825 DSize::Px(v) => Dimension::length(v),
826 _ => Dimension::auto(),
827 };
828 style.size = Size {
829 width: dim(node.width),
830 height: dim(node.height),
831 };
832 let (main, cross) = match dir {
833 Some(Layout::Row) => (node.width, node.height),
834 Some(Layout::Column) => (node.height, node.width),
835 _ => (DSize::Hug, DSize::Hug),
836 };
837 if main == DSize::Fill {
838 style.flex_grow = 1.0;
839 style.flex_basis = Dimension::length(0.0);
840 }
841 if cross == DSize::Fill {
842 style.align_self = Some(AlignSelf::STRETCH);
843 }
844 if dir == Some(Layout::None) {
845 style.position = Position::Absolute;
846 style.inset = taffy::Rect {
847 left: LengthPercentageAuto::length(node.x),
848 top: LengthPercentageAuto::length(node.y),
849 right: LengthPercentageAuto::auto(),
850 bottom: LengthPercentageAuto::auto(),
851 };
852 }
853 style.flex_shrink = 0.0;
854 match node.layout {
855 Layout::Row => style.flex_direction = FlexDirection::Row,
856 Layout::Column => style.flex_direction = FlexDirection::Column,
857 Layout::None => {}
858 }
859 style.gap = Size {
860 width: LengthPercentage::length(node.gap),
861 height: LengthPercentage::length(node.gap),
862 };
863 let [t, r, b, l] = node.padding;
864 style.padding = taffy::Rect {
865 top: LengthPercentage::length(t),
866 right: LengthPercentage::length(r),
867 bottom: LengthPercentage::length(b),
868 left: LengthPercentage::length(l),
869 };
870 style.align_items = match node.align {
871 Align::Center => AlignItems::CENTER,
872 Align::End => AlignItems::FLEX_END,
873 _ => AlignItems::FLEX_START,
874 };
875 style.justify_content = match node.justify {
876 Align::Center => JustifyContent::CENTER,
877 Align::End => JustifyContent::FLEX_END,
878 Align::SpaceBetween => JustifyContent::SPACE_BETWEEN,
879 Align::Start => JustifyContent::FLEX_START,
880 };
881 if let Some(text) = &node.text {
882 let wraps = !matches!(node.width, DSize::Hug);
883 texts.push((text.clone(), wraps));
884 return tree
885 .new_leaf_with_context(style, Some(Leaf::Text(texts.len() - 1)))
886 .expect("leaf");
887 }
888 let intrinsic = match (&node.vector, &node.image) {
889 (Some(v), _) => Some((v.view_box[2], v.view_box[3])),
890 (_, Some(i)) => images.get(&i.id).map(|f| (f.width as f32, f.height as f32)),
891 _ => None,
892 };
893 if let Some((w, h)) = intrinsic {
894 return tree
895 .new_leaf_with_context(style, Some(Leaf::Intrinsic(w, h)))
896 .expect("leaf");
897 }
898 let children: Vec<NodeId> = node
899 .children
900 .iter()
901 .map(|c| build(tree, c, Some(node), texts, hugs, images))
902 .collect();
903 let id = tree.new_with_children(style, &children).expect("node");
904 let hugs_by = |s: DSize| s == DSize::Hug || (parent.is_none() && s == DSize::Fill);
906 let (w, h) = (hugs_by(node.width), hugs_by(node.height));
907 if node.layout == Layout::None && !node.children.is_empty() && (w || h) {
908 hugs.push((id, w, h));
909 }
910 id
911}
912
913fn collect(tree: &TaffyTree<Option<Leaf>>, id: NodeId, ox: f32, oy: f32, out: &mut Vec<Frame>) {
914 let l = tree.layout(id).expect("laid out");
915 let (x, y) = (ox + l.location.x, oy + l.location.y);
916 out.push(Frame {
917 x,
918 y,
919 width: l.size.width,
920 height: l.size.height,
921 });
922 for child in tree.children(id).expect("children") {
923 collect(tree, child, x, y, out);
924 }
925}
926
927#[cfg(test)]
928mod tests {
929 use std::collections::HashMap;
930
931 use super::*;
932
933 fn scene(source: &str) -> Scene {
934 let files: HashMap<String, String> = [("a.design".to_string(), source.to_string())].into();
935 resolve::resolve(&files, "a.design").unwrap()
936 }
937
938 #[test]
939 fn auto_layout_places_children() {
940 let s = scene(
941 r#"<Frame width={200} height={100} layout="row" gap={10} padding={20}>
942 <Rect width={30} height={30} />
943 <Rect width="fill" height="fill" />
944</Frame>"#,
945 );
946 let frames = Renderer::new().layout(&s);
947 assert_eq!(
948 frames[1],
949 Frame {
950 x: 20.0,
951 y: 20.0,
952 width: 30.0,
953 height: 30.0
954 }
955 );
956 assert_eq!(
957 frames[2],
958 Frame {
959 x: 60.0,
960 y: 20.0,
961 width: 120.0,
962 height: 60.0
963 }
964 );
965 }
966
967 #[test]
968 fn text_hugs_its_content() {
969 let s =
970 scene(r#"<Frame layout="column" padding={10}><Text size={20}>Hello</Text></Frame>"#);
971 let frames = Renderer::new().layout(&s);
972 assert!(
973 frames[1].width > 40.0 && frames[1].width < 60.0,
974 "{:?}",
975 frames[1]
976 );
977 assert_eq!(frames[1].height, 28.0);
978 assert_eq!(frames[0].height, 48.0);
979 }
980
981 fn frames(source: &str) -> Vec<Frame> {
982 Renderer::new().layout(&scene(source))
983 }
984
985 fn at(x: f32, y: f32, width: f32, height: f32) -> Frame {
986 Frame {
987 x,
988 y,
989 width,
990 height,
991 }
992 }
993
994 #[test]
995 fn hug_takes_the_content_and_padding() {
996 let f = frames(
997 r#"<Frame layout="row" gap={4} padding={[2, 6]}>
998 <Rect width={10} height={20} /><Rect width={10} height={8} />
999</Frame>"#,
1000 );
1001 assert_eq!(f[0], at(0.0, 0.0, 36.0, 24.0));
1002 assert_eq!(f[1], at(6.0, 2.0, 10.0, 20.0));
1003 assert_eq!(f[2], at(20.0, 2.0, 10.0, 8.0));
1004 }
1005
1006 #[test]
1007 fn fill_shares_the_free_space_and_stretches_across() {
1008 let f = frames(
1009 r#"<Frame width={100} height={50} layout="column" padding={[5, 10, 15, 20]}>
1010 <Rect height={10} width="fill" /><Rect height="fill" width={10} /><Rect height="fill" width={10} />
1011</Frame>"#,
1012 );
1013 assert_eq!(f[1], at(20.0, 5.0, 70.0, 10.0));
1014 assert_eq!(f[2], at(20.0, 15.0, 10.0, 10.0));
1015 assert_eq!(f[3], at(20.0, 25.0, 10.0, 10.0));
1016 }
1017
1018 #[test]
1019 fn align_and_justify_place_children() {
1020 let row = |align: &str, justify: &str| {
1021 frames(&format!(
1022 r#"<Frame width={{100}} height={{40}} layout="row" align="{align}" justify="{justify}">
1023 <Rect width={{10}} height={{10}} /><Rect width={{20}} height={{20}} />
1024</Frame>"#
1025 ))
1026 };
1027 let f = row("center", "center");
1028 assert_eq!((f[1].x, f[1].y, f[2].x, f[2].y), (35.0, 15.0, 45.0, 10.0));
1029 let f = row("end", "end");
1030 assert_eq!((f[1].x, f[1].y, f[2].x, f[2].y), (70.0, 30.0, 80.0, 20.0));
1031 let f = row("start", "space-between");
1032 assert_eq!((f[1].x, f[1].y, f[2].x, f[2].y), (0.0, 0.0, 80.0, 0.0));
1033 }
1034
1035 #[test]
1036 fn no_layout_places_children_at_their_position() {
1037 let f = frames(
1038 r#"<Frame width={100} height={100}>
1039 <Rect x={10} y={-5} width={20} height={20} /><Rect x={50} y={60} width={5} height={5} />
1040</Frame>"#,
1041 );
1042 assert_eq!(f[1], at(10.0, -5.0, 20.0, 20.0));
1043 assert_eq!(f[2], at(50.0, 60.0, 5.0, 5.0));
1044 }
1045
1046 #[test]
1047 fn a_vector_hugs_its_view_box_and_keeps_its_aspect() {
1048 let f = frames(
1049 r#"<Frame layout="row">
1050 <Vector d="M0 0L24 12" viewBox="0 0 24 12" /><Vector d="M0 0L24 12" viewBox="0 0 24 12" height={6} />
1051</Frame>"#,
1052 );
1053 assert_eq!(f[1], at(0.0, 0.0, 24.0, 12.0));
1054 assert_eq!(f[2], at(24.0, 0.0, 12.0, 6.0));
1055 }
1056
1057 #[test]
1058 fn centered_text_sits_in_the_middle_of_its_width() {
1059 let left = frames(r#"<Frame><Text width={300}>Hi</Text></Frame>"#);
1060 let s = scene(
1061 r##"<Frame width={300} height={40} fill="#ffffff"><Text width={300} textAlign="center" color="#000000">Hi</Text></Frame>"##,
1062 );
1063 let p = Renderer::new().paint(&s, 1.0);
1064 let dark = |x: u16| (0..20).any(|y| p.sample(x, y).r < 128);
1065 assert_eq!(left[1].width, 300.0);
1066 assert!((140..160).any(dark), "ink near the middle");
1067 assert!(
1068 !(0..120).any(dark) && !(180..300).any(dark),
1069 "none near the edges"
1070 );
1071 }
1072
1073 fn rgba(p: &Pixmap, x: u16, y: u16) -> [u8; 4] {
1074 let c = p.sample(x, y).to_u8_array();
1075 if c[3] == 0 {
1077 return [0; 4];
1078 }
1079 let u = |v: u8| ((v as u32 * 255 + c[3] as u32 / 2) / c[3] as u32) as u8;
1080 [u(c[0]), u(c[1]), u(c[2]), c[3]]
1081 }
1082
1083 fn close(a: [u8; 4], b: [u8; 4]) -> bool {
1084 a.iter().zip(b).all(|(x, y)| x.abs_diff(y) <= 3)
1085 }
1086
1087 #[test]
1088 fn a_gradient_runs_along_its_angle() {
1089 let s = scene(
1090 r##"<Frame width={100} height={20} fill="linear-gradient(90deg, #ff0000 0%, #0000ff 100%)" />"##,
1091 );
1092 let p = Renderer::new().paint(&s, 1.0);
1093 assert!(
1094 close(rgba(&p, 0, 10), [255, 0, 0, 255]),
1095 "{:?}",
1096 rgba(&p, 0, 10)
1097 );
1098 assert!(
1099 close(rgba(&p, 99, 10), [0, 0, 255, 255]),
1100 "{:?}",
1101 rgba(&p, 99, 10)
1102 );
1103 let down = scene(
1104 r##"<Frame width={20} height={100} fill="linear-gradient(180deg, #ff0000 0%, #0000ff 100%)" />"##,
1105 );
1106 let p = Renderer::new().paint(&down, 1.0);
1107 assert!(close(rgba(&p, 10, 0), [255, 0, 0, 255]));
1108 assert!(close(rgba(&p, 10, 99), [0, 0, 255, 255]));
1109 }
1110
1111 #[test]
1112 fn a_stroke_is_drawn_inside_the_box() {
1113 let s = scene(
1114 r##"<Frame width={40} height={40}><Rect x={10} y={10} width={20} height={20} stroke="#ff0000" strokeWidth={4} /></Frame>"##,
1115 );
1116 let p = Renderer::new().paint(&s, 1.0);
1117 assert!(close(rgba(&p, 11, 20), [255, 0, 0, 255]));
1118 assert!(close(rgba(&p, 13, 20), [255, 0, 0, 255]));
1119 assert_eq!(rgba(&p, 9, 20)[3], 0, "nothing outside the box");
1120 assert_eq!(rgba(&p, 20, 20)[3], 0, "nothing in the middle");
1121 }
1122
1123 #[test]
1124 fn each_corner_takes_its_own_radius() {
1125 let s =
1126 scene(r##"<Frame width={40} height={40} radius={[20, 0, 0, 0]} fill="#000000" />"##);
1127 let p = Renderer::new().paint(&s, 1.0);
1128 assert_eq!(rgba(&p, 1, 1)[3], 0, "top left is rounded");
1129 assert_eq!(rgba(&p, 39, 0)[3], 255, "top right is square");
1130 assert_eq!(rgba(&p, 0, 39)[3], 255, "bottom left is square");
1131 }
1132
1133 #[test]
1136 fn a_shadow_follows_each_corner_and_the_spread() {
1137 let sharp = scene(
1140 r##"<Frame width={100} height={60}><Rect x={10} y={10} width={30} height={30} radius={[10, 0, 0, 0]} shadow={{ x: 50, spread: 5, color: "#000000" }} /></Frame>"##,
1141 );
1142 let p = Renderer::new().paint(&sharp, 1.0);
1143 assert_eq!(rgba(&p, 57, 7)[3], 0, "top left is rounded by 15");
1144 assert_eq!(rgba(&p, 92, 7)[3], 255, "top right only by 5");
1145 assert_eq!(rgba(&p, 92, 42)[3], 255, "bottom right only by 5");
1146 assert_eq!(rgba(&p, 57, 42)[3], 255, "bottom left only by 5");
1147 assert_eq!(rgba(&p, 94, 5)[3], 0, "but rounded");
1148 let blurred = scene(
1150 r##"<Frame width={100} height={60}><Rect x={10} y={10} width={30} height={30} radius={[20, 0, 0, 0]} shadow={{ x: 50, blur: 6, color: "#000000" }} /></Frame>"##,
1151 );
1152 let p = Renderer::new().paint(&blurred, 1.0);
1153 let (tl, tr) = (rgba(&p, 63, 13)[3], rgba(&p, 86, 13)[3]);
1154 assert!(tr > 150 && tl < 100, "top left {tl}, top right {tr}");
1155 let (bl, br) = (rgba(&p, 63, 36)[3], rgba(&p, 86, 36)[3]);
1156 assert!(
1157 bl.abs_diff(tr) <= 2 && br.abs_diff(tr) <= 2,
1158 "{bl} {br} {tr}"
1159 );
1160 }
1161
1162 fn shadow_edge(size: u32, blur: f32) -> Vec<u8> {
1167 let s = scene(&format!(
1168 r##"<Frame width={{{}}} height={{{}}}><Rect x={{40}} y={{40}} width={{{size}}} height={{{size}}} shadow={{{{ blur: {blur}, color: "#000000" }}}} /></Frame>"##,
1169 size + 80,
1170 size + 80
1171 ));
1172 let mut r = Renderer::new();
1173 let d = r.drawing(&s, None).unwrap();
1174 let middle = 40.0 + size as f32 / 2.0;
1175 let strip = Frame {
1176 x: 0.0,
1177 y: middle.floor(),
1178 width: 80.0,
1179 height: 1.0,
1180 };
1181 let p = paint_list(strip, &d.list, 1.0);
1182 (0..80).map(|x| p.sample(x, 0).a).collect()
1183 }
1184
1185 fn crossing(alpha: &[u8], level: f32) -> f32 {
1188 let i = alpha.iter().position(|&a| a as f32 >= level).unwrap();
1189 let (a, b) = (alpha[i - 1] as f32, alpha[i] as f32);
1190 i as f32 - 1.0 + (level - a) / (b - a) + 0.5
1191 }
1192
1193 #[test]
1198 fn a_shadow_edge_is_the_gaussian_at_every_size() {
1199 for size in [100, 2000, 8000, 16000] {
1200 for blur in [2.0f32, 8.0] {
1201 let sigma = blur / 2.0;
1202 let alpha = shadow_edge(size, blur);
1203 let reach = (39.5 - 2.5 * sigma).floor() as usize + 1;
1207 assert!(
1208 alpha[..reach].iter().all(|&a| a <= 2),
1209 "box {size} blur {blur}: {:?}",
1210 &alpha[..48]
1211 );
1212 let ramp = crossing(&alpha, 0.9 * 255.0) - crossing(&alpha, 0.1 * 255.0);
1213 assert!(
1214 (ramp / sigma - 2.563).abs() < 0.2,
1215 "box {size} blur {blur}: 10–90% over {ramp} px, σ {sigma}: {:?}",
1216 &alpha[..48]
1217 );
1218 let half = crossing(&alpha, 0.5 * 255.0);
1219 assert!(
1220 (half - 40.0).abs() < 0.25,
1221 "box {size} blur {blur}: half way at {half}"
1222 );
1223 }
1224 }
1225 }
1226
1227 #[test]
1228 fn opacity_applies_to_the_subtree_as_one() {
1229 let s = scene(
1232 r##"<Frame width={30} height={10} fill="#ffffff"><Frame opacity={0.5}>
1233 <Rect width={20} height={10} fill="#000000" /><Rect x={10} width={20} height={10} fill="#000000" />
1234</Frame></Frame>"##,
1235 );
1236 let p = Renderer::new().paint(&s, 1.0);
1237 let (alone, overlap) = (rgba(&p, 5, 5), rgba(&p, 15, 5));
1238 assert!(close(alone, overlap), "{alone:?} {overlap:?}");
1239 assert!(alone[0] > 100 && alone[0] < 155, "{alone:?}");
1240 }
1241
1242 #[test]
1243 fn clip_cuts_children_to_the_box() {
1244 let s = scene(
1245 r##"<Frame width={40} height={40}><Frame width={20} height={20} clip>
1246 <Rect x={10} y={10} width={30} height={30} fill="#000000" />
1247</Frame></Frame>"##,
1248 );
1249 let p = Renderer::new().paint(&s, 1.0);
1250 assert_eq!(rgba(&p, 15, 15)[3], 255);
1251 assert_eq!(rgba(&p, 25, 25)[3], 0);
1252 }
1253
1254 #[test]
1255 fn a_vector_fills_its_path_scaled_into_its_box() {
1256 let s = scene(
1258 r##"<Frame width={40} height={40}><Vector d="M0 0L10 0L0 10Z" viewBox="0 0 10 10" width={40} height={40} fill="#000000" /></Frame>"##,
1259 );
1260 let p = Renderer::new().paint(&s, 1.0);
1261 assert_eq!(rgba(&p, 5, 5)[3], 255);
1262 assert_eq!(rgba(&p, 35, 35)[3], 0);
1263 }
1264
1265 fn png_bytes(w: u32, h: u32, px: impl Fn(u32, u32) -> [u8; 4]) -> Vec<u8> {
1266 let mut out = Vec::new();
1267 let mut e = png::Encoder::new(&mut out, w, h);
1268 e.set_color(png::ColorType::Rgba);
1269 e.set_depth(png::BitDepth::Eight);
1270 let mut w_ = e.write_header().unwrap();
1271 let mut data = Vec::new();
1272 for y in 0..h {
1273 for x in 0..w {
1274 data.extend(px(x, y));
1275 }
1276 }
1277 w_.write_image_data(&data).unwrap();
1278 drop(w_);
1279 out
1280 }
1281
1282 #[test]
1283 fn an_image_covers_its_box() {
1284 let image = png_bytes(2, 1, |x, _| {
1287 if x == 0 {
1288 [255, 0, 0, 255]
1289 } else {
1290 [0, 0, 255, 255]
1291 }
1292 });
1293 let mut s = scene(r#"<Frame width={20} height={20} />"#);
1294 s.root.kind = Kind::Image;
1295 let file = std::sync::Arc::new(resolve::ImageFile::new(image).unwrap());
1296 s.root.image = Some(resolve::Image {
1297 src: "i.png".into(),
1298 id: file.id,
1299 at: Default::default(),
1300 });
1301 s.images.insert(file.id, file);
1302 let p = Renderer::new().paint(&s, 1.0);
1304 let (left, right) = (rgba(&p, 1, 10), rgba(&p, 18, 10));
1305 assert!(left[0] > 200 && left[2] < 50, "{left:?}");
1306 assert!(right[2] > 200 && right[0] < 50, "{right:?}");
1307 assert_eq!((left[3], right[3]), (255, 255));
1308 assert_eq!(Renderer::new().layout(&s)[0], at(0.0, 0.0, 20.0, 20.0));
1309 }
1310
1311 #[test]
1312 fn png_and_jpeg_decode() {
1313 let png = resolve::decode(&png_bytes(3, 2, |_, _| [1, 2, 3, 4])).unwrap();
1314 assert_eq!((png.width, png.height, png.data.data().len()), (3, 2, 24));
1315 let jpeg = std::fs::read(concat!(
1316 env!("CARGO_MANIFEST_DIR"),
1317 "/../docs/designs/checkout/shirt.jpg"
1318 ))
1319 .unwrap();
1320 let jpeg = resolve::decode(&jpeg).unwrap();
1321 assert_eq!((jpeg.width, jpeg.height), (160, 160));
1322 assert_eq!(jpeg.data.data().len(), 160 * 160 * 4);
1323 assert!(resolve::decode(b"\x89PNG\r\n\x1a\nbroken").is_err());
1324 }
1325
1326 #[test]
1327 fn paints_fills_and_text() {
1328 let s = scene(
1329 r##"<Frame width={100} height={40} fill="#ff0000" layout="row" padding={8}><Text color="#ffffff">Pay</Text></Frame>"##,
1330 );
1331 let pixmap = Renderer::new().paint(&s, 1.0);
1332 assert_eq!((pixmap.width(), pixmap.height()), (100, 40));
1333 let p = pixmap.sample(90, 30);
1334 assert_eq!((p.r, p.g, p.b, p.a), (255, 0, 0, 255));
1335 assert!((8..40).any(|x| (8..28).any(|y| pixmap.sample(x, y).g > 128)));
1337 }
1338
1339 #[test]
1340 fn a_hugging_frame_with_no_layout_takes_its_childrens_extent() {
1341 let s = scene(
1342 r##"<Frame fill="#ff0000"><Rect x={10} y={5} width={100} height={50} fill="#0000ff" /></Frame>"##,
1343 );
1344 let mut r = Renderer::new();
1345 let frames = r.layout(&s);
1346 assert_eq!((frames[0].width, frames[0].height), (110.0, 55.0));
1347 let s = scene(
1349 r##"<Frame><Frame x={20} y={0}><Rect x={0} y={30} width={10} height={10} /></Frame></Frame>"##,
1350 );
1351 let frames = r.layout(&s);
1352 assert_eq!((frames[0].width, frames[0].height), (30.0, 40.0));
1353 assert_eq!((frames[1].width, frames[1].height), (10.0, 40.0));
1354 let s = scene(r##"<Frame width={300}><Rect width={10} height={70} /></Frame>"##);
1356 let frames = r.layout(&s);
1357 assert_eq!((frames[0].width, frames[0].height), (300.0, 70.0));
1358 }
1359
1360 #[test]
1361 fn the_drawing_is_the_root_plus_what_is_painted_outside_it() {
1362 let mut r = Renderer::new();
1363 let s = scene(
1365 r##"<Frame width={40} height={30} radius={12} fill="#000000"><Text>Hi</Text></Frame>"##,
1366 );
1367 let (area, _) = r.display_list(&s);
1368 assert_eq!(
1369 area,
1370 Frame {
1371 x: 0.0,
1372 y: 0.0,
1373 width: 40.0,
1374 height: 30.0
1375 }
1376 );
1377 assert_eq!(r.paint(&s, 1.0).width(), 40);
1378 let s = scene(
1380 r##"<Frame width={40} height={30} fill="#ffffff" shadow={{ y: 4, blur: 8, color: "#000000ff" }}>
1381 <Rect x={30} y={-10} width={20} height={10} fill="#ff0000" />
1382</Frame>"##,
1383 );
1384 let (area, _) = r.display_list(&s);
1385 assert_eq!(
1386 area,
1387 Frame {
1388 x: -12.0,
1389 y: -10.0,
1390 width: 64.0,
1391 height: 56.0
1392 }
1393 );
1394 let p = r.paint(&s, 1.0);
1395 assert_eq!((p.width(), p.height()), (64, 56));
1396 assert!(close(rgba(&p, 12 + 45, 5), [255, 0, 0, 255]));
1398 assert!(rgba(&p, 12 + 20, 10 + 30 + 3)[3] > 100);
1400 let s = scene(
1402 r##"<Frame width={40} height={30} clip><Rect x={30} width={50} height={10} fill="#ff0000" /></Frame>"##,
1403 );
1404 assert_eq!(r.display_list(&s).0.width, 40.0);
1405 }
1406
1407 #[test]
1408 fn a_drawing_too_big_is_a_problem_not_a_picture() {
1409 let mut r = Renderer::new();
1410 let s = scene(r##"<Frame width={20000} height={10} fill="#000000" />"##);
1411 assert_eq!(
1412 crate::png(&mut r, &s).unwrap_err(),
1413 "the drawing is 20000×10 pixels; a design draws at most 16384 a side"
1414 );
1415 assert_eq!(r.paint(&s, 1.0).width(), 16384);
1417 }
1418
1419 struct Sized(HashMap<String, String>);
1422
1423 impl crate::Files for Sized {
1424 fn bytes(&self, path: &str) -> Option<Vec<u8>> {
1425 self.0.get(path).map(|s| s.clone().into_bytes())
1426 }
1427 fn image(&self, path: &str) -> Option<Result<std::sync::Arc<resolve::ImageFile>, String>> {
1428 let name = path.strip_suffix(".png")?;
1429 let (size, n) = name.split_once('-')?;
1430 let (w, h) = size.split_once('x')?;
1431 let (w, h): (u32, u32) = (w.parse().ok()?, h.parse().ok()?);
1432 Some(Ok(std::sync::Arc::new(resolve::ImageFile {
1433 id: resolve::ImageId::of(path.as_bytes()),
1434 bytes: n.as_bytes().to_vec(),
1435 width: w,
1436 height: h,
1437 pixels: std::sync::Arc::new(vec![0; 4 * w as usize * h as usize]),
1439 pixels_width: w,
1440 pixels_height: h,
1441 })))
1442 }
1443 }
1444
1445 #[test]
1450 fn images_that_do_not_fit_the_atlas_are_a_problem_where_written() {
1451 let design = |files: &[&str]| {
1452 let images: String = files
1453 .iter()
1454 .map(|f| format!("\n <Image src=\"{f}\" width={{40}} height={{40}} />"))
1455 .collect();
1456 Sized(
1457 [(
1458 "a.design".to_string(),
1459 format!(r#"<Frame layout="row">{images}</Frame>"#),
1460 )]
1461 .into(),
1462 )
1463 };
1464 let mut r = Renderer::new();
1465 let four = design(&[
1466 "4096x4096-a.png",
1467 "4096x4096-b.png",
1468 "4096x4096-c.png",
1469 "4096x4096-d.png",
1470 ]);
1471 let scene = resolve::resolve(&four, "a.design").unwrap();
1472 assert!(r.drawing(&scene, None).is_ok());
1473 let again = design(&[
1475 "4096x4096-a.png",
1476 "4096x4096-b.png",
1477 "4096x4096-a.png",
1478 "4096x4096-c.png",
1479 "4096x4096-d.png",
1480 "4096x4096-b.png",
1481 ]);
1482 let scene = resolve::resolve(&again, "a.design").unwrap();
1483 assert!(r.drawing(&scene, None).is_ok());
1484 let six = design(&[
1485 "4096x4096-a.png",
1486 "4096x4096-b.png",
1487 "4096x4096-c.png",
1488 "4096x4096-d.png",
1489 "4096x4096-e.png",
1490 "4096x4096-f.png",
1491 ]);
1492 let scene = resolve::resolve(&six, "a.design").unwrap();
1493 let Err(e) = r.drawing(&scene, None) else {
1494 panic!("six 4096-pixel squares fit")
1495 };
1496 let at = e.at.unwrap();
1497 assert_eq!(
1498 (at.file.as_str(), at.pos.line, at.pos.col),
1499 ("a.design", 6, 10)
1500 );
1501 assert_eq!(
1502 e.message,
1503 "this doesn't fit: the images a drawing paints — its image files, at most 4096 \
1504 pixels a side, and its shadows — share the GPU's one 8192×8192 atlas, and the 4 \
1505 before this 4096×4096 fill it"
1506 );
1507 }
1508
1509 #[test]
1515 fn a_drawing_with_the_last_ones_images_paints_them_under_the_same_blobs() {
1516 let design = |text: &str, files: &[&str], blur: u32| {
1517 let images: String = files
1518 .iter()
1519 .map(|f| format!("\n <Image src=\"{f}\" width={{40}} height={{40}} />"))
1520 .collect();
1521 let source = format!(
1522 r##"<Frame layout="row"><Text>{text}</Text>{images}
1523 <Rect width={{40}} height={{40}} shadow={{{{ blur: {blur}, color: "#000000" }}}} />
1524</Frame>"##
1525 );
1526 resolve::resolve(
1527 &Sized([("a.design".to_string(), source)].into()),
1528 "a.design",
1529 )
1530 .unwrap()
1531 };
1532 let ids = |d: &Drawing| d.images.iter().map(|i| i.data.id()).collect::<Vec<_>>();
1533 let painted = |d: &Drawing| {
1534 d.list
1535 .iter()
1536 .filter_map(|c| match c {
1537 Draw::Fill(_, Paint::Image(i, _)) => Some(i.data.id()),
1538 _ => None,
1539 })
1540 .collect::<std::collections::BTreeSet<_>>()
1541 };
1542 let mut r = Renderer::new();
1543 let files = ["300x200-a.png", "100x100-b.png", "300x200-a.png"];
1544 let first = r.drawing(&design("One", &files, 6), None).unwrap();
1545 assert!(first.images.len() > 2);
1547 assert_eq!(painted(&first), ids(&first).into_iter().collect());
1548
1549 let edited = r
1551 .drawing(&design("Two words", &files, 6), Some(&first))
1552 .unwrap();
1553 assert_eq!(ids(&edited), ids(&first));
1554 assert_eq!(painted(&edited), painted(&first));
1555
1556 let fresh = r.drawing(&design("One", &files, 6), None).unwrap();
1559 let other = ["300x200-a.png", "100x100-c.png"];
1560 let swapped = ["100x100-b.png", "300x200-a.png"];
1561 for d in [
1562 fresh,
1563 r.drawing(&design("One", &other, 6), Some(&first)).unwrap(),
1564 r.drawing(&design("One", &swapped, 6), Some(&first))
1565 .unwrap(),
1566 r.drawing(&design("One", &files, 7), Some(&first)).unwrap(),
1567 ] {
1568 let shared: Vec<_> = ids(&d)
1569 .into_iter()
1570 .filter(|i| ids(&first).contains(i))
1571 .collect();
1572 assert!(shared.is_empty(), "{shared:?}");
1573 assert_eq!(painted(&d), ids(&d).into_iter().collect());
1574 }
1575 }
1576
1577 #[test]
1578 fn a_root_that_fills_on_its_own_hugs() {
1579 let mut r = Renderer::new();
1580 for root in [
1581 r##"<Frame width="fill" height="fill" fill="#ff0000"><Rect width={100} height={50} /></Frame>"##,
1582 r##"<Frame width="fill" layout="row" fill="#ff0000"><Rect width={100} height={50} /></Frame>"##,
1583 ] {
1584 let d = r.drawing(&scene(root), None).unwrap();
1585 assert_eq!((d.area.width, d.area.height), (100.0, 50.0), "{root}");
1586 }
1587 }
1588
1589 #[test]
1590 fn a_drawing_is_laid_out_once_and_refuses_to_be_too_big() {
1591 let mut r = Renderer::new();
1592 let s = scene(r#"<Frame layout="row" padding={4}><Rect width={10} height={10} /></Frame>"#);
1593 let d = r.drawing(&s, None).unwrap();
1594 assert_eq!(d.frames, r.layout(&s));
1595 let (area, list) = r.display_list(&s);
1596 assert_eq!((d.area, d.list.len()), (area, list.len()));
1597 let big = scene(r##"<Frame width={20000} height={10} fill="#000000" />"##);
1598 assert!(
1599 r.drawing(&big, None)
1600 .err()
1601 .unwrap()
1602 .message
1603 .contains("at most 16384 a side")
1604 );
1605 }
1606
1607 #[test]
1608 fn no_input_makes_the_engine_panic() {
1609 let attrs = [
1610 "fill",
1611 "stroke",
1612 "strokeWidth",
1613 "radius",
1614 "shadow",
1615 "clip",
1616 "layout",
1617 "gap",
1618 "padding",
1619 "align",
1620 "justify",
1621 "width",
1622 "height",
1623 "x",
1624 "y",
1625 "opacity",
1626 "onClick",
1627 "size",
1628 "weight",
1629 "color",
1630 "lineHeight",
1631 "textAlign",
1632 "d",
1633 "viewBox",
1634 "src",
1635 "name",
1636 ];
1637 let values = [
1638 "\"#fff\u{200b}\"",
1639 "\"#ffé\"",
1640 "\"€\"",
1641 "\"\"",
1642 "\"#\"",
1643 "\"#12345\"",
1644 "\"linear-gradient(\"",
1645 "\"linear-gradient(é)\"",
1646 "\"linear-gradient(90deg, #fff é%)\"",
1647 "{-1000000}",
1648 "{1000000}",
1649 "{0}",
1650 "{-0.0001}",
1651 "{[]}",
1652 "{[1]}",
1653 "{[1,2,3]}",
1654 "{[1000000, 1000000, 1000000, 1000000]}",
1655 "{{}}",
1656 "{{ blur: 1000000, spread: 1000000 }}",
1657 "{{ x: \"é\" }}",
1658 "{true}",
1659 "\"navigate \"",
1660 "\"navigate ../../../x.design\"",
1661 "\"M\"",
1662 "\"M0 0 L1000000 1000000 Z\"",
1663 "\"0 0 0 0\"",
1664 "\"é é é é\"",
1665 "\"hug\"",
1666 "\"fill\"",
1667 "\"none\"",
1668 "\"🙂\"",
1669 ];
1670 let mut r = Renderer::new();
1671 for el in ["Frame", "Text", "Rect", "Ellipse", "Vector", "Image"] {
1672 for attr in attrs {
1673 for value in values {
1674 let source = format!(
1675 r#"<Frame layout="row"><{el} {attr}={value}>{}</{el}></Frame>"#,
1676 if el == "Text" { "é 🙂" } else { "" }
1677 );
1678 let files: HashMap<String, String> =
1679 [("a.design".to_string(), source.clone())].into();
1680 if let Ok(s) = resolve::resolve(&files, "a.design") {
1681 r.layout(&s);
1682 let _ = crate::png(&mut r, &s);
1683 }
1684 }
1685 }
1686 }
1687 let source = r##"import B from "./b.design"
1690<Frame props={{ t: "héllo 🙂" }} layout="column" fill="#fff">
1691 <Text color="#123">{t} — ok</Text><B />
1692</Frame>"##;
1693 for (i, _) in source.char_indices() {
1694 let files: HashMap<String, String> = [
1695 ("a.design".to_string(), source[..i].to_string()),
1696 ("b.design".to_string(), "<Frame />".to_string()),
1697 ]
1698 .into();
1699 let _ = resolve::resolve(&files, "a.design");
1700 }
1701 }
1702}
1703
1704#[cfg(test)]
1705mod examples {
1706 use std::path::PathBuf;
1707
1708 use super::*;
1709 use crate::Files;
1710
1711 struct Repo(PathBuf);
1713
1714 impl Files for Repo {
1715 fn bytes(&self, path: &str) -> Option<Vec<u8>> {
1716 std::fs::read(self.0.join(path)).ok()
1717 }
1718 }
1719
1720 fn repo() -> Repo {
1721 Repo(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".."))
1722 }
1723
1724 #[test]
1729 fn the_examples_paint_as_their_references() {
1730 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata");
1731 let update = std::env::var_os("DEVPAGE_UPDATE_REFERENCES").is_some();
1732 let mut renderer = Renderer::new();
1733 for name in ["button", "checkout", "confirmation", "details"] {
1734 let path = format!("docs/designs/checkout/{name}.design");
1735 let scene = crate::resolve(&repo(), &path).unwrap_or_else(|p| panic!("{p:?}"));
1736 let png = crate::png(&mut renderer, &scene).unwrap();
1737 let reference = dir.join(format!("{name}.png"));
1738 if update {
1739 std::fs::write(&reference, &png).unwrap();
1740 continue;
1741 }
1742 let expected = resolve::decode(&std::fs::read(&reference).unwrap()).unwrap();
1743 let painted = resolve::decode(&png).unwrap();
1744 assert_eq!(
1745 (painted.width, painted.height),
1746 (expected.width, expected.height),
1747 "{name}"
1748 );
1749 if let Err(d) = matches(
1750 painted.data.data(),
1751 expected.data.data(),
1752 painted.width as usize,
1753 ) {
1754 panic!("{name}: {d:?}");
1755 }
1756 }
1757 }
1758
1759 struct Edited(Repo, &'static str, String);
1761
1762 impl Files for Edited {
1763 fn bytes(&self, path: &str) -> Option<Vec<u8>> {
1764 if path == self.1 {
1765 Some(self.2.clone().into_bytes())
1766 } else {
1767 self.0.bytes(path)
1768 }
1769 }
1770 }
1771
1772 #[test]
1777 fn a_missing_divider_or_icon_is_outside_the_tolerance() {
1778 let path = "docs/designs/checkout/checkout.design";
1779 let original = std::fs::read_to_string(repo().0.join(path)).unwrap();
1780 let mut renderer = Renderer::new();
1781 let mut paint = |files: &dyn Files| {
1782 let scene = crate::resolve(files, path).unwrap_or_else(|p| panic!("{p:?}"));
1783 pixels(&crate::png(&mut renderer, &scene).unwrap()).unwrap()
1784 };
1785 let whole = paint(&repo());
1786 for (part, hidden) in [
1787 (
1788 r##"<Rect name="Divider" width="fill" height={1} fill="#e2e8f0" />"##,
1789 r##"<Rect name="Divider" width="fill" height={1} fill="#f8fafc" />"##,
1790 ),
1791 (
1792 r##"viewBox="0 0 24 24" width={20} height={20} fill="#64748b""##,
1793 r##"viewBox="0 0 24 24" width={20} height={20} fill="#ffffff""##,
1794 ),
1795 ] {
1796 assert!(original.contains(part), "{part}");
1797 let edited = Edited(repo(), path, original.replace(part, hidden));
1798 let without = paint(&edited);
1799 let d = matches(&whole.0, &without.0, whole.1 as usize);
1800 assert!(d.is_err(), "hiding {part} went unnoticed: {d:?}");
1801 }
1802 }
1803
1804 #[test]
1805 fn matching_tolerates_anti_aliasing_only() {
1806 let width = 100;
1807 let mut a = vec![255u8; 4 * width * 100];
1809 for y in 20..60 {
1810 for x in 20..60 {
1811 let edge = x == 20 || x == 59 || y == 20 || y == 59;
1812 let v = if edge { 128 } else { 0 };
1813 a[4 * (y * width + x)..][..3].copy_from_slice(&[v; 3]);
1814 }
1815 }
1816 let mut b = a.clone();
1818 for px in b.chunks_mut(4).filter(|p| p[0] == 128) {
1819 px[..3].copy_from_slice(&[40; 3]);
1820 }
1821 let d = compare(&a, &b, width).unwrap();
1822 assert_eq!((d.share, d.max), (0.0, 88));
1823 assert!(matches(&a, &b, width).is_ok());
1824 let mut line = a.clone();
1826 for x in 0..width {
1827 line[4 * (80 * width + x)..][..3].copy_from_slice(&[226; 3]);
1828 }
1829 let d = matches(&a, &line, width).unwrap_err().unwrap();
1830 assert_eq!(d.cluster, width);
1831 assert_eq!(matches(&a, &a[4..], width), Err(None));
1832 }
1833}