Skip to main content

devpage_design/
render.rs

1//! Scene → pixels. The engine lays a scene out once (taffy, parley) and
2//! turns it into a display list of vello drawing commands; a backend draws
3//! the list — `vello_cpu` here, for the PNG.
4
5use 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, Rgba, Scene,
22    Size as DSize, TextAlign, Theme,
23};
24use crate::shadow::{self, Outline};
25
26/// Inter's ascender and descender, as fractions of the font size: how far a
27/// line's glyphs can reach above and below its baseline.
28const INTER_ASCENT: f64 = 0.969;
29const INTER_DESCENT: f64 = 0.242;
30
31/// The largest drawing a design may have, in pixels a side at 1×: far beyond
32/// any screen, and a PNG of it still fits in memory.
33pub 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
42/// The fonts and text layout state a renderer keeps between paints.
43pub struct Renderer {
44    fonts: FontContext,
45    layouts: LayoutContext<()>,
46    /// While a display list is made: the image each image file is painted
47    /// from in it, and every image it paints, in the order it first does,
48    /// with the attribute that asked for it.
49    blobs: HashMap<ImageId, ImageData>,
50    painted: Vec<(Painted, Origin)>,
51}
52
53/// An image a drawing paints, and what it shows: an image file, known by
54/// its id, or a shadow's pixels.
55#[derive(Clone)]
56struct Painted {
57    data: ImageData,
58    file: Option<ImageId>,
59}
60
61impl Painted {
62    /// Whether the two show the same pixels.
63    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/// A node's box after layout, in the scene's pixels.
82#[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
90/// What a `Draw::Fill` covers its path with.
91pub enum Paint {
92    Solid(PColor),
93    /// A gradient whose points are in scene pixels.
94    Gradient(Gradient),
95    /// An image, placed by the transform from its pixels to scene pixels.
96    Image(ImageData, Affine),
97}
98
99/// One drawing command, in scene pixels: what both vello backends replay.
100/// Every decision about the drawing is made before a command is made; a
101/// backend only translates it into its library's call.
102pub enum Draw {
103    Fill(BezPath, Paint),
104    Stroke(BezPath, f32, PColor),
105    Glyphs(FontData, f32, PColor, Vec<Glyph>),
106    PushClip(BezPath),
107    /// A layer drawn at an opacity, cut to the path — or to the whole drawing.
108    PushOpacity(f32, Option<BezPath>),
109    /// A layer added to what is under it, color and alpha alike, so two
110    /// fills in such layers that meet edge to edge sum to exactly what is in
111    /// them, with no seam where an edge pixel is partly each. Nothing in it
112    /// reaches the path, which bounds it.
113    PushAdd(BezPath),
114    Pop,
115}
116
117/// Why a scene can't be drawn: too big a drawing, or images and shadows that
118/// don't fit the GPU's atlas — `at` the attribute that broke it, when one did.
119#[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
131/// A scene laid out and ready to paint: every node's box in tree order, the
132/// design's drawing, the commands that paint it, and the images they paint.
133pub struct Drawing {
134    pub frames: Vec<Frame>,
135    pub area: Frame,
136    pub list: Vec<Draw>,
137    /// Every image the list paints, each under a blob of its own, in the
138    /// order the list first paints it: what the GPU's atlas holds for this
139    /// drawing, placed as [`crate::atlas::place`] places them.
140    pub images: Vec<ImageData>,
141    /// What each of `images` shows.
142    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    /// Lays the scene out: every node's box, in tree order.
191    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        // A frame with no layout places its children absolutely, so flexbox
204        // gives a hugging one no size; it takes its children's extent
205        // instead. `hugs` is in post-order, so a pass settles every frame
206        // whose children are settled, and each pass settles at least one
207        // more level — so `hugs.len()` passes after the first always suffice.
208        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                    // A vector or an image hugging keeps its own aspect.
258                    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    /// The scene as drawing commands — laid out once, shaped once — and the
297    /// area they cover: the design's **drawing**, its root's box plus
298    /// anything painted outside it, in whole pixels. The list is clipped to
299    /// that area, so every backend shows exactly it.
300    pub fn display_list(&mut self, scene: &Scene, theme: Theme) -> (Frame, Vec<Draw>) {
301        let frames = self.layout(scene);
302        self.list_for(scene, &frames, theme)
303    }
304
305    /// The scene laid out once and turned into its display list in `theme`
306    /// — every color its value there — or why it
307    /// can't be drawn: a drawing bigger than `MAX_SIDE` a side, or images and
308    /// shadows that don't fit the GPU's atlas together. What the PNG, the
309    /// canvas and `devpage check` all start from.
310    ///
311    /// Each image is painted under a blob of its own, made in the order the
312    /// drawing first paints it — except that a drawing made `after` the one
313    /// a canvas last showed, painting the same images in the same order,
314    /// paints that drawing's: the GPU then keeps the atlas it has, which
315    /// holds exactly them, placed as a fresh one would.
316    pub fn drawing(
317        &mut self,
318        scene: &Scene,
319        theme: Theme,
320        after: Option<&Drawing>,
321    ) -> Result<Drawing, Undrawable> {
322        let frames = self.layout(scene);
323        let (area, list) = self.list_for(scene, &frames, theme);
324        let painted = std::mem::take(&mut self.painted);
325        if area.width > MAX_SIDE || area.height > MAX_SIDE {
326            return Err(Undrawable {
327                message: format!(
328                    "the drawing is {}×{} pixels; a design draws at most {} a side",
329                    area.width, area.height, MAX_SIDE
330                ),
331                at: None,
332            });
333        }
334        let sizes: Vec<(u32, u32)> = painted
335            .iter()
336            .map(|(p, _)| (p.data.width, p.data.height))
337            .collect();
338        if let Err(n) = crate::atlas::place(&sizes) {
339            let (Painted { data, .. }, at) = &painted[n];
340            let what = if at.file.is_empty() {
341                "an image"
342            } else {
343                "this"
344            };
345            return Err(Undrawable {
346                message: format!(
347                    "{what} doesn't fit: the images a drawing paints — its image files, \
348                     at most {side} pixels a side, and its shadows — share the GPU's one \
349                     {atlas}×{atlas} atlas, and the {n} before this {w}×{h} fill it",
350                    side = resolve::MAX_IMAGE_SIDE,
351                    atlas = crate::atlas::SIDE,
352                    w = data.width,
353                    h = data.height,
354                ),
355                at: Some(at.clone()),
356            });
357        }
358        let mut list = list;
359        let mut shows: Vec<Painted> = painted.into_iter().map(|(p, _)| p).collect();
360        if let Some(last) = after
361            && last.shows.len() == shows.len()
362            && last.shows.iter().zip(&shows).all(|(a, b)| a.same(b))
363        {
364            let last_of: HashMap<u64, &ImageData> = shows
365                .iter()
366                .zip(&last.shows)
367                .map(|(new, old)| (new.data.data.id(), &old.data))
368                .collect();
369            for draw in &mut list {
370                if let Draw::Fill(_, Paint::Image(data, _)) = draw {
371                    *data = last_of[&data.data.id()].clone();
372                }
373            }
374            shows = last.shows.clone();
375        }
376        let images = shows.iter().map(|p| p.data.clone()).collect();
377        Ok(Drawing {
378            frames,
379            area,
380            list,
381            images,
382            shows,
383        })
384    }
385
386    /// The display list for a scene already laid out as `frames`.
387    fn list_for(&mut self, scene: &Scene, frames: &[Frame], theme: Theme) -> (Frame, Vec<Draw>) {
388        let root = frames[0];
389        let mut area = KRect::new(
390            root.x as f64,
391            root.y as f64,
392            (root.x + root.width) as f64,
393            (root.y + root.height) as f64,
394        );
395        let mut list = Vec::new();
396        let mut at = 0;
397        // Each drawing paints its images under blobs of its own, made in the
398        // order it first paints them: the order `atlas::place` places them in,
399        // on every machine.
400        self.blobs.clear();
401        self.painted.clear();
402        self.node(
403            scene,
404            &scene.root,
405            frames,
406            &mut at,
407            &mut list,
408            None,
409            &mut area,
410            theme,
411        );
412        self.blobs.clear();
413        // Out to whole pixels, ignoring the slivers floating point leaves on
414        // a curve's bounds.
415        const SLIVER: f64 = 0.01;
416        let area = KRect::new(
417            (area.x0 + SLIVER).floor(),
418            (area.y0 + SLIVER).floor(),
419            (area.x1 - SLIVER).ceil(),
420            (area.y1 - SLIVER).ceil(),
421        );
422        list.insert(0, Draw::PushClip(area.to_path(0.1)));
423        list.push(Draw::Pop);
424        let area = Frame {
425            x: area.x0 as f32,
426            y: area.y0 as f32,
427            width: area.width() as f32,
428            height: area.height() as f32,
429        };
430        (area, list)
431    }
432
433    /// Paints the scene's drawing with `vello_cpu` at `scale` device pixels
434    /// per scene pixel, its top-left at the drawing's. A drawing bigger than
435    /// `MAX_SIDE` a side is painted only that far.
436    pub fn paint(&mut self, scene: &Scene, theme: Theme, scale: f32) -> Pixmap {
437        let (area, list) = self.display_list(scene, theme);
438        paint_list(area, &list, scale)
439    }
440
441    /// Adds `node`'s commands to `list`, and what they cover — inside `clip`
442    /// — to `area`.
443    #[allow(clippy::too_many_arguments)]
444    fn node(
445        &mut self,
446        scene: &Scene,
447        node: &Node,
448        frames: &[Frame],
449        at: &mut usize,
450        list: &mut Vec<Draw>,
451        clip: Option<KRect>,
452        area: &mut KRect,
453        theme: Theme,
454    ) {
455        let f = frames[*at];
456        *at += 1;
457        let mut cover = |r: KRect| {
458            let r = clip.map_or(r, |c| r.intersect(c));
459            if r.width() > 0.0 && r.height() > 0.0 {
460                *area = area.union(r);
461            }
462        };
463        let opacity = node.opacity < 1.0;
464        if opacity {
465            list.push(Draw::PushOpacity(node.opacity, None));
466        }
467        let rect = KRect::new(
468            f.x as f64,
469            f.y as f64,
470            (f.x + f.width) as f64,
471            (f.y + f.height) as f64,
472        );
473        let shape = |inset: f64| -> BezPath {
474            let r = rect.inset(-inset);
475            if node.kind == Kind::Ellipse {
476                KEllipse::from_rect(r).to_path(0.1)
477            } else {
478                let [tl, tr, br, bl] = node.radius.map(|v| (v as f64 - inset).max(0.0));
479                RoundedRect::from_rect(r, (tl, tr, br, bl)).to_path(0.1)
480            }
481        };
482        if let Some(s) = &node.shadow {
483            // The box's own shape, widened by the spread — every corner's
484            // radius by it too — and moved by the offset.
485            let spread = s.spread as f64;
486            let r = rect
487                .inflate(spread, spread)
488                .with_origin((rect.x0 - spread + s.x as f64, rect.y0 - spread + s.y as f64));
489            let outline = if node.kind == Kind::Ellipse {
490                Outline::ellipse(r)
491            } else {
492                Outline::rounded(r, node.radius.map(|v| (v as f64 + spread).max(0.0)))
493            };
494            let std_dev = (s.blur as f64 / 2.0).max(0.0);
495            if r.width() > 0.0 && r.height() > 0.0 {
496                // A blur reaches three standard deviations past its shape.
497                cover(r.inflate(3.0 * std_dev, 3.0 * std_dev));
498            }
499            let mut images = Vec::new();
500            shadow::shadow(outline, std_dev, s.color.get(theme), list, &mut images);
501            self.painted.extend(
502                images
503                    .into_iter()
504                    .map(|data| (Painted { data, file: None }, s.at.clone())),
505            );
506        }
507        // A vector's fill and stroke follow its path; every other box's
508        // follow the box.
509        let vector = node.vector.as_ref().map(|v| {
510            let [vx, vy, vw, vh] = v.view_box.map(|n| n as f64);
511            let s = (f.width as f64 / vw).min(f.height as f64 / vh);
512            let tx = f.x as f64 + (f.width as f64 - vw * s) / 2.0 - vx * s;
513            let ty = f.y as f64 + (f.height as f64 - vh * s) / 2.0 - vy * s;
514            let mut path = BezPath::from_svg(&v.d).unwrap_or_default();
515            path.apply_affine(Affine::translate((tx, ty)) * Affine::scale(s));
516            path
517        });
518        if let Some(fill) = &node.fill {
519            let paint = match fill {
520                Fill::Solid(c) => Paint::Solid(color(c.get(theme))),
521                Fill::Linear { angle, stops } => {
522                    Paint::Gradient(gradient(rect, *angle, stops, theme))
523                }
524            };
525            let path = vector.clone().unwrap_or_else(|| shape(0.0));
526            cover(path.bounding_box());
527            list.push(Draw::Fill(path, paint));
528        }
529        if let Some(image) = &node.image {
530            let file = &scene.images[&image.id];
531            let data = match self.blobs.get(&image.id) {
532                Some(data) => data.clone(),
533                None => {
534                    let data = file.image_data();
535                    self.blobs.insert(image.id, data.clone());
536                    self.painted.push((
537                        Painted {
538                            data: data.clone(),
539                            file: Some(image.id),
540                        },
541                        image.at.clone(),
542                    ));
543                    data
544                }
545            };
546            // Covering the box: the file at its own size scaled to fill it,
547            // centered, cut to its shape — drawn from its pixels, which may
548            // be fewer.
549            let (iw, ih) = (file.width as f64, file.height as f64);
550            let s = (f.width as f64 / iw).max(f.height as f64 / ih);
551            let place = Affine::translate((
552                f.x as f64 + (f.width as f64 - iw * s) / 2.0,
553                f.y as f64 + (f.height as f64 - ih * s) / 2.0,
554            )) * Affine::scale(s)
555                * Affine::scale_non_uniform(
556                    iw / file.pixels_width as f64,
557                    ih / file.pixels_height as f64,
558                );
559            cover(rect);
560            list.push(Draw::Fill(shape(0.0), Paint::Image(data, place)));
561        }
562        if let Some(c) = node.stroke {
563            let path = match &vector {
564                Some(path) => path.clone(),
565                None => shape(node.stroke_width as f64 / 2.0),
566            };
567            let half = node.stroke_width as f64 / 2.0;
568            cover(path.bounding_box().inflate(half, half));
569            list.push(Draw::Stroke(path, node.stroke_width, color(c.get(theme))));
570        }
571        if let Some(text) = &node.text {
572            let layout = self.text_layout(text, Some(f.width + 0.5));
573            for line in layout.lines() {
574                let m = line.metrics();
575                let size = text.size as f64;
576                let baseline = f.y as f64 + m.baseline as f64;
577                cover(KRect::new(
578                    f.x as f64 + m.inline_min_coord as f64,
579                    (f.y as f64 + m.block_min_coord as f64).min(baseline - INTER_ASCENT * size),
580                    f.x as f64 + m.inline_max_coord as f64,
581                    (f.y as f64 + m.block_max_coord as f64).max(baseline + INTER_DESCENT * size),
582                ));
583                for item in line.items() {
584                    if let PositionedLayoutItem::GlyphRun(run) = item {
585                        let glyphs = run
586                            .positioned_glyphs()
587                            .map(|g| Glyph {
588                                id: g.id,
589                                x: f.x + g.x,
590                                y: f.y + g.y,
591                            })
592                            .collect();
593                        list.push(Draw::Glyphs(
594                            run.run().font().clone(),
595                            run.run().font_size(),
596                            color(text.color.get(theme)),
597                            glyphs,
598                        ));
599                    }
600                }
601            }
602        }
603        let inner = if node.clip {
604            list.push(Draw::PushClip(shape(0.0)));
605            Some(clip.map_or(rect, |c| rect.intersect(c)))
606        } else {
607            clip
608        };
609        for child in &node.children {
610            self.node(scene, child, frames, at, list, inner, area, theme);
611        }
612        if node.clip {
613            list.push(Draw::Pop);
614        }
615        if opacity {
616            list.push(Draw::Pop);
617        }
618    }
619}
620
621/// Replays a display list with `vello_cpu` into a pixmap of its area at
622/// `scale`, as `Renderer::paint` does.
623pub fn paint_list(area: Frame, list: &[Draw], scale: f32) -> Pixmap {
624    let side = |v: f32| ((v.min(MAX_SIDE) * scale).ceil().clamp(1.0, u16::MAX as f32)) as u16;
625    let (w, h) = (side(area.width), side(area.height));
626    let mut ctx = RenderContext::new(w, h);
627    let mut resources = Resources::default();
628    // Each image once, however many times it is painted.
629    let mut sources: HashMap<u64, vello_cpu::ImageSource> = HashMap::new();
630    ctx.set_transform(
631        Affine::scale(scale as f64) * Affine::translate((-area.x as f64, -area.y as f64)),
632    );
633    for draw in list {
634        match draw {
635            Draw::Fill(path, paint) => {
636                match paint {
637                    Paint::Solid(c) => ctx.set_paint(*c),
638                    Paint::Gradient(g) => ctx.set_paint(g.clone()),
639                    Paint::Image(data, place) => {
640                        let image = sources
641                            .entry(data.data.id())
642                            .or_insert_with(|| vello_cpu::ImageSource::from_peniko_image_data(data))
643                            .clone();
644                        ctx.set_paint(ImageBrush {
645                            image,
646                            sampler: Default::default(),
647                        });
648                        ctx.set_paint_transform(*place);
649                    }
650                }
651                ctx.fill_path(path);
652                ctx.reset_paint_transform();
653            }
654            Draw::Stroke(path, width, c) => {
655                ctx.set_paint(*c);
656                ctx.set_stroke(vello_cpu::kurbo::Stroke::new(*width as f64));
657                ctx.stroke_path(path);
658            }
659            Draw::Glyphs(font, size, c, glyphs) => {
660                ctx.set_paint(*c);
661                let _ = ctx
662                    .glyph_run(&mut resources, font)
663                    .font_size(*size)
664                    .hint(true)
665                    .fill_glyphs(glyphs.iter().copied());
666            }
667            Draw::PushClip(path) => ctx.push_clip_layer(path),
668            Draw::PushOpacity(o, clip) => ctx.push_layer(clip.as_ref(), None, Some(*o), None, None),
669            Draw::PushAdd(clip) => ctx.push_layer(
670                Some(clip),
671                Some(BlendMode::new(Mix::Normal, Compose::Plus)),
672                None,
673                None,
674                None,
675            ),
676            Draw::Pop => ctx.pop_layer(),
677        }
678    }
679    ctx.flush();
680    let mut pixmap = Pixmap::new(w, h);
681    ctx.render(&mut pixmap, &mut resources);
682    pixmap
683}
684
685/// What a leaf of the layout tree measures by: a text, or a vector's or an
686/// image's own size.
687enum Leaf {
688    Text(usize),
689    Intrinsic(f32, f32),
690}
691
692fn color(c: Rgba) -> PColor {
693    PColor::from_rgba8(c.r, c.g, c.b, c.a)
694}
695
696/// A CSS linear gradient over `rect`: 0° points up, 90° right; the line runs
697/// through the center, long enough that its ends touch the far corners.
698/// Its stops take their colors in `theme`.
699fn gradient(rect: KRect, angle: f32, stops: &[(Color, f32)], theme: Theme) -> Gradient {
700    let a = (angle as f64).to_radians();
701    let dir = Vec2::new(a.sin(), -a.cos());
702    let half = (rect.width() * a.sin().abs() + rect.height() * a.cos().abs()) / 2.0;
703    let center = rect.center();
704    let stops: Vec<(f32, PColor)> = stops
705        .iter()
706        .map(|(c, at)| (*at, color(c.get(theme))))
707        .collect();
708    Gradient::new_linear(center - dir * half, center + dir * half).with_stops(stops.as_slice())
709}
710
711/// A PNG's or JPEG's pixels, straight RGBA.
712/// How far apart two drawings of the same scene may be. Two rasterizers of one
713/// display list differ only in how they anti-alias an edge: a pixel on an edge
714/// takes some value between the colors either side of it, and each rasterizer
715/// picks a slightly different one. So a pixel is **unexplained** when, in some
716/// channel, its value lies more than [`EDGE`] outside the range of the other
717/// drawing's pixels around it (itself and its eight neighbours), in either
718/// direction. A missing or moved part — a divider, an icon, a word — leaves
719/// pixels no neighbour in the other drawing explains. At most [`SHARE`] of the
720/// pixels may be unexplained, and no 8-connected cluster of them may be larger
721/// than [`CLUSTER`].
722pub const EDGE: u8 = 8;
723/// The largest share of pixels that may be unexplained.
724pub const SHARE: f64 = 0.0005;
725/// The largest cluster of touching unexplained pixels.
726pub const CLUSTER: usize = 4;
727
728/// How two drawings of the same size differ.
729#[derive(Debug, Clone, Copy, PartialEq)]
730pub struct Difference {
731    /// The share of pixels that are unexplained.
732    pub share: f64,
733    /// The size of the largest 8-connected cluster of unexplained pixels.
734    pub cluster: usize,
735    /// The largest difference in any channel between the same pixel of each.
736    pub max: u8,
737}
738
739impl Difference {
740    /// Whether this is only the anti-aliasing two rasterizers may differ by.
741    pub fn within_tolerance(&self) -> bool {
742        self.share <= SHARE && self.cluster <= CLUSTER
743    }
744}
745
746/// How two RGBA buffers `width` pixels wide differ; `None` when their sizes
747/// differ.
748pub fn compare(a: &[u8], b: &[u8], width: usize) -> Option<Difference> {
749    if a.len() != b.len() || width == 0 || !a.len().is_multiple_of(4 * width) {
750        return None;
751    }
752    let count = a.len() / 4;
753    let height = count / width;
754    let neighbours = |i: usize| {
755        let (x, y) = (i % width, i / width);
756        let xs = x.saturating_sub(1)..=(x + 1).min(width - 1);
757        (y.saturating_sub(1)..=(y + 1).min(height - 1))
758            .flat_map(move |ny| xs.clone().map(move |nx| ny * width + nx))
759    };
760    // Whether pixel `i` of `p` lies outside what `q` has around it.
761    let outside = |p: &[u8], q: &[u8], i: usize| {
762        (0..4).any(|c| {
763            let v = p[4 * i + c];
764            let (lo, hi) = neighbours(i).fold((u8::MAX, 0), |(lo, hi), n| {
765                (lo.min(q[4 * n + c]), hi.max(q[4 * n + c]))
766            });
767            v.saturating_add(EDGE) < lo || v > hi.saturating_add(EDGE)
768        })
769    };
770    let max = a
771        .iter()
772        .zip(b)
773        .map(|(x, y)| x.abs_diff(*y))
774        .max()
775        .unwrap_or(0);
776    let unexplained: Vec<bool> = (0..count)
777        .map(|i| outside(a, b, i) || outside(b, a, i))
778        .collect();
779    let mut seen = vec![false; count];
780    let (mut total, mut cluster) = (0, 0);
781    let mut stack = Vec::new();
782    for start in 0..count {
783        if !unexplained[start] || seen[start] {
784            continue;
785        }
786        seen[start] = true;
787        stack.push(start);
788        let mut size = 0;
789        while let Some(i) = stack.pop() {
790            size += 1;
791            for n in neighbours(i) {
792                if unexplained[n] && !seen[n] {
793                    seen[n] = true;
794                    stack.push(n);
795                }
796            }
797        }
798        total += size;
799        cluster = cluster.max(size);
800    }
801    Some(Difference {
802        share: total as f64 / count.max(1) as f64,
803        cluster,
804        max,
805    })
806}
807
808/// Whether two RGBA buffers `width` pixels wide are the same drawing within
809/// the tolerance; otherwise how they differ, or `None` for different sizes.
810pub fn matches(a: &[u8], b: &[u8], width: usize) -> Result<(), Option<Difference>> {
811    match compare(a, b, width) {
812        Some(d) if d.within_tolerance() => Ok(()),
813        other => Err(other),
814    }
815}
816
817/// A PNG's or JPEG's pixels as straight RGBA, with its width and height —
818/// for comparing drawings.
819pub fn pixels(data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
820    let image = resolve::decode(data).ok()?;
821    Some((image.data.data().to_vec(), image.width, image.height))
822}
823
824fn build(
825    tree: &mut TaffyTree<Option<Leaf>>,
826    node: &Node,
827    parent: Option<&Node>,
828    texts: &mut Vec<(resolve::TextStyle, bool)>,
829    hugs: &mut Vec<(NodeId, bool, bool)>,
830    images: &ImageFiles,
831) -> NodeId {
832    let mut style = Style::default();
833    let dir = parent.map(|p| p.layout);
834    let dim = |s: DSize| match s {
835        DSize::Px(v) => Dimension::length(v),
836        _ => Dimension::auto(),
837    };
838    style.size = Size {
839        width: dim(node.width),
840        height: dim(node.height),
841    };
842    let (main, cross) = match dir {
843        Some(Layout::Row) => (node.width, node.height),
844        Some(Layout::Column) => (node.height, node.width),
845        _ => (DSize::Hug, DSize::Hug),
846    };
847    if main == DSize::Fill {
848        style.flex_grow = 1.0;
849        style.flex_basis = Dimension::length(0.0);
850    }
851    if cross == DSize::Fill {
852        style.align_self = Some(AlignSelf::STRETCH);
853    }
854    if dir == Some(Layout::None) {
855        style.position = Position::Absolute;
856        style.inset = taffy::Rect {
857            left: LengthPercentageAuto::length(node.x),
858            top: LengthPercentageAuto::length(node.y),
859            right: LengthPercentageAuto::auto(),
860            bottom: LengthPercentageAuto::auto(),
861        };
862    }
863    style.flex_shrink = 0.0;
864    match node.layout {
865        Layout::Row => style.flex_direction = FlexDirection::Row,
866        Layout::Column => style.flex_direction = FlexDirection::Column,
867        Layout::None => {}
868    }
869    style.gap = Size {
870        width: LengthPercentage::length(node.gap),
871        height: LengthPercentage::length(node.gap),
872    };
873    let [t, r, b, l] = node.padding;
874    style.padding = taffy::Rect {
875        top: LengthPercentage::length(t),
876        right: LengthPercentage::length(r),
877        bottom: LengthPercentage::length(b),
878        left: LengthPercentage::length(l),
879    };
880    style.align_items = match node.align {
881        Align::Center => AlignItems::CENTER,
882        Align::End => AlignItems::FLEX_END,
883        _ => AlignItems::FLEX_START,
884    };
885    style.justify_content = match node.justify {
886        Align::Center => JustifyContent::CENTER,
887        Align::End => JustifyContent::FLEX_END,
888        Align::SpaceBetween => JustifyContent::SPACE_BETWEEN,
889        Align::Start => JustifyContent::FLEX_START,
890    };
891    if let Some(text) = &node.text {
892        let wraps = !matches!(node.width, DSize::Hug);
893        texts.push((text.clone(), wraps));
894        return tree
895            .new_leaf_with_context(style, Some(Leaf::Text(texts.len() - 1)))
896            .expect("leaf");
897    }
898    let intrinsic = match (&node.vector, &node.image) {
899        (Some(v), _) => Some((v.view_box[2], v.view_box[3])),
900        (_, Some(i)) => images.get(&i.id).map(|f| (f.width as f32, f.height as f32)),
901        _ => None,
902    };
903    if let Some((w, h)) = intrinsic {
904        return tree
905            .new_leaf_with_context(style, Some(Leaf::Intrinsic(w, h)))
906            .expect("leaf");
907    }
908    let children: Vec<NodeId> = node
909        .children
910        .iter()
911        .map(|c| build(tree, c, Some(node), texts, hugs, images))
912        .collect();
913    let id = tree.new_with_children(style, &children).expect("node");
914    // A root has no parent space to fill: on its own, `"fill"` hugs.
915    let hugs_by = |s: DSize| s == DSize::Hug || (parent.is_none() && s == DSize::Fill);
916    let (w, h) = (hugs_by(node.width), hugs_by(node.height));
917    if node.layout == Layout::None && !node.children.is_empty() && (w || h) {
918        hugs.push((id, w, h));
919    }
920    id
921}
922
923fn collect(tree: &TaffyTree<Option<Leaf>>, id: NodeId, ox: f32, oy: f32, out: &mut Vec<Frame>) {
924    let l = tree.layout(id).expect("laid out");
925    let (x, y) = (ox + l.location.x, oy + l.location.y);
926    out.push(Frame {
927        x,
928        y,
929        width: l.size.width,
930        height: l.size.height,
931    });
932    for child in tree.children(id).expect("children") {
933        collect(tree, child, x, y, out);
934    }
935}
936
937#[cfg(test)]
938mod tests {
939    use std::collections::HashMap;
940
941    use super::*;
942
943    fn scene(source: &str) -> Scene {
944        let files: HashMap<String, String> = [("a.design".to_string(), source.to_string())].into();
945        resolve::resolve(&files, "a.design").unwrap()
946    }
947
948    #[test]
949    fn auto_layout_places_children() {
950        let s = scene(
951            r#"<Frame width={200} height={100} layout="row" gap={10} padding={20}>
952  <Rect width={30} height={30} />
953  <Rect width="fill" height="fill" />
954</Frame>"#,
955        );
956        let frames = Renderer::new().layout(&s);
957        assert_eq!(
958            frames[1],
959            Frame {
960                x: 20.0,
961                y: 20.0,
962                width: 30.0,
963                height: 30.0
964            }
965        );
966        assert_eq!(
967            frames[2],
968            Frame {
969                x: 60.0,
970                y: 20.0,
971                width: 120.0,
972                height: 60.0
973            }
974        );
975    }
976
977    #[test]
978    fn text_hugs_its_content() {
979        let s =
980            scene(r#"<Frame layout="column" padding={10}><Text size={20}>Hello</Text></Frame>"#);
981        let frames = Renderer::new().layout(&s);
982        assert!(
983            frames[1].width > 40.0 && frames[1].width < 60.0,
984            "{:?}",
985            frames[1]
986        );
987        assert_eq!(frames[1].height, 28.0);
988        assert_eq!(frames[0].height, 48.0);
989    }
990
991    fn frames(source: &str) -> Vec<Frame> {
992        Renderer::new().layout(&scene(source))
993    }
994
995    fn at(x: f32, y: f32, width: f32, height: f32) -> Frame {
996        Frame {
997            x,
998            y,
999            width,
1000            height,
1001        }
1002    }
1003
1004    #[test]
1005    fn hug_takes_the_content_and_padding() {
1006        let f = frames(
1007            r#"<Frame layout="row" gap={4} padding={[2, 6]}>
1008  <Rect width={10} height={20} /><Rect width={10} height={8} />
1009</Frame>"#,
1010        );
1011        assert_eq!(f[0], at(0.0, 0.0, 36.0, 24.0));
1012        assert_eq!(f[1], at(6.0, 2.0, 10.0, 20.0));
1013        assert_eq!(f[2], at(20.0, 2.0, 10.0, 8.0));
1014    }
1015
1016    #[test]
1017    fn fill_shares_the_free_space_and_stretches_across() {
1018        let f = frames(
1019            r#"<Frame width={100} height={50} layout="column" padding={[5, 10, 15, 20]}>
1020  <Rect height={10} width="fill" /><Rect height="fill" width={10} /><Rect height="fill" width={10} />
1021</Frame>"#,
1022        );
1023        assert_eq!(f[1], at(20.0, 5.0, 70.0, 10.0));
1024        assert_eq!(f[2], at(20.0, 15.0, 10.0, 10.0));
1025        assert_eq!(f[3], at(20.0, 25.0, 10.0, 10.0));
1026    }
1027
1028    #[test]
1029    fn align_and_justify_place_children() {
1030        let row = |align: &str, justify: &str| {
1031            frames(&format!(
1032                r#"<Frame width={{100}} height={{40}} layout="row" align="{align}" justify="{justify}">
1033  <Rect width={{10}} height={{10}} /><Rect width={{20}} height={{20}} />
1034</Frame>"#
1035            ))
1036        };
1037        let f = row("center", "center");
1038        assert_eq!((f[1].x, f[1].y, f[2].x, f[2].y), (35.0, 15.0, 45.0, 10.0));
1039        let f = row("end", "end");
1040        assert_eq!((f[1].x, f[1].y, f[2].x, f[2].y), (70.0, 30.0, 80.0, 20.0));
1041        let f = row("start", "space-between");
1042        assert_eq!((f[1].x, f[1].y, f[2].x, f[2].y), (0.0, 0.0, 80.0, 0.0));
1043    }
1044
1045    #[test]
1046    fn no_layout_places_children_at_their_position() {
1047        let f = frames(
1048            r#"<Frame width={100} height={100}>
1049  <Rect x={10} y={-5} width={20} height={20} /><Rect x={50} y={60} width={5} height={5} />
1050</Frame>"#,
1051        );
1052        assert_eq!(f[1], at(10.0, -5.0, 20.0, 20.0));
1053        assert_eq!(f[2], at(50.0, 60.0, 5.0, 5.0));
1054    }
1055
1056    #[test]
1057    fn a_vector_hugs_its_view_box_and_keeps_its_aspect() {
1058        let f = frames(
1059            r#"<Frame layout="row">
1060  <Vector d="M0 0L24 12" viewBox="0 0 24 12" /><Vector d="M0 0L24 12" viewBox="0 0 24 12" height={6} />
1061</Frame>"#,
1062        );
1063        assert_eq!(f[1], at(0.0, 0.0, 24.0, 12.0));
1064        assert_eq!(f[2], at(24.0, 0.0, 12.0, 6.0));
1065    }
1066
1067    #[test]
1068    fn centered_text_sits_in_the_middle_of_its_width() {
1069        let left = frames(r#"<Frame><Text width={300}>Hi</Text></Frame>"#);
1070        let s = scene(
1071            r##"<Frame width={300} height={40} fill="#ffffff"><Text width={300} textAlign="center" color="#000000">Hi</Text></Frame>"##,
1072        );
1073        let p = Renderer::new().paint(&s, Theme::Light, 1.0);
1074        let dark = |x: u16| (0..20).any(|y| p.sample(x, y).r < 128);
1075        assert_eq!(left[1].width, 300.0);
1076        assert!((140..160).any(dark), "ink near the middle");
1077        assert!(
1078            !(0..120).any(dark) && !(180..300).any(dark),
1079            "none near the edges"
1080        );
1081    }
1082
1083    fn rgba(p: &Pixmap, x: u16, y: u16) -> [u8; 4] {
1084        let c = p.sample(x, y).to_u8_array();
1085        // Unpremultiply, to compare with the colors a design names.
1086        if c[3] == 0 {
1087            return [0; 4];
1088        }
1089        let u = |v: u8| ((v as u32 * 255 + c[3] as u32 / 2) / c[3] as u32) as u8;
1090        [u(c[0]), u(c[1]), u(c[2]), c[3]]
1091    }
1092
1093    fn close(a: [u8; 4], b: [u8; 4]) -> bool {
1094        a.iter().zip(b).all(|(x, y)| x.abs_diff(y) <= 3)
1095    }
1096
1097    #[test]
1098    fn a_gradient_runs_along_its_angle() {
1099        let s = scene(
1100            r##"<Frame width={100} height={20} fill="linear-gradient(90deg, #ff0000 0%, #0000ff 100%)" />"##,
1101        );
1102        let p = Renderer::new().paint(&s, Theme::Light, 1.0);
1103        assert!(
1104            close(rgba(&p, 0, 10), [255, 0, 0, 255]),
1105            "{:?}",
1106            rgba(&p, 0, 10)
1107        );
1108        assert!(
1109            close(rgba(&p, 99, 10), [0, 0, 255, 255]),
1110            "{:?}",
1111            rgba(&p, 99, 10)
1112        );
1113        let down = scene(
1114            r##"<Frame width={20} height={100} fill="linear-gradient(180deg, #ff0000 0%, #0000ff 100%)" />"##,
1115        );
1116        let p = Renderer::new().paint(&down, Theme::Light, 1.0);
1117        assert!(close(rgba(&p, 10, 0), [255, 0, 0, 255]));
1118        assert!(close(rgba(&p, 10, 99), [0, 0, 255, 255]));
1119    }
1120
1121    #[test]
1122    fn a_stroke_is_drawn_inside_the_box() {
1123        let s = scene(
1124            r##"<Frame width={40} height={40}><Rect x={10} y={10} width={20} height={20} stroke="#ff0000" strokeWidth={4} /></Frame>"##,
1125        );
1126        let p = Renderer::new().paint(&s, Theme::Light, 1.0);
1127        assert!(close(rgba(&p, 11, 20), [255, 0, 0, 255]));
1128        assert!(close(rgba(&p, 13, 20), [255, 0, 0, 255]));
1129        assert_eq!(rgba(&p, 9, 20)[3], 0, "nothing outside the box");
1130        assert_eq!(rgba(&p, 20, 20)[3], 0, "nothing in the middle");
1131    }
1132
1133    #[test]
1134    fn each_corner_takes_its_own_radius() {
1135        let s =
1136            scene(r##"<Frame width={40} height={40} radius={[20, 0, 0, 0]} fill="#000000" />"##);
1137        let p = Renderer::new().paint(&s, Theme::Light, 1.0);
1138        assert_eq!(rgba(&p, 1, 1)[3], 0, "top left is rounded");
1139        assert_eq!(rgba(&p, 39, 0)[3], 255, "top right is square");
1140        assert_eq!(rgba(&p, 0, 39)[3], 255, "bottom left is square");
1141    }
1142
1143    /// A shadow takes the shape of its box: every corner its own radius, each
1144    /// widened by the spread.
1145    #[test]
1146    fn a_shadow_follows_each_corner_and_the_spread() {
1147        // The shadow's box is 55..95 × 5..45: its top-left corner rounded by
1148        // 10 + 5, the others by 0 + 5.
1149        let sharp = scene(
1150            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>"##,
1151        );
1152        let p = Renderer::new().paint(&sharp, Theme::Light, 1.0);
1153        assert_eq!(rgba(&p, 57, 7)[3], 0, "top left is rounded by 15");
1154        assert_eq!(rgba(&p, 92, 7)[3], 255, "top right only by 5");
1155        assert_eq!(rgba(&p, 92, 42)[3], 255, "bottom right only by 5");
1156        assert_eq!(rgba(&p, 57, 42)[3], 255, "bottom left only by 5");
1157        assert_eq!(rgba(&p, 94, 5)[3], 0, "but rounded");
1158        // Blurred, each corner still fades by its own radius.
1159        let blurred = scene(
1160            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>"##,
1161        );
1162        let p = Renderer::new().paint(&blurred, Theme::Light, 1.0);
1163        let (tl, tr) = (rgba(&p, 63, 13)[3], rgba(&p, 86, 13)[3]);
1164        assert!(tr > 150 && tl < 100, "top left {tl}, top right {tr}");
1165        let (bl, br) = (rgba(&p, 63, 36)[3], rgba(&p, 86, 36)[3]);
1166        assert!(
1167            bl.abs_diff(tr) <= 2 && br.abs_diff(tr) <= 2,
1168            "{bl} {br} {tr}"
1169        );
1170    }
1171
1172    /// The alpha across a box's left edge, a pixel at a time, through the
1173    /// middle of a `size` square box with a black shadow blurred by `blur`.
1174    /// Only a strip around the edge is painted, so even the largest box is
1175    /// cheap to look at.
1176    fn shadow_edge(size: u32, blur: f32) -> Vec<u8> {
1177        let s = scene(&format!(
1178            r##"<Frame width={{{}}} height={{{}}}><Rect x={{40}} y={{40}} width={{{size}}} height={{{size}}} shadow={{{{ blur: {blur}, color: "#000000" }}}} /></Frame>"##,
1179            size + 80,
1180            size + 80
1181        ));
1182        let mut r = Renderer::new();
1183        let d = r.drawing(&s, Theme::Light, None).unwrap();
1184        let middle = 40.0 + size as f32 / 2.0;
1185        let strip = Frame {
1186            x: 0.0,
1187            y: middle.floor(),
1188            width: 80.0,
1189            height: 1.0,
1190        };
1191        let p = paint_list(strip, &d.list, 1.0);
1192        (0..80).map(|x| p.sample(x, 0).a).collect()
1193    }
1194
1195    /// Where a rising run of alphas crosses `level`, in pixels, between pixel
1196    /// centers.
1197    fn crossing(alpha: &[u8], level: f32) -> f32 {
1198        let i = alpha.iter().position(|&a| a as f32 >= level).unwrap();
1199        let (a, b) = (alpha[i - 1] as f32, alpha[i] as f32);
1200        i as f32 - 1.0 + (level - a) / (b - a) + 0.5
1201    }
1202
1203    /// A shadow is a true Gaussian at every allowed size: it fades to nothing
1204    /// three standard deviations out, and its edge rises from 10% to 90% over
1205    /// 2.563 standard deviations — however large the box, however slight the
1206    /// blur.
1207    #[test]
1208    fn a_shadow_edge_is_the_gaussian_at_every_size() {
1209        for size in [100, 2000, 8000, 16000] {
1210            for blur in [2.0f32, 8.0] {
1211                let sigma = blur / 2.0;
1212                let alpha = shadow_edge(size, blur);
1213                // The box's edge is at x = 40. Two and a half standard
1214                // deviations out a Gaussian edge is 0.6% (1.6 of 255), and
1215                // past that less: no step where the shadow's image ends.
1216                let reach = (39.5 - 2.5 * sigma).floor() as usize + 1;
1217                assert!(
1218                    alpha[..reach].iter().all(|&a| a <= 2),
1219                    "box {size} blur {blur}: {:?}",
1220                    &alpha[..48]
1221                );
1222                let ramp = crossing(&alpha, 0.9 * 255.0) - crossing(&alpha, 0.1 * 255.0);
1223                assert!(
1224                    (ramp / sigma - 2.563).abs() < 0.2,
1225                    "box {size} blur {blur}: 10–90% over {ramp} px, σ {sigma}: {:?}",
1226                    &alpha[..48]
1227                );
1228                let half = crossing(&alpha, 0.5 * 255.0);
1229                assert!(
1230                    (half - 40.0).abs() < 0.25,
1231                    "box {size} blur {blur}: half way at {half}"
1232                );
1233            }
1234        }
1235    }
1236
1237    #[test]
1238    fn opacity_applies_to_the_subtree_as_one() {
1239        // Two overlapping opaque squares at half opacity: where they overlap
1240        // is as light as where they don't, because the group fades as one.
1241        let s = scene(
1242            r##"<Frame width={30} height={10} fill="#ffffff"><Frame opacity={0.5}>
1243  <Rect width={20} height={10} fill="#000000" /><Rect x={10} width={20} height={10} fill="#000000" />
1244</Frame></Frame>"##,
1245        );
1246        let p = Renderer::new().paint(&s, Theme::Light, 1.0);
1247        let (alone, overlap) = (rgba(&p, 5, 5), rgba(&p, 15, 5));
1248        assert!(close(alone, overlap), "{alone:?} {overlap:?}");
1249        assert!(alone[0] > 100 && alone[0] < 155, "{alone:?}");
1250    }
1251
1252    #[test]
1253    fn clip_cuts_children_to_the_box() {
1254        let s = scene(
1255            r##"<Frame width={40} height={40}><Frame width={20} height={20} clip>
1256  <Rect x={10} y={10} width={30} height={30} fill="#000000" />
1257</Frame></Frame>"##,
1258        );
1259        let p = Renderer::new().paint(&s, Theme::Light, 1.0);
1260        assert_eq!(rgba(&p, 15, 15)[3], 255);
1261        assert_eq!(rgba(&p, 25, 25)[3], 0);
1262    }
1263
1264    #[test]
1265    fn a_vector_fills_its_path_scaled_into_its_box() {
1266        // A triangle in a 10×10 view box, drawn at 40×40.
1267        let s = scene(
1268            r##"<Frame width={40} height={40}><Vector d="M0 0L10 0L0 10Z" viewBox="0 0 10 10" width={40} height={40} fill="#000000" /></Frame>"##,
1269        );
1270        let p = Renderer::new().paint(&s, Theme::Light, 1.0);
1271        assert_eq!(rgba(&p, 5, 5)[3], 255);
1272        assert_eq!(rgba(&p, 35, 35)[3], 0);
1273    }
1274
1275    fn png_bytes(w: u32, h: u32, px: impl Fn(u32, u32) -> [u8; 4]) -> Vec<u8> {
1276        let mut out = Vec::new();
1277        let mut e = png::Encoder::new(&mut out, w, h);
1278        e.set_color(png::ColorType::Rgba);
1279        e.set_depth(png::BitDepth::Eight);
1280        let mut w_ = e.write_header().unwrap();
1281        let mut data = Vec::new();
1282        for y in 0..h {
1283            for x in 0..w {
1284                data.extend(px(x, y));
1285            }
1286        }
1287        w_.write_image_data(&data).unwrap();
1288        drop(w_);
1289        out
1290    }
1291
1292    #[test]
1293    fn an_image_covers_its_box() {
1294        // A 2×1 image, left half red and right half blue, covering a 20×20
1295        // box: scaled to 40×20 and centered, so the box shows the middle.
1296        let image = png_bytes(2, 1, |x, _| {
1297            if x == 0 {
1298                [255, 0, 0, 255]
1299            } else {
1300                [0, 0, 255, 255]
1301            }
1302        });
1303        let mut s = scene(r#"<Frame width={20} height={20} />"#);
1304        s.root.kind = Kind::Image;
1305        let file = std::sync::Arc::new(resolve::ImageFile::new(image).unwrap());
1306        s.root.image = Some(resolve::Image {
1307            src: "i.png".into(),
1308            id: file.id,
1309            at: Default::default(),
1310        });
1311        s.images.insert(file.id, file);
1312        // The image is sampled smoothly, so each side is mostly its color.
1313        let p = Renderer::new().paint(&s, Theme::Light, 1.0);
1314        let (left, right) = (rgba(&p, 1, 10), rgba(&p, 18, 10));
1315        assert!(left[0] > 200 && left[2] < 50, "{left:?}");
1316        assert!(right[2] > 200 && right[0] < 50, "{right:?}");
1317        assert_eq!((left[3], right[3]), (255, 255));
1318        assert_eq!(Renderer::new().layout(&s)[0], at(0.0, 0.0, 20.0, 20.0));
1319    }
1320
1321    #[test]
1322    fn png_and_jpeg_decode() {
1323        let png = resolve::decode(&png_bytes(3, 2, |_, _| [1, 2, 3, 4])).unwrap();
1324        assert_eq!((png.width, png.height, png.data.data().len()), (3, 2, 24));
1325        let jpeg = std::fs::read(concat!(
1326            env!("CARGO_MANIFEST_DIR"),
1327            "/../docs/designs/checkout/shirt.jpg"
1328        ))
1329        .unwrap();
1330        let jpeg = resolve::decode(&jpeg).unwrap();
1331        assert_eq!((jpeg.width, jpeg.height), (160, 160));
1332        assert_eq!(jpeg.data.data().len(), 160 * 160 * 4);
1333        assert!(resolve::decode(b"\x89PNG\r\n\x1a\nbroken").is_err());
1334    }
1335
1336    /// A drawing is made for a theme: each color its value there.
1337    #[test]
1338    fn a_drawing_takes_each_colors_value_in_its_theme() {
1339        let files: HashMap<String, String> = [
1340            (
1341                "themes.yml".to_string(),
1342                "kind: themes\nlight:\n  bg: \"#ffffff\"\n  edge: \"#ff0000\"\ndark:\n  bg: \"#000000\"\n  edge: \"#00ff00\"\n"
1343                    .to_string(),
1344            ),
1345            (
1346                "a.design".to_string(),
1347                r##"<Frame width={40} height={20} fill={theme.bg} stroke={theme.edge} strokeWidth={4}>
1348  <Rect x={10} y={8} width={20} height={4} fill="linear-gradient(90deg, theme.edge 0%, theme.edge 100%)" />
1349</Frame>"##
1350                    .to_string(),
1351            ),
1352        ]
1353        .into();
1354        let s = resolve::resolve(&files, "a.design").unwrap();
1355        let mut r = Renderer::new();
1356        for (theme, bg, edge) in [
1357            (Theme::Light, [255, 255, 255, 255], [255, 0, 0, 255]),
1358            (Theme::Dark, [0, 0, 0, 255], [0, 255, 0, 255]),
1359        ] {
1360            let p = r.paint(&s, theme, 1.0);
1361            assert_eq!(rgba(&p, 6, 6), bg, "{theme:?}");
1362            assert_eq!(rgba(&p, 1, 1), edge, "{theme:?}");
1363            assert_eq!(rgba(&p, 20, 10), edge, "{theme:?}");
1364        }
1365    }
1366
1367    #[test]
1368    fn paints_fills_and_text() {
1369        let s = scene(
1370            r##"<Frame width={100} height={40} fill="#ff0000" layout="row" padding={8}><Text color="#ffffff">Pay</Text></Frame>"##,
1371        );
1372        let pixmap = Renderer::new().paint(&s, Theme::Light, 1.0);
1373        assert_eq!((pixmap.width(), pixmap.height()), (100, 40));
1374        let p = pixmap.sample(90, 30);
1375        assert_eq!((p.r, p.g, p.b, p.a), (255, 0, 0, 255));
1376        // Some pixel of the text is lighter than the fill.
1377        assert!((8..40).any(|x| (8..28).any(|y| pixmap.sample(x, y).g > 128)));
1378    }
1379
1380    #[test]
1381    fn a_hugging_frame_with_no_layout_takes_its_childrens_extent() {
1382        let s = scene(
1383            r##"<Frame fill="#ff0000"><Rect x={10} y={5} width={100} height={50} fill="#0000ff" /></Frame>"##,
1384        );
1385        let mut r = Renderer::new();
1386        let frames = r.layout(&s);
1387        assert_eq!((frames[0].width, frames[0].height), (110.0, 55.0));
1388        // Nested: the inner frame settles first, then the outer one.
1389        let s = scene(
1390            r##"<Frame><Frame x={20} y={0}><Rect x={0} y={30} width={10} height={10} /></Frame></Frame>"##,
1391        );
1392        let frames = r.layout(&s);
1393        assert_eq!((frames[0].width, frames[0].height), (30.0, 40.0));
1394        assert_eq!((frames[1].width, frames[1].height), (10.0, 40.0));
1395        // A fixed side stays fixed; only the hugging one follows the children.
1396        let s = scene(r##"<Frame width={300}><Rect width={10} height={70} /></Frame>"##);
1397        let frames = r.layout(&s);
1398        assert_eq!((frames[0].width, frames[0].height), (300.0, 70.0));
1399    }
1400
1401    #[test]
1402    fn the_drawing_is_the_root_plus_what_is_painted_outside_it() {
1403        let mut r = Renderer::new();
1404        // Paint inside the root: the drawing is exactly the root.
1405        let s = scene(
1406            r##"<Frame width={40} height={30} radius={12} fill="#000000"><Text>Hi</Text></Frame>"##,
1407        );
1408        let (area, _) = r.display_list(&s, Theme::Light);
1409        assert_eq!(
1410            area,
1411            Frame {
1412                x: 0.0,
1413                y: 0.0,
1414                width: 40.0,
1415                height: 30.0
1416            }
1417        );
1418        assert_eq!(r.paint(&s, Theme::Light, 1.0).width(), 40);
1419        // A shadow and a child past the edge widen it; the PNG shows them.
1420        let s = scene(
1421            r##"<Frame width={40} height={30} fill="#ffffff" shadow={{ y: 4, blur: 8, color: "#000000ff" }}>
1422  <Rect x={30} y={-10} width={20} height={10} fill="#ff0000" />
1423</Frame>"##,
1424        );
1425        let (area, _) = r.display_list(&s, Theme::Light);
1426        assert_eq!(
1427            area,
1428            Frame {
1429                x: -12.0,
1430                y: -10.0,
1431                width: 64.0,
1432                height: 56.0
1433            }
1434        );
1435        let p = r.paint(&s, Theme::Light, 1.0);
1436        assert_eq!((p.width(), p.height()), (64, 56));
1437        // The red child, at (30..50, -10..0), drawn at its place in the PNG.
1438        assert!(close(rgba(&p, 12 + 45, 5), [255, 0, 0, 255]));
1439        // The shadow, below the box.
1440        assert!(rgba(&p, 12 + 20, 10 + 30 + 3)[3] > 100);
1441        // A clipping parent cuts what its children paint, and the area too.
1442        let s = scene(
1443            r##"<Frame width={40} height={30} clip><Rect x={30} width={50} height={10} fill="#ff0000" /></Frame>"##,
1444        );
1445        assert_eq!(r.display_list(&s, Theme::Light).0.width, 40.0);
1446    }
1447
1448    #[test]
1449    fn a_drawing_too_big_is_a_problem_not_a_picture() {
1450        let mut r = Renderer::new();
1451        let s = scene(r##"<Frame width={20000} height={10} fill="#000000" />"##);
1452        assert_eq!(
1453            crate::png(&mut r, &s, Theme::Light).unwrap_err(),
1454            "the drawing is 20000×10 pixels; a design draws at most 16384 a side"
1455        );
1456        // Painting for the canvas stops at the limit rather than allocating more.
1457        assert_eq!(r.paint(&s, Theme::Light, 1.0).width(), 16384);
1458    }
1459
1460    /// Files whose `.png`s are image files of the size their name says,
1461    /// `w`x`h`.png — made without decoding, their pixels never touched.
1462    struct Sized(HashMap<String, String>);
1463
1464    impl crate::Files for Sized {
1465        fn bytes(&self, path: &str) -> Option<Vec<u8>> {
1466            self.0.get(path).map(|s| s.clone().into_bytes())
1467        }
1468        fn image(&self, path: &str) -> Option<Result<std::sync::Arc<resolve::ImageFile>, String>> {
1469            let name = path.strip_suffix(".png")?;
1470            let (size, n) = name.split_once('-')?;
1471            let (w, h) = size.split_once('x')?;
1472            let (w, h): (u32, u32) = (w.parse().ok()?, h.parse().ok()?);
1473            Some(Ok(std::sync::Arc::new(resolve::ImageFile {
1474                id: resolve::ImageId::of(path.as_bytes()),
1475                bytes: n.as_bytes().to_vec(),
1476                width: w,
1477                height: h,
1478                // Zeroed pages the system hands out only once written.
1479                pixels: std::sync::Arc::new(vec![0; 4 * w as usize * h as usize]),
1480                pixels_width: w,
1481                pixels_height: h,
1482            })))
1483        }
1484    }
1485
1486    /// Every image a drawing paints shares the GPU's one atlas. Images that
1487    /// don't fit together are the design's problem, at the `src` of the first
1488    /// that doesn't — never a hole in the canvas. One file shown many times
1489    /// takes its room once.
1490    #[test]
1491    fn images_that_do_not_fit_the_atlas_are_a_problem_where_written() {
1492        let design = |files: &[&str]| {
1493            let images: String = files
1494                .iter()
1495                .map(|f| format!("\n  <Image src=\"{f}\" width={{40}} height={{40}} />"))
1496                .collect();
1497            Sized(
1498                [(
1499                    "a.design".to_string(),
1500                    format!(r#"<Frame layout="row">{images}</Frame>"#),
1501                )]
1502                .into(),
1503            )
1504        };
1505        let mut r = Renderer::new();
1506        let four = design(&[
1507            "4096x4096-a.png",
1508            "4096x4096-b.png",
1509            "4096x4096-c.png",
1510            "4096x4096-d.png",
1511        ]);
1512        let scene = resolve::resolve(&four, "a.design").unwrap();
1513        assert!(r.drawing(&scene, Theme::Light, None).is_ok());
1514        // The same four again, and again: still four images.
1515        let again = design(&[
1516            "4096x4096-a.png",
1517            "4096x4096-b.png",
1518            "4096x4096-a.png",
1519            "4096x4096-c.png",
1520            "4096x4096-d.png",
1521            "4096x4096-b.png",
1522        ]);
1523        let scene = resolve::resolve(&again, "a.design").unwrap();
1524        assert!(r.drawing(&scene, Theme::Light, None).is_ok());
1525        let six = design(&[
1526            "4096x4096-a.png",
1527            "4096x4096-b.png",
1528            "4096x4096-c.png",
1529            "4096x4096-d.png",
1530            "4096x4096-e.png",
1531            "4096x4096-f.png",
1532        ]);
1533        let scene = resolve::resolve(&six, "a.design").unwrap();
1534        let Err(e) = r.drawing(&scene, Theme::Light, None) else {
1535            panic!("six 4096-pixel squares fit")
1536        };
1537        let at = e.at.unwrap();
1538        assert_eq!(
1539            (at.file.as_str(), at.pos.line, at.pos.col),
1540            ("a.design", 6, 10)
1541        );
1542        assert_eq!(
1543            e.message,
1544            "this doesn't fit: the images a drawing paints — its image files, at most 4096 \
1545             pixels a side, and its shadows — share the GPU's one 8192×8192 atlas, and the 4 \
1546             before this 4096×4096 fill it"
1547        );
1548    }
1549
1550    /// A drawing made after another that paints the same images — files and
1551    /// shadows alike, in the same order — paints the last one's, under the
1552    /// same blobs, so the GPU keeps them in its atlas: an edit to text or
1553    /// layout uploads no image again. Any other change of images, and any
1554    /// drawing made after none, paints every image under a blob of its own.
1555    #[test]
1556    fn a_drawing_with_the_last_ones_images_paints_them_under_the_same_blobs() {
1557        let design = |text: &str, files: &[&str], blur: u32| {
1558            let images: String = files
1559                .iter()
1560                .map(|f| format!("\n  <Image src=\"{f}\" width={{40}} height={{40}} />"))
1561                .collect();
1562            let source = format!(
1563                r##"<Frame layout="row"><Text>{text}</Text>{images}
1564  <Rect width={{40}} height={{40}} shadow={{{{ blur: {blur}, color: "#000000" }}}} />
1565</Frame>"##
1566            );
1567            resolve::resolve(
1568                &Sized([("a.design".to_string(), source)].into()),
1569                "a.design",
1570            )
1571            .unwrap()
1572        };
1573        let ids = |d: &Drawing| d.images.iter().map(|i| i.data.id()).collect::<Vec<_>>();
1574        let painted = |d: &Drawing| {
1575            d.list
1576                .iter()
1577                .filter_map(|c| match c {
1578                    Draw::Fill(_, Paint::Image(i, _)) => Some(i.data.id()),
1579                    _ => None,
1580                })
1581                .collect::<std::collections::BTreeSet<_>>()
1582        };
1583        let mut r = Renderer::new();
1584        let files = ["300x200-a.png", "100x100-b.png", "300x200-a.png"];
1585        let first = r
1586            .drawing(&design("One", &files, 6), Theme::Light, None)
1587            .unwrap();
1588        // Two files, one shown twice, and the shadow's images.
1589        assert!(first.images.len() > 2);
1590        assert_eq!(painted(&first), ids(&first).into_iter().collect());
1591
1592        // New text and a new layout: the very same images.
1593        let edited = r
1594            .drawing(&design("Two words", &files, 6), Theme::Light, Some(&first))
1595            .unwrap();
1596        assert_eq!(ids(&edited), ids(&first));
1597        assert_eq!(painted(&edited), painted(&first));
1598
1599        // A drawing made after none, and drawings whose images differ — a
1600        // file, their order, a shadow — share nothing with the last.
1601        let fresh = r
1602            .drawing(&design("One", &files, 6), Theme::Light, None)
1603            .unwrap();
1604        let other = ["300x200-a.png", "100x100-c.png"];
1605        let swapped = ["100x100-b.png", "300x200-a.png"];
1606        for d in [
1607            fresh,
1608            r.drawing(&design("One", &other, 6), Theme::Light, Some(&first))
1609                .unwrap(),
1610            r.drawing(&design("One", &swapped, 6), Theme::Light, Some(&first))
1611                .unwrap(),
1612            r.drawing(&design("One", &files, 7), Theme::Light, Some(&first))
1613                .unwrap(),
1614        ] {
1615            let shared: Vec<_> = ids(&d)
1616                .into_iter()
1617                .filter(|i| ids(&first).contains(i))
1618                .collect();
1619            assert!(shared.is_empty(), "{shared:?}");
1620            assert_eq!(painted(&d), ids(&d).into_iter().collect());
1621        }
1622    }
1623
1624    #[test]
1625    fn a_root_that_fills_on_its_own_hugs() {
1626        let mut r = Renderer::new();
1627        for root in [
1628            r##"<Frame width="fill" height="fill" fill="#ff0000"><Rect width={100} height={50} /></Frame>"##,
1629            r##"<Frame width="fill" layout="row" fill="#ff0000"><Rect width={100} height={50} /></Frame>"##,
1630        ] {
1631            let d = r.drawing(&scene(root), Theme::Light, None).unwrap();
1632            assert_eq!((d.area.width, d.area.height), (100.0, 50.0), "{root}");
1633        }
1634    }
1635
1636    #[test]
1637    fn a_drawing_is_laid_out_once_and_refuses_to_be_too_big() {
1638        let mut r = Renderer::new();
1639        let s = scene(r#"<Frame layout="row" padding={4}><Rect width={10} height={10} /></Frame>"#);
1640        let d = r.drawing(&s, Theme::Light, None).unwrap();
1641        assert_eq!(d.frames, r.layout(&s));
1642        let (area, list) = r.display_list(&s, Theme::Light);
1643        assert_eq!((d.area, d.list.len()), (area, list.len()));
1644        let big = scene(r##"<Frame width={20000} height={10} fill="#000000" />"##);
1645        assert!(
1646            r.drawing(&big, Theme::Light, None)
1647                .err()
1648                .unwrap()
1649                .message
1650                .contains("at most 16384 a side")
1651        );
1652    }
1653
1654    #[test]
1655    fn no_input_makes_the_engine_panic() {
1656        let attrs = [
1657            "fill",
1658            "stroke",
1659            "strokeWidth",
1660            "radius",
1661            "shadow",
1662            "clip",
1663            "layout",
1664            "gap",
1665            "padding",
1666            "align",
1667            "justify",
1668            "width",
1669            "height",
1670            "x",
1671            "y",
1672            "opacity",
1673            "onClick",
1674            "size",
1675            "weight",
1676            "color",
1677            "lineHeight",
1678            "textAlign",
1679            "d",
1680            "viewBox",
1681            "src",
1682            "name",
1683        ];
1684        let values = [
1685            "\"#fff\u{200b}\"",
1686            "\"#ffé\"",
1687            "\"€\"",
1688            "\"\"",
1689            "\"#\"",
1690            "\"#12345\"",
1691            "\"linear-gradient(\"",
1692            "\"linear-gradient(é)\"",
1693            "\"linear-gradient(90deg, #fff é%)\"",
1694            "{-1000000}",
1695            "{1000000}",
1696            "{0}",
1697            "{-0.0001}",
1698            "{[]}",
1699            "{[1]}",
1700            "{[1,2,3]}",
1701            "{[1000000, 1000000, 1000000, 1000000]}",
1702            "{{}}",
1703            "{{ blur: 1000000, spread: 1000000 }}",
1704            "{{ x: \"é\" }}",
1705            "{true}",
1706            "\"navigate \"",
1707            "\"navigate ../../../x.design\"",
1708            "\"M\"",
1709            "\"M0 0 L1000000 1000000 Z\"",
1710            "\"0 0 0 0\"",
1711            "\"é é é é\"",
1712            "\"hug\"",
1713            "\"fill\"",
1714            "\"none\"",
1715            "\"🙂\"",
1716        ];
1717        let mut r = Renderer::new();
1718        for el in ["Frame", "Text", "Rect", "Ellipse", "Vector", "Image"] {
1719            for attr in attrs {
1720                for value in values {
1721                    let source = format!(
1722                        r#"<Frame layout="row"><{el} {attr}={value}>{}</{el}></Frame>"#,
1723                        if el == "Text" { "é 🙂" } else { "" }
1724                    );
1725                    let files: HashMap<String, String> =
1726                        [("a.design".to_string(), source.clone())].into();
1727                    if let Ok(s) = resolve::resolve(&files, "a.design") {
1728                        r.layout(&s);
1729                        let _ = crate::png(&mut r, &s, Theme::Light);
1730                    }
1731                }
1732            }
1733        }
1734        // Truncations of a real design at every character, multi-byte ones
1735        // included.
1736        let source = r##"import B from "./b.design"
1737<Frame props={{ t: "héllo 🙂" }} layout="column" fill="#fff">
1738  <Text color="#123">{t} — ok</Text><B />
1739</Frame>"##;
1740        for (i, _) in source.char_indices() {
1741            let files: HashMap<String, String> = [
1742                ("a.design".to_string(), source[..i].to_string()),
1743                ("b.design".to_string(), "<Frame />".to_string()),
1744            ]
1745            .into();
1746            let _ = resolve::resolve(&files, "a.design");
1747        }
1748    }
1749}
1750
1751#[cfg(test)]
1752mod examples {
1753    use std::path::PathBuf;
1754
1755    use super::*;
1756    use crate::Files;
1757
1758    /// The repository's files, read from disk as the server reads them.
1759    struct Repo(PathBuf);
1760
1761    impl Files for Repo {
1762        fn bytes(&self, path: &str) -> Option<Vec<u8>> {
1763            std::fs::read(self.0.join(path)).ok()
1764        }
1765    }
1766
1767    fn repo() -> Repo {
1768        Repo(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".."))
1769    }
1770
1771    /// Each example design, painted natively in each theme, matches its
1772    /// reference PNG in `design/testdata/` — `<name>.png` in the light theme,
1773    /// `<name>-dark.png` in the dark one — within the tolerance [`matches`]
1774    /// applies. After a deliberate change to
1775    /// how designs look, `DEVPAGE_UPDATE_REFERENCES=1 cargo test` rewrites
1776    /// them — and the diff shows the new drawings for review.
1777    #[test]
1778    fn the_examples_paint_as_their_references() {
1779        let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata");
1780        let update = std::env::var_os("DEVPAGE_UPDATE_REFERENCES").is_some();
1781        let mut renderer = Renderer::new();
1782        for (theme, suffix) in [(Theme::Light, ""), (Theme::Dark, "-dark")] {
1783            for name in ["button", "checkout", "confirmation", "details"] {
1784                let path = format!("docs/designs/checkout/{name}.design");
1785                let scene = crate::resolve(&repo(), &path).unwrap_or_else(|p| panic!("{p:?}"));
1786                assert!(scene.themed, "{name}");
1787                let png = crate::png(&mut renderer, &scene, theme).unwrap();
1788                let name = format!("{name}{suffix}");
1789                let reference = dir.join(format!("{name}.png"));
1790                if update {
1791                    std::fs::write(&reference, &png).unwrap();
1792                    continue;
1793                }
1794                let expected = resolve::decode(&std::fs::read(&reference).unwrap()).unwrap();
1795                let painted = resolve::decode(&png).unwrap();
1796                assert_eq!(
1797                    (painted.width, painted.height),
1798                    (expected.width, expected.height),
1799                    "{name}"
1800                );
1801                if let Err(d) = matches(
1802                    painted.data.data(),
1803                    expected.data.data(),
1804                    painted.width as usize,
1805                ) {
1806                    panic!("{name}: {d:?}");
1807                }
1808            }
1809        }
1810    }
1811
1812    /// The repository with one file's text replaced.
1813    struct Edited(Repo, &'static str, String);
1814
1815    impl Files for Edited {
1816        fn bytes(&self, path: &str) -> Option<Vec<u8>> {
1817            if path == self.1 {
1818                Some(self.2.clone().into_bytes())
1819            } else {
1820                self.0.bytes(path)
1821            }
1822        }
1823    }
1824
1825    /// Hiding one small part of the checkout design — painting it in the
1826    /// color behind it, so nothing else moves — takes it outside the
1827    /// tolerance: the comparison catches a missing part, not only a missing
1828    /// screen.
1829    #[test]
1830    fn a_missing_divider_or_icon_is_outside_the_tolerance() {
1831        let path = "docs/designs/checkout/checkout.design";
1832        let original = std::fs::read_to_string(repo().0.join(path)).unwrap();
1833        let mut renderer = Renderer::new();
1834        let mut paint = |files: &dyn Files| {
1835            let scene = crate::resolve(files, path).unwrap_or_else(|p| panic!("{p:?}"));
1836            pixels(&crate::png(&mut renderer, &scene, Theme::Light).unwrap()).unwrap()
1837        };
1838        let whole = paint(&repo());
1839        for (part, hidden) in [
1840            (
1841                r##"<Rect name="Divider" width="fill" height={1} fill={theme.border} />"##,
1842                r##"<Rect name="Divider" width="fill" height={1} fill={theme.background} />"##,
1843            ),
1844            (
1845                r##"viewBox="0 0 24 24" width={20} height={20} fill={theme.muted}"##,
1846                r##"viewBox="0 0 24 24" width={20} height={20} fill={theme.card}"##,
1847            ),
1848        ] {
1849            assert!(original.contains(part), "{part}");
1850            let edited = Edited(repo(), path, original.replace(part, hidden));
1851            let without = paint(&edited);
1852            let d = matches(&whole.0, &without.0, whole.1 as usize);
1853            assert!(d.is_err(), "hiding {part} went unnoticed: {d:?}");
1854        }
1855    }
1856
1857    #[test]
1858    fn matching_tolerates_anti_aliasing_only() {
1859        let width = 100;
1860        // A dark square on white, its edge anti-aliased to grey.
1861        let mut a = vec![255u8; 4 * width * 100];
1862        for y in 20..60 {
1863            for x in 20..60 {
1864                let edge = x == 20 || x == 59 || y == 20 || y == 59;
1865                let v = if edge { 128 } else { 0 };
1866                a[4 * (y * width + x)..][..3].copy_from_slice(&[v; 3]);
1867            }
1868        }
1869        // Another rasterizer's edge: any grey between the two sides.
1870        let mut b = a.clone();
1871        for px in b.chunks_mut(4).filter(|p| p[0] == 128) {
1872            px[..3].copy_from_slice(&[40; 3]);
1873        }
1874        let d = compare(&a, &b, width).unwrap();
1875        assert_eq!((d.share, d.max), (0.0, 88));
1876        assert!(matches(&a, &b, width).is_ok());
1877        // A missing one-pixel divider is unexplained along its whole length.
1878        let mut line = a.clone();
1879        for x in 0..width {
1880            line[4 * (80 * width + x)..][..3].copy_from_slice(&[226; 3]);
1881        }
1882        let d = matches(&a, &line, width).unwrap_err().unwrap();
1883        assert_eq!(d.cluster, width);
1884        assert_eq!(matches(&a, &a[4..], width), Err(None));
1885    }
1886}