use crate::compare::*;
use std::collections::HashMap;
use crate::render::testing::*;
use crate::resolve::{self, Theme};
use super::*;
#[test]
fn the_drawing_is_the_root_plus_what_is_painted_outside_it() {
let mut r = Renderer::new();
let s = scene(
r##"<Frame width={40} height={30} radius={12} fill="#000000"><Text>Hi</Text></Frame>"##,
);
let area = r.drawing(&s, Theme::Light, None).unwrap().area();
assert_eq!(
area,
Frame {
x: 0.0,
y: 0.0,
width: 40.0,
height: 30.0
}
);
assert_eq!(r.paint(&s, Theme::Light, 1.0).width(), 40);
let s = scene(
r##"<Frame width={40} height={30} fill="#ffffff" shadow={{ y: 4, blur: 8, color: "#000000ff" }}>
<Rect x={30} y={-10} width={20} height={10} fill="#ff0000" />
</Frame>"##,
);
let area = r.drawing(&s, Theme::Light, None).unwrap().area();
assert_eq!(
area,
Frame {
x: -12.0,
y: -10.0,
width: 64.0,
height: 56.0
}
);
let p = r.paint(&s, Theme::Light, 1.0);
assert_eq!((p.width(), p.height()), (64, 56));
assert!(close(rgba(&p, 12 + 45, 5), [255, 0, 0, 255]));
assert!(rgba(&p, 12 + 20, 10 + 30 + 3)[3] > 100);
let s = scene(
r##"<Frame width={40} height={30} clip><Rect x={30} width={50} height={10} fill="#ff0000" /></Frame>"##,
);
assert_eq!(
r.drawing(&s, Theme::Light, None).unwrap().area().width,
40.0
);
}
#[test]
fn a_drawing_of_any_size_is_a_picture() {
let mut r = Renderer::new();
let s = scene(r##"<Frame width={70000} height={10} fill="#000000" />"##);
let (px, w, h) = pixels(&crate::png(&mut r, &s, Theme::Light).unwrap()).unwrap();
assert_eq!((w, h), (70000, 10));
assert_eq!(&px[px.len() - 4..], &[0, 0, 0, 255]);
}
struct Sized(HashMap<String, String>);
impl crate::Files for Sized {
fn bytes(&self, path: &str) -> Option<Vec<u8>> {
self.0.get(path).map(|s| s.clone().into_bytes())
}
fn list(&self, path: &str) -> Option<Vec<String>> {
self.0.list(path)
}
fn image(&self, path: &str) -> Option<Result<std::sync::Arc<resolve::ImageFile>, String>> {
let name = path.strip_suffix(".png")?;
let (size, n) = name.split_once('-')?;
let (w, h) = size.split_once('x')?;
let (w, h): (u32, u32) = (w.parse().ok()?, h.parse().ok()?);
Some(Ok(std::sync::Arc::new(resolve::ImageFile {
id: resolve::ImageId::of(path.as_bytes()),
bytes: n.as_bytes().to_vec(),
width: w,
height: h,
pixels: std::sync::Arc::new(vec![0; 4 * w as usize * h as usize]),
pixels_width: w,
pixels_height: h,
})))
}
}
#[test]
fn images_that_do_not_fit_the_atlas_are_a_problem_where_written() {
let design = |files: &[&str]| {
let images: String = files
.iter()
.map(|f| format!("\n <Image src=\"{f}\" width={{40}} height={{40}} />"))
.collect();
Sized(
[(
"a.design".to_string(),
format!(r#"<Frame layout="row">{images}</Frame>"#),
)]
.into(),
)
};
let mut r = Renderer::new();
let four = design(&[
"4096x4096-a.png",
"4096x4096-b.png",
"4096x4096-c.png",
"4096x4096-d.png",
]);
let scene = resolve::resolve(&four, "a.design").unwrap();
assert!(r.drawing(&scene, Theme::Light, None).is_ok());
let again = design(&[
"4096x4096-a.png",
"4096x4096-b.png",
"4096x4096-a.png",
"4096x4096-c.png",
"4096x4096-d.png",
"4096x4096-b.png",
]);
let scene = resolve::resolve(&again, "a.design").unwrap();
assert!(r.drawing(&scene, Theme::Light, None).is_ok());
let six = design(&[
"4096x4096-a.png",
"4096x4096-b.png",
"4096x4096-c.png",
"4096x4096-d.png",
"4096x4096-e.png",
"4096x4096-f.png",
]);
let scene = resolve::resolve(&six, "a.design").unwrap();
let Err(e) = r.drawing(&scene, Theme::Light, None) else {
panic!("six 4096-pixel squares fit")
};
let at = e.at.unwrap();
assert_eq!(
(at.file.as_str(), at.pos.line, at.pos.col),
("a.design", 6, 10)
);
assert_eq!(
e.message,
"this doesn't fit: the images a drawing paints — its image files, at most 4096 \
pixels a side, and its shadows — share the GPU's one 8192×8192 atlas, and the 4 \
before this 4096×4096 fill it"
);
}
#[test]
fn a_drawing_with_the_last_ones_images_paints_them_under_the_same_blobs() {
let design = |text: &str, files: &[&str], blur: u32| {
let images: String = files
.iter()
.map(|f| format!("\n <Image src=\"{f}\" width={{40}} height={{40}} />"))
.collect();
let source = format!(
r##"<Frame layout="row"><Text>{text}</Text>{images}
<Rect width={{40}} height={{40}} shadow={{{{ blur: {blur}, color: "#000000" }}}} />
</Frame>"##
);
resolve::resolve(
&Sized([("a.design".to_string(), source)].into()),
"a.design",
)
.unwrap()
};
let ids = |d: &Drawing| d.images.iter().map(|i| i.data.id()).collect::<Vec<_>>();
let painted = |d: &Drawing| {
d.list
.chunks()
.flat_map(|(_, c)| c.commands.iter().map(|c| &c.draw))
.filter_map(|c| match c {
Draw::Fill(_, Paint::Image(i, _)) => Some(i.data.id()),
_ => None,
})
.collect::<std::collections::BTreeSet<_>>()
};
let mut r = Renderer::new();
let files = ["300x200-a.png", "100x100-b.png", "300x200-a.png"];
let first = r
.drawing(&design("One", &files, 6), Theme::Light, None)
.unwrap();
assert!(first.images.len() > 2);
assert_eq!(painted(&first), ids(&first).into_iter().collect());
let edited = r
.drawing(&design("Two words", &files, 6), Theme::Light, Some(&first))
.unwrap();
assert_eq!(ids(&edited), ids(&first));
assert_eq!(painted(&edited), painted(&first));
let fresh = r
.drawing(&design("One", &files, 6), Theme::Light, None)
.unwrap();
let other = ["300x200-a.png", "100x100-c.png"];
let swapped = ["100x100-b.png", "300x200-a.png"];
for d in [
fresh,
r.drawing(&design("One", &other, 6), Theme::Light, Some(&first))
.unwrap(),
r.drawing(&design("One", &swapped, 6), Theme::Light, Some(&first))
.unwrap(),
r.drawing(&design("One", &files, 7), Theme::Light, Some(&first))
.unwrap(),
] {
let shared: Vec<_> = ids(&d)
.into_iter()
.filter(|i| ids(&first).contains(i))
.collect();
assert!(shared.is_empty(), "{shared:?}");
assert_eq!(painted(&d), ids(&d).into_iter().collect());
}
}
#[test]
fn no_input_makes_the_engine_panic() {
let attrs = [
"fill",
"stroke",
"strokeWidth",
"radius",
"shadow",
"clip",
"layout",
"gap",
"padding",
"align",
"justify",
"width",
"height",
"x",
"y",
"opacity",
"onClick",
"size",
"weight",
"color",
"lineHeight",
"textAlign",
"d",
"viewBox",
"src",
"font",
"name",
];
let values = [
"\"#fff\u{200b}\"",
"\"#ffé\"",
"\"€\"",
"\"\"",
"\"#\"",
"\"#12345\"",
"\"linear-gradient(\"",
"\"linear-gradient(é)\"",
"\"linear-gradient(90deg, #fff é%)\"",
"{-1000000}",
"{1000000}",
"{0}",
"{-0.0001}",
"{[]}",
"{[1]}",
"{[1,2,3]}",
"{[1000000, 1000000, 1000000, 1000000]}",
"{{}}",
"{{ blur: 1000000, spread: 1000000 }}",
"{{ x: \"é\" }}",
"{true}",
"\"navigate \"",
"\"navigate ../../../x.design\"",
"\"M\"",
"\"M0 0 L1000000 1000000 Z\"",
"\"0 0 0 0\"",
"\"é é é é\"",
"\"hug\"",
"\"fill\"",
"\"none\"",
"\"🙂\"",
];
let mut r = Renderer::new();
for el in ["Frame", "Text", "Rect", "Ellipse", "Vector", "Image"] {
for attr in attrs {
for value in values {
let source = format!(
r#"<Frame layout="row"><{el} {attr}={value}>{}</{el}></Frame>"#,
if el == "Text" { "é 🙂" } else { "" }
);
let files: HashMap<String, String> =
[("a.design".to_string(), source.clone())].into();
if let Ok(s) = resolve::resolve(&files, "a.design") {
if let Ok(d) = r.drawing(&s, Theme::Light, None) {
let _ = crate::cpu::tile(&d.list, 1.0, 0, 0);
}
}
}
}
}
let source = r##"import B from "./b.design"
<Frame props={{ t: "héllo 🙂" }} layout="column" fill="#fff">
<Text color="#123">{t} — ok</Text><B />
</Frame>"##;
for (i, _) in source.char_indices() {
let files: HashMap<String, String> = [
("a.design".to_string(), source[..i].to_string()),
("b.design".to_string(), "<Frame />".to_string()),
]
.into();
let _ = resolve::resolve(&files, "a.design");
}
}
#[test]
fn a_click_is_answered_from_the_index() {
let mut rows = String::new();
for i in 0..1000 {
rows.push_str(&format!(
"<Frame width={{400}} height={{100}} onClick=\"navigate ./r{i}.design\"><Frame width={{50}} height={{50}} clip><Rect x={{40}} width={{100}} height={{20}} onClick=\"back\" /></Frame></Frame>\n"
));
}
let mut files: HashMap<String, String> = (0..1000)
.map(|i| (format!("r{i}.design"), "<Frame />".to_string()))
.collect();
files.insert(
"a.design".to_string(),
format!("<Frame layout=\"column\">{rows}</Frame>"),
);
let s = resolve::resolve(&files, "a.design").unwrap();
let mut r = Renderer::new();
let d = r.drawing(&s, Theme::Light, None).unwrap();
assert_eq!(d.action_at(200.0, 99_950.0), Some("navigate r999.design"));
assert_eq!(d.action_at(45.0, 99_905.0), Some("back"));
assert_eq!(d.action_at(80.0, 99_905.0), Some("navigate r999.design"));
assert_eq!(d.action_at(500.0, 99_905.0), None);
}