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