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, Scene,
22    Size as DSize, TextAlign,
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) -> (Frame, Vec<Draw>) {
301        let frames = self.layout(scene);
302        self.list_for(scene, &frames)
303    }
304
305    /// The scene laid out once and turned into its display list, or why it
306    /// can't be drawn: a drawing bigger than `MAX_SIDE` a side, or images and
307    /// shadows that don't fit the GPU's atlas together. What the PNG, the
308    /// canvas and `devpage check` all start from.
309    ///
310    /// Each image is painted under a blob of its own, made in the order the
311    /// drawing first paints it — except that a drawing made `after` the one
312    /// a canvas last showed, painting the same images in the same order,
313    /// paints that drawing's: the GPU then keeps the atlas it has, which
314    /// holds exactly them, placed as a fresh one would.
315    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    /// The display list for a scene already laid out as `frames`.
385    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        // Each drawing paints its images under blobs of its own, made in the
396        // order it first paints them: the order `atlas::place` places them in,
397        // on every machine.
398        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        // Out to whole pixels, ignoring the slivers floating point leaves on
411        // a curve's bounds.
412        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    /// Paints the scene's drawing with `vello_cpu` at `scale` device pixels
431    /// per scene pixel, its top-left at the drawing's. A drawing bigger than
432    /// `MAX_SIDE` a side is painted only that far.
433    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    /// Adds `node`'s commands to `list`, and what they cover — inside `clip`
439    /// — to `area`.
440    #[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            // The box's own shape, widened by the spread — every corner's
480            // radius by it too — and moved by the offset.
481            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                // A blur reaches three standard deviations past its shape.
493                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        // A vector's fill and stroke follow its path; every other box's
504        // follow the box.
505        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            // Covering the box: the file at its own size scaled to fill it,
541            // centered, cut to its shape — drawn from its pixels, which may
542            // be fewer.
543            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
615/// Replays a display list with `vello_cpu` into a pixmap of its area at
616/// `scale`, as `Renderer::paint` does.
617pub 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    // Each image once, however many times it is painted.
623    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
679/// What a leaf of the layout tree measures by: a text, or a vector's or an
680/// image's own size.
681enum 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
690/// A CSS linear gradient over `rect`: 0° points up, 90° right; the line runs
691/// through the center, long enough that its ends touch the far corners.
692fn 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
701/// A PNG's or JPEG's pixels, straight RGBA.
702/// How far apart two drawings of the same scene may be. Two rasterizers of one
703/// display list differ only in how they anti-alias an edge: a pixel on an edge
704/// takes some value between the colors either side of it, and each rasterizer
705/// picks a slightly different one. So a pixel is **unexplained** when, in some
706/// channel, its value lies more than [`EDGE`] outside the range of the other
707/// drawing's pixels around it (itself and its eight neighbours), in either
708/// direction. A missing or moved part — a divider, an icon, a word — leaves
709/// pixels no neighbour in the other drawing explains. At most [`SHARE`] of the
710/// pixels may be unexplained, and no 8-connected cluster of them may be larger
711/// than [`CLUSTER`].
712pub const EDGE: u8 = 8;
713/// The largest share of pixels that may be unexplained.
714pub const SHARE: f64 = 0.0005;
715/// The largest cluster of touching unexplained pixels.
716pub const CLUSTER: usize = 4;
717
718/// How two drawings of the same size differ.
719#[derive(Debug, Clone, Copy, PartialEq)]
720pub struct Difference {
721    /// The share of pixels that are unexplained.
722    pub share: f64,
723    /// The size of the largest 8-connected cluster of unexplained pixels.
724    pub cluster: usize,
725    /// The largest difference in any channel between the same pixel of each.
726    pub max: u8,
727}
728
729impl Difference {
730    /// Whether this is only the anti-aliasing two rasterizers may differ by.
731    pub fn within_tolerance(&self) -> bool {
732        self.share <= SHARE && self.cluster <= CLUSTER
733    }
734}
735
736/// How two RGBA buffers `width` pixels wide differ; `None` when their sizes
737/// differ.
738pub 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    // Whether pixel `i` of `p` lies outside what `q` has around it.
751    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
798/// Whether two RGBA buffers `width` pixels wide are the same drawing within
799/// the tolerance; otherwise how they differ, or `None` for different sizes.
800pub 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
807/// A PNG's or JPEG's pixels as straight RGBA, with its width and height —
808/// for comparing drawings.
809pub 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    // A root has no parent space to fill: on its own, `"fill"` hugs.
905    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        // Unpremultiply, to compare with the colors a design names.
1076        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    /// A shadow takes the shape of its box: every corner its own radius, each
1134    /// widened by the spread.
1135    #[test]
1136    fn a_shadow_follows_each_corner_and_the_spread() {
1137        // The shadow's box is 55..95 × 5..45: its top-left corner rounded by
1138        // 10 + 5, the others by 0 + 5.
1139        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        // Blurred, each corner still fades by its own radius.
1149        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    /// The alpha across a box's left edge, a pixel at a time, through the
1163    /// middle of a `size` square box with a black shadow blurred by `blur`.
1164    /// Only a strip around the edge is painted, so even the largest box is
1165    /// cheap to look at.
1166    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    /// Where a rising run of alphas crosses `level`, in pixels, between pixel
1186    /// centers.
1187    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    /// A shadow is a true Gaussian at every allowed size: it fades to nothing
1194    /// three standard deviations out, and its edge rises from 10% to 90% over
1195    /// 2.563 standard deviations — however large the box, however slight the
1196    /// blur.
1197    #[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                // The box's edge is at x = 40. Two and a half standard
1204                // deviations out a Gaussian edge is 0.6% (1.6 of 255), and
1205                // past that less: no step where the shadow's image ends.
1206                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        // Two overlapping opaque squares at half opacity: where they overlap
1230        // is as light as where they don't, because the group fades as one.
1231        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        // A triangle in a 10×10 view box, drawn at 40×40.
1257        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        // A 2×1 image, left half red and right half blue, covering a 20×20
1285        // box: scaled to 40×20 and centered, so the box shows the middle.
1286        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        // The image is sampled smoothly, so each side is mostly its color.
1303        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        // Some pixel of the text is lighter than the fill.
1336        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        // Nested: the inner frame settles first, then the outer one.
1348        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        // A fixed side stays fixed; only the hugging one follows the children.
1355        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        // Paint inside the root: the drawing is exactly the root.
1364        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        // A shadow and a child past the edge widen it; the PNG shows them.
1379        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        // The red child, at (30..50, -10..0), drawn at its place in the PNG.
1397        assert!(close(rgba(&p, 12 + 45, 5), [255, 0, 0, 255]));
1398        // The shadow, below the box.
1399        assert!(rgba(&p, 12 + 20, 10 + 30 + 3)[3] > 100);
1400        // A clipping parent cuts what its children paint, and the area too.
1401        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        // Painting for the canvas stops at the limit rather than allocating more.
1416        assert_eq!(r.paint(&s, 1.0).width(), 16384);
1417    }
1418
1419    /// Files whose `.png`s are image files of the size their name says,
1420    /// `w`x`h`.png — made without decoding, their pixels never touched.
1421    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                // Zeroed pages the system hands out only once written.
1438                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    /// Every image a drawing paints shares the GPU's one atlas. Images that
1446    /// don't fit together are the design's problem, at the `src` of the first
1447    /// that doesn't — never a hole in the canvas. One file shown many times
1448    /// takes its room once.
1449    #[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        // The same four again, and again: still four images.
1474        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    /// A drawing made after another that paints the same images — files and
1510    /// shadows alike, in the same order — paints the last one's, under the
1511    /// same blobs, so the GPU keeps them in its atlas: an edit to text or
1512    /// layout uploads no image again. Any other change of images, and any
1513    /// drawing made after none, paints every image under a blob of its own.
1514    #[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        // Two files, one shown twice, and the shadow's images.
1546        assert!(first.images.len() > 2);
1547        assert_eq!(painted(&first), ids(&first).into_iter().collect());
1548
1549        // New text and a new layout: the very same images.
1550        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        // A drawing made after none, and drawings whose images differ — a
1557        // file, their order, a shadow — share nothing with the last.
1558        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        // Truncations of a real design at every character, multi-byte ones
1688        // included.
1689        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    /// The repository's files, read from disk as the server reads them.
1712    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    /// Each example design, painted natively, matches its reference PNG in
1725    /// `design/testdata/` within the tolerance [`matches`] applies. After a deliberate change to
1726    /// how designs look, `DEVPAGE_UPDATE_REFERENCES=1 cargo test` rewrites
1727    /// them — and the diff shows the new drawings for review.
1728    #[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    /// The repository with one file's text replaced.
1760    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    /// Hiding one small part of the checkout design — painting it in the
1773    /// color behind it, so nothing else moves — takes it outside the
1774    /// tolerance: the comparison catches a missing part, not only a missing
1775    /// screen.
1776    #[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        // A dark square on white, its edge anti-aliased to grey.
1808        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        // Another rasterizer's edge: any grey between the two sides.
1817        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        // A missing one-pixel divider is unexplained along its whole length.
1825        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}