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