#![cfg(all(feature = "skia-native", unix))]
use velox_dom::{Props, VNode, h, layout::compute_layout};
use velox_renderer::render_vnode_to_rgba;
use velox_style::Stylesheet;
const W: i32 = 200;
const H: i32 = 120;
const WHITE: [u8; 4] = [255, 255, 255, 255];
const BLUE: [u8; 4] = [0, 0, 255, 255];
const GREEN: [u8; 4] = [0, 255, 0, 255];
#[derive(Clone, Copy, PartialEq)]
enum Clip {
None,
Inline(&'static str),
Class(&'static str),
}
impl Clip {
fn decl(self) -> &'static str {
match self {
Clip::None => "",
Clip::Inline(v) | Clip::Class(v) => v,
}
}
fn is_none(self) -> bool {
matches!(self, Clip::None)
}
}
#[derive(Clone, Copy)]
struct Box3 {
own_background: bool,
child: bool,
}
impl Box3 {
const CHILD_ONLY: Box3 = Box3 {
own_background: false,
child: true,
};
const OWN_BACKGROUND: Box3 = Box3 {
own_background: true,
child: false,
};
}
fn tree(clip: Clip, deco: Box3) -> VNode {
let mut subject = String::from("width: 100px; height: 100px;");
if !clip.is_none() {
match clip {
Clip::Inline(_) => subject.push_str(&format!(" clip-path: {};", clip.decl())),
Clip::Class(_) => subject.push_str(&format!("clip-path: {};", clip.decl())),
Clip::None => {}
}
}
if deco.own_background {
subject.push_str(" background: #00ff00;");
}
let mut props = Props::new();
if let Clip::Class(_) = clip {
props = props.set("class", "clip");
}
props = props.set("style", subject);
let children = if deco.child {
vec![h(
"div",
Props::new().set("style", "width: 100px; height: 100px; background: #0000ff;"),
vec![],
)]
} else {
vec![]
};
h(
"div",
Props::new().set("style", "width: 200px; height: 120px; background: #ffffff;"),
vec![h("div", props, children)],
)
}
fn render(clip: Clip, deco: Box3) -> Vec<u8> {
let sheet = match clip {
Clip::Class(v) => Stylesheet::parse(&format!(".clip {{ clip-path: {v}; }}")),
_ => Stylesheet::default(),
};
render_vnode_to_rgba(&tree(clip, deco), &sheet, W, H).expect("render to rgba")
}
fn px(buf: &[u8], x: i32, y: i32) -> [u8; 4] {
let i = ((y * W + x) * 4) as usize;
buf[i..i + 4].try_into().expect("four bytes per pixel")
}
fn hex(c: [u8; 4]) -> String {
format!("#{:02x}{:02x}{:02x}{:02x}", c[0], c[1], c[2], c[3])
}
fn assert_same(actual: &[u8], expected: &[u8], what: &str) {
if actual == expected {
return;
}
if actual.len() != expected.len() {
panic!(
"{what}: the frames differ in size ({} vs {} bytes)",
actual.len(),
expected.len()
);
}
let at = actual
.iter()
.zip(expected.iter())
.position(|(a, b)| a != b)
.expect("equal length");
let p = (at / 4) as i32;
panic!(
"{what}: first difference at pixel ({}, {}) — painted {}, expected {}",
p % W,
p / W,
hex(px(actual, p % W, p / W)),
hex(px(expected, p % W, p / W))
);
}
fn assert_differs(actual: &[u8], other: &[u8], what: &str) {
if actual == other {
panic!("{what}: the two frames are byte-identical");
}
}
#[test]
fn the_subject_and_its_child_are_where_these_tests_assume() {
let laid = compute_layout(&tree(Clip::None, Box3::CHILD_ONLY), 800, 600);
assert_eq!(
(laid.rect.x, laid.rect.y, laid.rect.w, laid.rect.h),
(0, 0, 200, 120),
"the page must fill the surface from the origin"
);
let subject = &laid.children[0];
assert_eq!(
(
subject.rect.x,
subject.rect.y,
subject.rect.w,
subject.rect.h
),
(0, 0, 100, 100),
"the element under test must be 100x100 at the origin"
);
let child = &subject.children[0];
assert_eq!(
(child.rect.x, child.rect.y, child.rect.w, child.rect.h),
(0, 0, 100, 100),
"the child must fill the element exactly, so every inset edge cuts it"
);
}
#[test]
fn a_class_rule_inset_paints_exactly_like_an_inline_one_and_neither_like_no_clip() {
let unclipped = render(Clip::None, Box3::CHILD_ONLY);
let inline = render(Clip::Inline("inset(10px)"), Box3::CHILD_ONLY);
let class_rule = render(Clip::Class("inset(10px)"), Box3::CHILD_ONLY);
assert_eq!(
hex(px(&unclipped, 50, 50)),
hex(BLUE),
"the control must paint the child at all, or nothing below means anything"
);
assert_differs(
&class_rule,
&unclipped,
"clip-path clipped nothing: the declaration never reached paint",
);
assert_differs(&inline, &unclipped, "the inline clip-path clipped nothing");
assert_same(
&class_rule,
&inline,
"class-rule and inline `clip-path` render differently",
);
assert_eq!(hex(px(&class_rule, 50, 50)), hex(BLUE));
assert_eq!(hex(px(&class_rule, 5, 50)), hex(WHITE));
assert_eq!(hex(px(&class_rule, 50, 5)), hex(WHITE));
assert_eq!(hex(px(&class_rule, 95, 95)), hex(WHITE));
}
#[test]
fn a_clipped_element_differs_from_an_unclipped_one() {
assert_differs(
&render(Clip::Inline("inset(10px)"), Box3::CHILD_ONLY),
&render(Clip::None, Box3::CHILD_ONLY),
"an inset clip and no clip at all produced the same pixels",
);
}
#[test]
fn a_child_painted_past_the_inset_edge_is_cut_at_the_edge() {
let clipped = render(Clip::Inline("inset(20px)"), Box3::CHILD_ONLY);
assert_eq!(
hex(px(&clipped, 50, 50)),
hex(BLUE),
"the middle of the element is inside a 20px inset and must show the child"
);
for (x, y, edge) in [
(5, 50, "left"),
(95, 50, "right"),
(50, 5, "top"),
(50, 95, "bottom"),
(5, 5, "top-left corner"),
(95, 95, "bottom-right corner"),
] {
assert_eq!(
hex(px(&clipped, x, y)),
hex(WHITE),
"the {edge} of a 20px inset must be clear, and the child overhangs it"
);
}
assert_eq!(hex(px(&clipped, 21, 50)), hex(BLUE));
assert_eq!(hex(px(&clipped, 78, 50)), hex(BLUE));
assert_eq!(hex(px(&clipped, 50, 21)), hex(BLUE));
assert_eq!(hex(px(&clipped, 50, 78)), hex(BLUE));
}
#[test]
fn the_elements_own_background_is_clipped_too() {
let clipped = render(Clip::Inline("inset(20px)"), Box3::OWN_BACKGROUND);
assert_eq!(
hex(px(&clipped, 50, 50)),
hex(GREEN),
"the middle of the element is inside the inset and must show its own fill"
);
for (x, y, edge) in [
(5, 50, "left"),
(95, 50, "right"),
(50, 5, "top"),
(50, 95, "bottom"),
(95, 95, "bottom-right corner"),
] {
assert_eq!(
hex(px(&clipped, x, y)),
hex(WHITE),
"the element's OWN background was not clipped at the {edge} of a 20px inset"
);
}
let unclipped = render(Clip::None, Box3::OWN_BACKGROUND);
assert_eq!(hex(px(&unclipped, 5, 50)), hex(GREEN));
assert_differs(
&clipped,
&unclipped,
"a 20px inset left the element's own background untouched",
);
}
#[test]
fn a_class_rule_background_is_clipped_too() {
let class_rule = render(Clip::Class("inset(20px)"), Box3::OWN_BACKGROUND);
assert_eq!(hex(px(&class_rule, 5, 50)), hex(WHITE));
assert_eq!(hex(px(&class_rule, 50, 50)), hex(GREEN));
assert_differs(
&class_rule,
&render(Clip::None, Box3::OWN_BACKGROUND),
"a class-rule 20px inset left the element's own background untouched",
);
}
#[test]
fn the_two_value_shorthand_clips_top_bottom_and_left_right_apart() {
let clipped = render(Clip::Inline("inset(10px 40px)"), Box3::CHILD_ONLY);
assert_differs(
&clipped,
&render(Clip::None, Box3::CHILD_ONLY),
"this shorthand form clipped nothing at all",
);
assert_eq!(hex(px(&clipped, 50, 50)), hex(BLUE), "centre is inside");
assert_eq!(
hex(px(&clipped, 30, 50)),
hex(WHITE),
"x=30 is inside the 40px side"
);
assert_eq!(
hex(px(&clipped, 50, 5)),
hex(WHITE),
"y=5 is inside the 10px side"
);
assert_eq!(
hex(px(&clipped, 70, 50)),
hex(WHITE),
"x=70 is past the 40px side"
);
assert_eq!(
hex(px(&clipped, 50, 10)),
hex(BLUE),
"y=10 is at the 10px side"
);
assert_eq!(
hex(px(&clipped, 40, 50)),
hex(BLUE),
"x=40 is at the 40px side"
);
}
#[test]
fn the_three_value_shorthand_clips_top_sides_bottom() {
let clipped = render(Clip::Inline("inset(10px 40px 60px)"), Box3::CHILD_ONLY);
assert_differs(
&clipped,
&render(Clip::None, Box3::CHILD_ONLY),
"this shorthand form clipped nothing at all",
);
assert_eq!(hex(px(&clipped, 50, 20)), hex(BLUE), "centre is inside");
assert_eq!(
hex(px(&clipped, 50, 45)),
hex(WHITE),
"y=45 is past the 60px bottom"
);
assert_eq!(
hex(px(&clipped, 50, 8)),
hex(WHITE),
"y=8 is inside the 10px top"
);
assert_eq!(
hex(px(&clipped, 30, 20)),
hex(WHITE),
"x=30 is inside the 40px side"
);
assert_eq!(
hex(px(&clipped, 50, 39)),
hex(BLUE),
"y=39 is inside the bottom edge"
);
}
#[test]
fn the_four_value_shorthand_clips_each_side_on_its_own() {
let clipped = render(Clip::Inline("inset(5px 10px 15px 20px)"), Box3::CHILD_ONLY);
assert_differs(
&clipped,
&render(Clip::None, Box3::CHILD_ONLY),
"this shorthand form clipped nothing at all",
);
assert_eq!(hex(px(&clipped, 50, 50)), hex(BLUE), "centre is inside");
assert_eq!(
hex(px(&clipped, 10, 50)),
hex(WHITE),
"x=10 is inside the 20px left"
);
assert_eq!(
hex(px(&clipped, 95, 50)),
hex(WHITE),
"x=95 is inside the 10px right"
);
assert_eq!(
hex(px(&clipped, 50, 2)),
hex(WHITE),
"y=2 is inside the 5px top"
);
assert_eq!(
hex(px(&clipped, 50, 90)),
hex(WHITE),
"y=90 is inside the 15px bottom"
);
assert_eq!(
hex(px(&clipped, 20, 50)),
hex(BLUE),
"x=20 is the left edge itself"
);
assert_eq!(
hex(px(&clipped, 89, 50)),
hex(BLUE),
"x=89 is the last right-edge column"
);
assert_eq!(
hex(px(&clipped, 19, 50)),
hex(WHITE),
"x=19 is one column outside the left edge"
);
}
#[test]
fn a_five_value_inset_is_rejected_rather_than_guessed_at() {
let rejected = render(
Clip::Inline("inset(5px 10px 15px 20px 25px)"),
Box3::CHILD_ONLY,
);
assert_same(
&rejected,
&render(Clip::None, Box3::CHILD_ONLY),
"a five-value inset must be dropped, not truncated to its first four",
);
}
#[test]
fn every_shape_this_renderer_does_not_draw_is_rejected_and_paints_everything() {
let unclipped = render(Clip::None, Box3::CHILD_ONLY);
assert_eq!(hex(px(&unclipped, 50, 50)), hex(BLUE));
assert_differs(
&unclipped,
&vec![0u8; (W * H * 4) as usize],
"the control is blank: nothing below can prove a rejection",
);
for shape in [
"circle(50%)",
"circle(50px at 50px 50px)",
"ellipse(40px 20px)",
"polygon(0 0, 100px 0, 100px 100px)",
"url(#svg-clip)",
"inset(10% 20%)",
"inset(10em)",
"inset(10)",
"none",
] {
let painted = render(Clip::Inline(shape), Box3::CHILD_ONLY);
assert_same(
&painted,
&unclipped,
"`clip-path: {shape}` must be rejected outright: it painted something other \
than the unclipped control, so it was half-honoured",
);
assert_eq!(
hex(px(&painted, 95, 95)),
hex(BLUE),
"`clip-path: {shape}` clipped the child instead of being rejected"
);
}
}
#[test]
fn an_overflowing_inset_is_rejected_and_does_not_erase_the_subtree() {
let overflowing = render(Clip::Inline("inset(1e999px)"), Box3::CHILD_ONLY);
assert_same(
&overflowing,
&render(Clip::None, Box3::CHILD_ONLY),
"`inset(1e999px)` must be rejected, not turned into an infinite clip rect that \
empties the element",
);
assert_eq!(
hex(px(&overflowing, 50, 50)),
hex(BLUE),
"the subtree vanished: an infinite inset clipped the whole element"
);
}
#[test]
fn a_nan_or_partially_overflowing_inset_is_rejected_too() {
let unclipped = render(Clip::None, Box3::CHILD_ONLY);
for value in [
"inset(NaNpx)",
"inset(1e999px 10px)",
"inset(10px 10px 10px 1e999px)",
"inset(-1e999px)",
] {
assert_same(
&render(Clip::Inline(value), Box3::CHILD_ONLY),
&unclipped,
"`clip-path: {value}` must be rejected outright",
);
}
}
#[test]
fn an_uppercase_inset_is_not_a_clip() {
let upper = render(Clip::Inline("INSET(10px)"), Box3::CHILD_ONLY);
assert_same(
&upper,
&render(Clip::None, Box3::CHILD_ONLY),
"the case-sensitivity decision changed: `INSET(10px)` now clips",
);
assert_differs(
&upper,
&render(Clip::Inline("inset(10px)"), Box3::CHILD_ONLY),
"the control is wrong: lowercase `inset(10px)` must clip",
);
}
#[test]
fn a_negative_inset_is_a_no_op() {
let unclipped = render(Clip::None, Box3::CHILD_ONLY);
for value in ["inset(-10px)", "inset(-10px 20px)", "inset(10px -20px)"] {
assert_same(
&render(Clip::Inline(value), Box3::CHILD_ONLY),
&unclipped,
"`clip-path: {value}` must be dropped as invalid, not applied as an expansion",
);
}
assert_differs(
&unclipped,
&render(Clip::Inline("inset(10px)"), Box3::CHILD_ONLY),
"the control is wrong: `inset(10px)` must clip",
);
}
#[test]
fn an_empty_inset_is_the_elements_own_box() {
assert_same(
&render(Clip::Inline("inset()"), Box3::CHILD_ONLY),
&render(Clip::None, Box3::CHILD_ONLY),
"`inset()` is a zero inset, which is the element's own box",
);
}