1use 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
27pub 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
38pub struct Renderer {
40 fonts: FontContext,
41 inter: Vec<parley::fontique::Blob<u8>>,
43 registered: Option<BTreeSet<FontId>>,
46 layouts: LayoutContext<()>,
47 blobs: HashMap<ImageId, ImageData>,
51 painted: Vec<(Painted, Origin)>,
52}
53
54#[derive(Clone)]
57struct Painted {
58 data: ImageData,
59 file: Option<ImageId>,
60}
61
62impl Painted {
63 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#[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
91pub enum Paint {
93 Solid(PColor),
94 Gradient(Gradient),
96 Image(ImageData, Affine),
98}
99
100pub enum Draw {
104 Fill(BezPath, Paint),
105 Stroke(BezPath, f32, PColor),
106 Glyphs(FontData, f32, PColor, Vec<i16>, Vec<Glyph>),
109 PushClip(BezPath),
110 PushOpacity(f32, Option<BezPath>),
112 PushAdd(BezPath),
117 Pop,
118}
119
120#[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
134pub struct Drawing {
137 pub frames: Vec<Frame>,
138 pub area: Frame,
139 pub list: Vec<Draw>,
140 pub images: Vec<ImageData>,
144 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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
665pub 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 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
730enum 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
741fn 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
756pub const EDGE: u8 = 8;
768pub const SHARE: f64 = 0.0005;
770pub const CLUSTER: usize = 4;
772
773#[derive(Debug, Clone, Copy, PartialEq)]
775pub struct Difference {
776 pub share: f64,
778 pub cluster: usize,
780 pub max: u8,
782}
783
784impl Difference {
785 pub fn within_tolerance(&self) -> bool {
787 self.share <= SHARE && self.cluster <= CLUSTER
788 }
789}
790
791pub 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 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
853pub 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
862pub 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 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 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 #[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 #[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 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 #[test]
1287 fn a_shadow_follows_each_corner_and_the_spread() {
1288 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 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 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 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 #[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 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 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 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 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 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 #[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 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 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 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 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 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 assert!(close(rgba(&p, 12 + 45, 5), [255, 0, 0, 255]));
1580 assert!(rgba(&p, 12 + 20, 10 + 30 + 3)[3] > 100);
1582 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 assert_eq!(r.paint(&s, Theme::Light, 1.0).width(), 16384);
1599 }
1600
1601 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 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 #[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 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 #[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 assert!(first.images.len() > 2);
1734 assert_eq!(painted(&first), ids(&first).into_iter().collect());
1735
1736 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 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 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 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 #[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 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 #[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 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 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 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}