use crate::renderer::{ClipChains, CornerRadii, DrawCommand, FlattenedCommand};
use crate::shape::{Outline, PlacedShape, Spans};
use smallvec::SmallVec;
use crate::widgets::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RegionRect {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
impl RegionRect {
#[cfg(any(test, feature = "testing"))]
pub(crate) fn contains(&self, x: i32, y: i32) -> bool {
x >= self.x && y >= self.y && x < self.x + self.width && y < self.y + self.height
}
pub(crate) fn covering(r: Rect) -> Option<Self> {
let x = r.x.floor() as i32;
let y = r.y.floor() as i32;
let width = (r.x + r.width).ceil() as i32 - x;
let height = (r.y + r.height).ceil() as i32 - y;
(width > 0 && height > 0).then_some(Self {
x,
y,
width,
height,
})
}
}
pub(crate) fn placed_shape_to_rects(shape: PlacedShape, clips: &[PlacedShape]) -> Vec<RegionRect> {
if shape.rect.width <= 0.0 || shape.rect.height <= 0.0 {
return Vec::new();
}
let (mut top, mut bottom) = shape.y_bounds();
for clip in clips {
let (clip_top, clip_bottom) = clip.y_bounds();
top = top.max(clip_top);
bottom = bottom.min(clip_bottom);
}
let height = bottom - top;
if height <= 0.0 {
return Vec::new();
}
if shape.is_a_box() && clips.iter().all(PlacedShape::is_a_box) {
let boxed = clips.iter().try_fold(shape.world_aabb(), |boxed, clip| {
boxed.intersection(&clip.world_aabb())
});
return boxed.and_then(to_region_rect).into_iter().collect();
}
let outline = shape.outline();
let clip_outlines: SmallVec<[Outline; 2]> = clips.iter().map(PlacedShape::outline).collect();
let bitten = outline.is_bitten() || clip_outlines.iter().any(Outline::is_bitten);
let bounds = |y: f32| -> Option<(f32, f32)> {
let (mut left, mut right) = outline.bounds_at(y)?;
for clip in &clip_outlines {
let (clip_left, clip_right) = clip.bounds_at(y)?;
left = left.max(clip_left);
right = right.min(clip_right);
}
(left < right).then_some((left, right))
};
let bands = (height.ceil() as usize).clamp(4, 512);
let band_h = height / bands as f32;
let mut out: Vec<RegionRect> = Vec::new();
let mut previous: SmallVec<[usize; 2]> = SmallVec::new();
let mut upper = bounds(top);
for i in 0..bands {
let y0 = top + i as f32 * band_h;
let y1 = y0 + band_h;
let lower = bounds(y1);
let (Some((a0, b0)), Some((a1, b1))) = (upper, lower) else {
upper = lower;
previous.clear();
continue;
};
upper = lower;
let band = (a0.max(a1), b0.min(b1));
if band.0 >= band.1 {
previous.clear();
continue;
}
if !bitten {
place(&mut out, band, y0, band_h, None);
continue;
}
let mut spans = Spans::new();
spans.push(band);
outline.take_bites_from(&mut spans, y0, y1);
for clip in &clip_outlines {
clip.take_bites_from(&mut spans, y0, y1);
}
let mut current: SmallVec<[usize; 2]> = SmallVec::new();
for span in spans {
current.extend(place(&mut out, span, y0, band_h, Some(&previous)));
}
previous = current;
}
out
}
#[inline]
fn place(
out: &mut Vec<RegionRect>,
(left, right): (f32, f32),
y0: f32,
band_h: f32,
above: Option<&SmallVec<[usize; 2]>>,
) -> Option<usize> {
let rect = to_region_rect(Rect::new(left, y0, right - left, band_h))?;
let grows = |prev: &RegionRect| {
prev.x == rect.x && prev.width == rect.width && prev.y + prev.height == rect.y
};
let existing = match above {
Some(above) => above.iter().copied().find(|&i| grows(&out[i])),
None => out.last().is_some_and(grows).then(|| out.len() - 1),
};
Some(match existing {
Some(i) => {
out[i].height += rect.height;
i
}
None => {
out.push(rect);
out.len() - 1
}
})
}
fn to_region_rect(b: Rect) -> Option<RegionRect> {
let x0 = b.x.round() as i32;
let y0 = b.y.round() as i32;
let x1 = (b.x + b.width).round() as i32;
let y1 = (b.y + b.height).round() as i32;
(x1 > x0 && y1 > y0).then_some(RegionRect {
x: x0,
y: y0,
width: x1 - x0,
height: y1 - y0,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct InputRegionOp {
pub takes: bool,
pub rect: RegionRect,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct InputRegionRequest {
pub base: Vec<RegionRect>,
pub ops: Vec<InputRegionOp>,
}
pub(crate) fn input_ops_from_commands(
commands: &[FlattenedCommand],
chains: &ClipChains,
) -> Vec<InputRegionOp> {
let mut out = Vec::new();
for cmd in commands {
let DrawCommand::InputRegion {
rect,
corner_radii,
curvature,
takes,
} = &*cmd.command
else {
continue;
};
out.extend(
shape_of(cmd, chains, *rect, *corner_radii, *curvature)
.into_iter()
.map(|rect| InputRegionOp {
takes: *takes,
rect,
}),
);
}
out
}
pub(crate) fn shape_of(
cmd: &FlattenedCommand,
chains: &ClipChains,
rect: Rect,
corner_radii: CornerRadii,
curvature: f32,
) -> Vec<RegionRect> {
let clips: SmallVec<[PlacedShape; 2]> = match cmd.clip() {
Some(clip) => chains.shapes(&clip).copied().collect(),
None => SmallVec::new(),
};
placed_shape_to_rects(
PlacedShape::placed(rect, corner_radii, curvature, cmd.world_transform),
&clips,
)
}
pub(crate) fn base_rects(declared: Option<&[Rect]>, width: f32, height: f32) -> Vec<RegionRect> {
match declared {
None => Vec::from_iter(RegionRect::covering(Rect::new(0.0, 0.0, width, height))),
Some(rects) => rects
.iter()
.copied()
.filter_map(RegionRect::covering)
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transform::Transform;
fn covers(rects: &[RegionRect], x: i32, y: i32) -> bool {
rects.iter().any(|r| r.contains(x, y))
}
fn upright(rect: Rect, radii: CornerRadii) -> Vec<RegionRect> {
placed_shape_to_rects(shape(rect, radii, Transform::IDENTITY), &[])
}
fn shape(rect: Rect, radii: CornerRadii, placement: Transform) -> PlacedShape {
PlacedShape::placed(rect, radii, 1.0, placement)
}
fn round(radius: f32) -> CornerRadii {
CornerRadii::uniform(radius)
}
#[test]
fn zero_radius_is_bounding_box() {
let rects = upright(Rect::new(10.0, 20.0, 100.0, 50.0), round(0.0));
assert_eq!(
rects,
vec![RegionRect {
x: 10,
y: 20,
width: 100,
height: 50
}]
);
}
#[test]
fn degenerate_rect_is_empty() {
assert!(upright(Rect::new(0.0, 0.0, 0.0, 40.0), round(8.0)).is_empty());
assert!(upright(Rect::new(0.0, 0.0, 40.0, -1.0), round(8.0)).is_empty());
}
#[test]
fn rounded_corners_are_cut() {
let rects = upright(Rect::new(0.0, 0.0, 100.0, 60.0), round(20.0));
assert!(!covers(&rects, 0, 0));
assert!(!covers(&rects, 99, 0));
assert!(!covers(&rects, 0, 59));
assert!(!covers(&rects, 99, 59));
assert!(covers(&rects, 50, 30));
assert!(covers(&rects, 0, 30));
assert!(covers(&rects, 99, 30));
assert!(covers(&rects, 50, 0));
assert!(covers(&rects, 50, 59));
}
#[test]
fn slabs_stay_inside_the_curve() {
let r = 16.0f32;
let rects = upright(Rect::new(0.0, 0.0, 80.0, 80.0), round(r));
for rect in &rects {
for (px, py) in [
(rect.x, rect.y),
(rect.x + rect.width, rect.y),
(rect.x, rect.y + rect.height),
(rect.x + rect.width, rect.y + rect.height),
] {
let (px, py) = (px as f32, py as f32);
let cx = px.clamp(r, 80.0 - r);
let cy = py.clamp(r, 80.0 - r);
let dist = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
assert!(
dist <= r + 0.75,
"corner ({px}, {py}) is {dist} from the curve center, radius {r}"
);
}
}
}
#[test]
fn radius_clamped_to_half_size() {
let rects = upright(Rect::new(0.0, 0.0, 40.0, 40.0), round(100.0));
assert!(!rects.is_empty());
assert!(covers(&rects, 20, 20), "the middle is covered");
assert!(!covers(&rects, 0, 0), "the corner is not");
}
#[test]
fn two_corners_on_one_edge_meet_rather_than_cross() {
use crate::widgets::Corners;
for (w, h) in [(40.0_f32, 40.0_f32), (120.0, 40.0), (40.0, 120.0)] {
for r in [(w.min(h)) / 2.0, w.min(h), 500.0] {
let shape = PlacedShape::placed(
Rect::new(0.0, 0.0, w, h),
round(r),
1.0,
Transform::IDENTITY,
);
let rects = placed_shape_to_rects(shape, &[]);
assert!(
!rects.is_empty(),
"{w}x{h} r={r}: the shape has an area and the region has none"
);
let corners = Corners {
radii: round(r),
curvature: 1.0,
};
let local = Rect::new(0.0, 0.0, w, h);
for rect in &rects {
for y in rect.y..rect.y + rect.height {
for x in rect.x..rect.x + rect.width {
assert!(
local.contains_shape(x as f32 + 0.5, y as f32 + 0.5, corners),
"{w}x{h} r={r}: ({x}, {y}) is published and not drawn"
);
}
}
}
}
}
}
#[test]
fn a_square_corner_is_not_rounded_because_another_one_is() {
let rects = upright(
Rect::new(0.0, 0.0, 40.0, 100.0),
CornerRadii {
top_left: 16.0,
top_right: 0.0,
bottom_right: 0.0,
bottom_left: 16.0,
},
);
assert!(
covers(&rects, 39, 0) && covers(&rects, 39, 99),
"the cut edge is straight, so its corners reach the edge"
);
assert!(
!covers(&rects, 0, 0) && !covers(&rects, 0, 99),
"and the round ones are still cut"
);
}
#[test]
fn corners_are_cut_one_by_one() {
let rects = upright(Rect::new(0.0, 0.0, 100.0, 60.0), CornerRadii::top(20.0));
assert!(!covers(&rects, 0, 0) && !covers(&rects, 99, 0), "top cut");
assert!(
covers(&rects, 0, 59) && covers(&rects, 99, 59),
"bottom square"
);
}
#[test]
fn an_elliptical_corner_is_cut_on_both_of_its_axes() {
let rects = placed_shape_to_rects(
shape(
Rect::new(0.0, 0.0, 100.0, 100.0),
round(16.0),
Transform::scale_xy(2.0, 1.0),
),
&[],
);
assert!(!covers(&rects, 0, 0), "the corner itself is outside");
assert!(
covers(&rects, 0, 20),
"the curve is 16 tall, so the left edge is reached by 20 down"
);
assert!(
!covers(&rects, 5, 1),
"and 32 wide, so it is still outside 5 in at the top"
);
}
#[test]
fn slabs_stay_inside_a_shape_with_four_different_corners() {
let declared = CornerRadii {
top_left: 30.0,
top_right: 10.0,
bottom_right: 0.0,
bottom_left: 20.0,
};
let (sx, sy) = (1.2f32, 2.0f32);
let (w, h) = (100.0f32, 50.0f32);
let half = (w * 0.5).min(h * 0.5);
let radii = CornerRadii {
top_left: declared.top_left.min(half),
top_right: declared.top_right.min(half),
bottom_right: declared.bottom_right.min(half),
bottom_left: declared.bottom_left.min(half),
};
let rects = placed_shape_to_rects(
PlacedShape::placed(
Rect::new(0.0, 0.0, w, h),
declared,
1.0,
Transform::scale_xy(sx, sy),
),
&[],
);
assert!(!rects.is_empty());
let (ww, hh) = (w * sx, h * sy);
let corners = [
(
radii.top_left * sx,
radii.top_left * sy,
radii.top_left * sx,
radii.top_left * sy,
true,
true,
),
(
ww - radii.top_right * sx,
radii.top_right * sy,
radii.top_right * sx,
radii.top_right * sy,
false,
true,
),
(
radii.bottom_left * sx,
hh - radii.bottom_left * sy,
radii.bottom_left * sx,
radii.bottom_left * sy,
true,
false,
),
];
for rect in &rects {
for (px, py) in [
(rect.x, rect.y),
(rect.x + rect.width, rect.y),
(rect.x, rect.y + rect.height),
(rect.x + rect.width, rect.y + rect.height),
] {
let (px, py) = (px as f32, py as f32);
for (cx, cy, rx, ry, left, top) in corners {
let in_x = if left { px < cx } else { px > cx };
let in_y = if top { py < cy } else { py > cy };
if !in_x || !in_y {
continue;
}
let d = ((px - cx) / rx).powi(2) + ((py - cy) / ry).powi(2);
assert!(
d <= 1.0 + 0.1,
"({px}, {py}) is outside the {rx}x{ry} corner at ({cx}, {cy}): {d}"
);
}
}
}
}
#[test]
fn a_side_ends_where_its_own_corners_begin() {
let shape = PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
radii: CornerRadii {
top_left: 0.0,
top_right: 0.0,
bottom_right: 30.0,
bottom_left: 0.0,
},
curvature: 1.0,
placement: Transform::IDENTITY,
};
let close = |a: f32, b: f32| (a - b).abs() < 1e-3;
let (left, right) = shape.span_at(100.0).expect("the bottom edge is there");
assert!(
close(left, 0.0),
"square at the bottom left, so the side reaches — got {left}"
);
assert!(
close(right, 70.0),
"and stops where the 30-radius bottom-right corner starts — got {right}"
);
let mirrored = PlacedShape {
radii: CornerRadii {
bottom_right: 0.0,
bottom_left: 30.0,
..shape.radii
},
..shape
};
let (left, right) = mirrored.span_at(100.0).expect("the bottom edge is there");
assert!(
close(left, 30.0) && close(right, 100.0),
"got ({left}, {right})"
);
}
#[test]
fn a_corner_is_small_enough_to_ignore_only_once_it_is_on_screen() {
let scaled = placed_shape_to_rects(
PlacedShape::placed(
Rect::new(0.0, 0.0, 20.0, 20.0),
round(0.4),
1.0,
Transform::scale(8.0),
),
&[],
);
assert!(
!covers(&scaled, 0, 0),
"3 pixels of curve is a cut corner, not a rounding error"
);
let tiny = placed_shape_to_rects(
PlacedShape::placed(
Rect::new(0.0, 0.0, 100.0, 50.0),
round(0.4),
1.0,
Transform::IDENTITY,
),
&[],
);
assert_eq!(tiny.len(), 1, "one rectangle, as it has always been");
}
#[test]
fn a_corner_is_sampled_finely_enough_at_the_size_it_is_drawn() {
use crate::widgets::Corners;
let coverage = |scale: f32| {
let placement = Transform::scale(scale);
let shape = PlacedShape::placed(
Rect::new(0.0, 0.0, 120.0, 120.0),
round(40.0),
2.0,
placement,
);
let rects = placed_shape_to_rects(shape, &[]);
let local = Rect::new(0.0, 0.0, 120.0, 120.0);
let corners = Corners::superellipse(40.0, 2.0);
let to_local = placement.inverse().expect("invertible");
let inside = |x: i32, y: i32| {
let (lx, ly) = to_local.transform_point(x as f32 + 0.5, y as f32 + 0.5);
local.contains_shape(lx, ly, corners)
};
let covered: i64 = rects.iter().map(|r| r.width as i64 * r.height as i64).sum();
let side = (120.0 * scale) as i32 + 4;
let mut drawn = 0i64;
for y in -2..side {
for x in -2..side {
if inside(x, y) {
drawn += 1;
}
}
}
(covered, drawn)
};
let (covered, drawn) = coverage(1.0);
assert!(
covered * 1000 >= drawn * 985,
"at scale 1 the region covers {covered} of {drawn}"
);
let (covered, drawn) = coverage(8.0);
assert!(
covered * 1000 >= drawn * 995,
"at scale 8 the region covers {covered} of {drawn} — a corner eight \
times the size needs more points, not the same number spread \
further apart"
);
}
#[test]
fn sampling_a_corner_does_not_multiply_the_rectangles() {
let count = |k: f32| {
placed_shape_to_rects(
PlacedShape::placed(
Rect::new(0.0, 0.0, 300.0, 120.0),
round(40.0),
k,
Transform::IDENTITY,
),
&[],
)
.len()
};
let circle = count(1.0);
for k in [0.5_f32, 2.0] {
assert!(
count(k) <= circle * 2,
"k={k} publishes {} rectangles against the circle's {circle}",
count(k)
);
}
}
#[test]
fn a_region_is_inside_its_shape_and_almost_all_of_it() {
use crate::widgets::Corners;
let size = 120.0;
let placements = [
("turned", Transform::rotate_degrees(0.0)),
("turned", Transform::rotate_degrees(20.0)),
("turned", Transform::rotate_degrees(45.0)),
(
"sheared",
Transform::rotate_degrees(20.0).then(&Transform::scale_xy(2.0, 1.0)),
),
(
"sheared",
Transform::rotate_degrees(45.0).then(&Transform::scale_xy(4.0, 1.0)),
),
];
for k in [0.0_f32, 0.5, 1.0, 2.0, 5.0, 8.0, -1.0] {
for (degrees, placement) in placements {
let shape =
PlacedShape::placed(Rect::new(0.0, 0.0, size, size), round(40.0), k, placement);
let rects = placed_shape_to_rects(shape, &[]);
let local = Rect::new(0.0, 0.0, size, size);
let corners = Corners::superellipse(40.0, k);
let to_local = placement.inverse().expect("invertible");
let inside = |x: i32, y: i32| {
let (lx, ly) = to_local.transform_point(x as f32 + 0.5, y as f32 + 0.5);
local.contains_shape(lx, ly, corners)
};
let mut claimed_outside = 0;
let mut covered = 0;
for rect in &rects {
for y in rect.y..rect.y + rect.height {
for x in rect.x..rect.x + rect.width {
covered += 1;
if !inside(x, y) {
claimed_outside += 1;
}
}
}
}
let mut drawn = 0;
for y in -200..260 {
for x in -200..260 {
if inside(x, y) {
drawn += 1;
}
}
}
assert_eq!(
claimed_outside, 0,
"k={k} at {degrees}°: {claimed_outside} of {covered} published \
pixels are outside the shape, which has {drawn}"
);
assert!(
covered * 100 >= drawn * 97,
"k={k} at {degrees}°: the region covers {covered} of \
the shape's {drawn} pixels, and has given up too many"
);
}
}
}
#[test]
fn an_unequal_pair_of_corners_is_clamped_the_way_the_shader_clamps_it() {
use crate::widgets::{Corners, Rect as WRect};
for (w, h, radii) in [
(
120.0_f32,
60.0_f32,
CornerRadii {
top_left: 50.0,
top_right: 50.0,
bottom_right: 0.0,
bottom_left: 0.0,
},
),
(
120.0,
120.0,
CornerRadii {
top_left: 100.0,
top_right: 40.0,
bottom_right: 0.0,
bottom_left: 0.0,
},
),
(
60.0,
120.0,
CornerRadii {
top_left: 50.0,
top_right: 0.0,
bottom_right: 0.0,
bottom_left: 50.0,
},
),
] {
for k in [-1.0_f32, 0.0, 0.5, 1.0, 2.0] {
let shape =
PlacedShape::placed(WRect::new(0.0, 0.0, w, h), radii, k, Transform::IDENTITY);
let rects = placed_shape_to_rects(shape, &[]);
let local = WRect::new(0.0, 0.0, w, h);
let corners = Corners {
radii,
curvature: k,
};
let inside =
|x: i32, y: i32| local.contains_shape(x as f32 + 0.5, y as f32 + 0.5, corners);
let (mut covered, mut outside) = (0, 0);
for r in &rects {
for y in r.y..r.y + r.height {
for x in r.x..r.x + r.width {
covered += 1;
if !inside(x, y) {
outside += 1;
}
}
}
}
let mut drawn = 0;
for y in -50..250 {
for x in -50..250 {
if inside(x, y) {
drawn += 1;
}
}
}
assert_eq!(
outside, 0,
"k={k} {w}x{h} {radii:?}: {outside} of {covered} published \
pixels are outside a shape with {drawn}"
);
assert!(
covered * 100 >= drawn * 97,
"k={k} {w}x{h} {radii:?}: the region covers {covered} of the shape's \
{drawn} pixels — the corner is not the one the shader cuts"
);
}
}
}
#[test]
fn each_curvature_is_cut_as_the_curve_it_is() {
let corner = |curvature: f32| {
placed_shape_to_rects(
PlacedShape::placed(
Rect::new(0.0, 0.0, 100.0, 100.0),
round(40.0),
curvature,
Transform::IDENTITY,
),
&[],
)
};
assert!(
covers(&corner(1.0), 12, 12),
"the circle reaches this pixel"
);
assert!(
!covers(&corner(0.0), 12, 12),
"and the bevel does not, because the chord has already cut it"
);
assert!(covers(&corner(2.0), 12, 12));
assert!(
covers(&corner(2.0), 9, 9),
"a squircle reaches past the circle inscribed in it, and the \
region has to reach with it"
);
let area = |rects: Vec<RegionRect>| -> i64 {
rects.iter().map(|r| r.width as i64 * r.height as i64).sum()
};
assert!(
area(corner(0.0)) < area(corner(1.0)),
"a bevel takes more off the corner than a circle does"
);
let bevel = corner(0.0);
assert!(
!covers(&bevel, 18, 18),
"18 + 18 is short of the cut at 40, so the bevel has taken it"
);
assert!(
covers(&bevel, 25, 25),
"25 + 25 clears it, so the shape holds this one — a chord hung off \
the wrong end of either side does not run through x + y = 40 and \
gets one of these two wrong"
);
assert!(
covers(&bevel, 2, 45) && covers(&bevel, 45, 2),
"and just past each end of the chord the straight sides take over"
);
}
#[test]
fn a_scooped_corner_is_cut_outside_the_bite() {
let (w, h, r) = (120.0f32, 90.0f32, 28.0f32);
let rects = placed_shape_to_rects(
PlacedShape::placed(
Rect::new(0.0, 0.0, w, h),
round(r),
-1.0,
Transform::IDENTITY,
),
&[],
);
assert!(!rects.is_empty(), "a scooped box is not empty");
for rect in &rects {
for (px, py) in [
(rect.x, rect.y),
(rect.x + rect.width, rect.y),
(rect.x, rect.y + rect.height),
(rect.x + rect.width, rect.y + rect.height),
] {
let (px, py) = (px as f32, py as f32);
assert!(
(-0.5..=w + 0.5).contains(&px) && (-0.5..=h + 0.5).contains(&py),
"({px}, {py}) is outside the box"
);
for (cx, cy) in [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)] {
let d = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
assert!(
d >= r - 0.75,
"({px}, {py}) is {d} from the scooped corner ({cx}, {cy}), \
inside a bite of {r}"
);
}
}
}
assert!(covers(&rects, 60, 45));
}
#[test]
fn bands_that_round_alike_are_published_as_one_rectangle() {
let tall = placed_shape_to_rects(
PlacedShape::placed(
Rect::new(0.0, 0.0, 200.0, 900.0),
round(24.0),
1.0,
Transform::IDENTITY,
),
&[],
);
assert!(
tall.len() < 80,
"a 900-tall panel is tens of rectangles, not one per band — got {}",
tall.len()
);
assert!(covers(&tall, 100, 450), "and it still covers its middle");
assert!(!covers(&tall, 1, 1), "with its corners cut");
let square = placed_shape_to_rects(
PlacedShape::placed(
Rect::new(0.0, 0.0, 200.0, 900.0),
round(24.0),
1.0,
Transform::rotate_degrees(90.0).center_at(100.0, 450.0),
),
&[],
);
assert!(
square.len() < 80,
"a quarter turn is still an upright shape — got {}",
square.len()
);
}
#[test]
fn two_boxes_are_cut_to_what_they_share() {
let boxed = |x: f32, y: f32, w: f32, h: f32| {
PlacedShape::placed(Rect::new(x, y, w, h), round(0.0), 1.0, Transform::IDENTITY)
};
assert_eq!(
placed_shape_to_rects(
boxed(0.0, 0.0, 100.0, 100.0),
&[boxed(40.0, 30.0, 200.0, 200.0)]
),
vec![RegionRect {
x: 40,
y: 30,
width: 60,
height: 70
}],
"the overlap, on both axes at once"
);
assert!(
placed_shape_to_rects(boxed(0.0, 0.0, 50.0, 50.0), &[boxed(50.0, 0.0, 50.0, 50.0)])
.is_empty(),
"boxes that touch along an edge share no area"
);
assert!(
placed_shape_to_rects(
boxed(0.0, 0.0, 50.0, 50.0),
&[boxed(500.0, 0.0, 50.0, 50.0)]
)
.is_empty(),
"and ones that miss entirely leave nothing"
);
}
#[test]
fn a_corner_over_budget_is_cut_to_half_the_smaller_side() {
let cut = |w: f32, h: f32, radii: CornerRadii| {
placed_shape_to_rects(
PlacedShape::placed(Rect::new(0.0, 0.0, w, h), radii, 1.0, Transform::IDENTITY),
&[],
)
};
let pair = |a: f32, b: f32, c: f32, d: f32| CornerRadii {
top_left: a,
top_right: b,
bottom_right: c,
bottom_left: d,
};
for (label, rects, (x, y), inside) in [
(
"top pair, across the width",
cut(60.0, 200.0, pair(60.0, 60.0, 0.0, 0.0)),
(15, 5),
true,
),
(
"bottom pair, across the width",
cut(60.0, 200.0, pair(0.0, 0.0, 60.0, 60.0)),
(15, 195),
true,
),
(
"left pair, down the height",
cut(200.0, 60.0, pair(60.0, 0.0, 0.0, 60.0)),
(2, 15),
false,
),
(
"right pair, down the height",
cut(200.0, 60.0, pair(0.0, 60.0, 60.0, 0.0)),
(197, 15),
false,
),
] {
assert_eq!(
covers(&rects, x, y),
inside,
"{label}: the radius is cut to 30, and ({x}, {y}) is on the \
{} of the curve that leaves",
if inside { "inside" } else { "outside" }
);
}
}
#[test]
fn a_corner_the_clip_supplies_belongs_to_the_clip() {
let square_card = PlacedShape::placed(
Rect::new(-10.0, -10.0, 120.0, 120.0),
round(0.0),
1.0,
Transform::IDENTITY,
);
let rounded_scroller = PlacedShape::placed(
Rect::new(0.0, 0.0, 100.0, 100.0),
round(24.0),
1.0,
Transform::IDENTITY,
);
let rects = placed_shape_to_rects(square_card, &[rounded_scroller]);
assert!(
!covers(&rects, 0, 0) && !covers(&rects, 99, 99),
"the card declares square corners and the scroller cuts them anyway"
);
assert!(covers(&rects, 50, 50), "and the middle is still published");
}
#[test]
fn a_clip_squares_off_only_the_corners_it_cuts() {
let card = PlacedShape::placed(
Rect::new(0.0, 0.0, 100.0, 100.0),
round(20.0),
1.0,
Transform::IDENTITY,
);
let cuts_the_right = PlacedShape::placed(
Rect::new(-50.0, -50.0, 90.0, 200.0),
round(0.0),
1.0,
Transform::IDENTITY,
);
let rects = placed_shape_to_rects(card, &[cuts_the_right]);
assert!(
covers(&rects, 39, 0) && covers(&rects, 39, 99),
"the cut edge is straight, so its corners reach it"
);
assert!(
!covers(&rects, 0, 0) && !covers(&rects, 0, 99),
"and the card's own corners are still round"
);
}
#[test]
fn two_curves_over_one_another_leave_the_tighter() {
let same = Rect::new(0.0, 0.0, 100.0, 100.0);
let rounded = |r: f32| PlacedShape::placed(same, round(r), 1.0, Transform::IDENTITY);
let sharp_clip = placed_shape_to_rects(rounded(4.0), &[rounded(24.0)]);
assert!(
!covers(&sharp_clip, 2, 2),
"the clip rounds harder, so the clip's corner is what is left"
);
let sharp_shape = placed_shape_to_rects(rounded(24.0), &[rounded(4.0)]);
assert!(
!covers(&sharp_shape, 2, 2),
"and the other way round gives the same answer"
);
}
#[test]
fn a_clip_is_placed_before_it_cuts() {
let card = PlacedShape::placed(
Rect::new(0.0, 0.0, 100.0, 100.0),
round(0.0),
1.0,
Transform::translate(500.0, 300.0),
);
let clip = PlacedShape::placed(
Rect::new(0.0, 0.0, 100.0, 100.0),
round(0.0),
1.0,
Transform::translate(500.0, 300.0),
);
let rects = placed_shape_to_rects(card, &[clip]);
assert!(
covers(&rects, 550, 350),
"the clip covers the whole card, so nothing is cut"
);
let unplaced = PlacedShape {
curvature: 1.0,
placement: Transform::IDENTITY,
..clip
};
assert!(placed_shape_to_rects(card, &[unplaced]).is_empty());
}
#[test]
fn a_turned_square_does_not_claim_its_corners() {
let rects = placed_shape_to_rects(
shape(
Rect::new(0.0, 0.0, 100.0, 100.0),
round(0.0),
Transform::rotate_degrees(45.0).center_at(50.0, 50.0),
),
&[],
);
assert!(!rects.is_empty());
assert!(covers(&rects, 50, 20), "the top point is in the shape");
assert!(covers(&rects, 50, 50), "and so is the middle");
for (x, y) in [(-15, -15), (115, -15), (-15, 115), (115, 115)] {
assert!(
!covers(&rects, x, y),
"({x}, {y}) is a corner of the box, not of the shape"
);
}
}
#[test]
fn slabs_stay_inside_a_turned_shape() {
let (w, h) = (120.0f32, 60.0f32);
let transform = Transform::rotate_degrees(25.0).center_at(w / 2.0, h / 2.0);
let rects = placed_shape_to_rects(
PlacedShape {
rect: Rect::new(0.0, 0.0, w, h),
radii: round(0.0),
curvature: 1.0,
placement: transform,
},
&[],
);
assert!(!rects.is_empty());
let back = transform.inverse().expect("a rotation is invertible");
for rect in &rects {
for (px, py) in [
(rect.x, rect.y),
(rect.x + rect.width, rect.y),
(rect.x, rect.y + rect.height),
(rect.x + rect.width, rect.y + rect.height),
] {
let (lx, ly) = back.transform_point(px as f32, py as f32);
assert!(
lx >= -1.0 && ly >= -1.0 && lx <= w + 1.0 && ly <= h + 1.0,
"({px}, {py}) maps back to ({lx}, {ly}), outside the shape"
);
}
}
}
#[test]
fn a_turned_shape_claims_about_its_own_area() {
let (w, h) = (150.0f32, 90.0f32);
let rects = placed_shape_to_rects(
shape(
Rect::new(0.0, 0.0, w, h),
round(0.0),
Transform::rotate_degrees(20.0).center_at(w / 2.0, h / 2.0),
),
&[],
);
let covered: i64 = rects.iter().map(|r| r.width as i64 * r.height as i64).sum();
let shape = (w * h) as i64;
assert!(
covered <= shape,
"the rectangles inscribe the shape, so they cannot exceed its \
{shape} px — got {covered}"
);
assert!(
covered > shape * 9 / 10,
"and they should not give away much of it either — got {covered} \
of {shape}"
);
let boxed = {
let b = Transform::rotate_degrees(20.0)
.center_at(w / 2.0, h / 2.0)
.map_rect(Rect::new(0.0, 0.0, w, h));
(b.width * b.height) as i64
};
assert!(
boxed > shape * 17 / 10,
"the box this replaces is {boxed} px against the shape's {shape}"
);
}
#[test]
fn a_turned_rounded_shape_is_inside_itself_and_most_of_itself() {
let (w, h, r) = (110.0f32, 70.0f32, 16.0f32);
for degrees in [25.0f32, 45.0, 115.0, -30.0] {
let transform = Transform::rotate_degrees(degrees).center_at(w / 2.0, h / 2.0);
let rects = placed_shape_to_rects(
PlacedShape {
rect: Rect::new(0.0, 0.0, w, h),
radii: round(r),
curvature: 1.0,
placement: transform,
},
&[],
);
assert!(!rects.is_empty(), "at {degrees}°");
let back = transform.inverse().expect("a rotation is invertible");
let inside = |px: f32, py: f32| {
let (x, y) = back.transform_point(px, py);
let cx = x.clamp(r, w - r);
let cy = y.clamp(r, h - r);
((x - cx).powi(2) + (y - cy).powi(2)).sqrt() <= r + 1.0
&& (-1.0..=w + 1.0).contains(&x)
&& (-1.0..=h + 1.0).contains(&y)
};
for rect in &rects {
for (px, py) in [
(rect.x, rect.y),
(rect.x + rect.width, rect.y),
(rect.x, rect.y + rect.height),
(rect.x + rect.width, rect.y + rect.height),
] {
assert!(
inside(px as f32, py as f32),
"at {degrees}°, ({px}, {py}) is outside the shape"
);
}
}
let covered: i64 = rects.iter().map(|r| r.width as i64 * r.height as i64).sum();
let shape_area = (w * h - (4.0 - std::f32::consts::PI) * r * r) as i64;
assert!(
covered > shape_area * 9 / 10,
"at {degrees}°, {covered} px of a {shape_area} px shape"
);
}
}
#[test]
fn a_clip_narrows_a_turned_shape() {
let diamond = shape(
Rect::new(0.0, 0.0, 100.0, 100.0),
round(0.0),
Transform::rotate_degrees(45.0).center_at(50.0, 50.0),
);
let box_clip = |r: Rect| [shape(r, round(0.0), Transform::IDENTITY)];
let clipped =
placed_shape_to_rects(diamond, &box_clip(Rect::new(50.0, -50.0, 100.0, 200.0)));
assert!(
clipped.iter().all(|r| r.x >= 50),
"nothing reaches left of the clip"
);
assert!(
covers(&clipped, 60, 50),
"and what is on show is still there"
);
assert!(
placed_shape_to_rects(diamond, &box_clip(Rect::new(500.0, 0.0, 10.0, 10.0))).is_empty(),
"a clip that misses leaves nothing to publish"
);
}
#[test]
fn every_clip_in_a_chain_narrows_the_region() {
let card = shape(
Rect::new(0.0, 0.0, 200.0, 200.0),
round(0.0),
Transform::IDENTITY,
);
let circle = shape(
Rect::new(0.0, 0.0, 100.0, 100.0),
round(50.0),
Transform::IDENTITY,
);
let inset = shape(
Rect::new(10.0, 10.0, 80.0, 80.0),
round(0.0),
Transform::IDENTITY,
);
let rects = placed_shape_to_rects(card, &[inset, circle]);
assert!(covers(&rects, 50, 50), "inside both");
assert!(
covers(&rects, 50, 12),
"the inset's top edge, inside the circle"
);
assert!(
!covers(&rects, 12, 12),
"the inset's corner, which the circle has cut away"
);
assert!(!covers(&rects, 50, 5), "inside the circle, above the inset");
}
#[test]
fn a_region_under_clips_turned_apart_follows_both() {
use crate::renderer::clip::ClipTree;
use crate::renderer::{FlattenedCommand, RenderLayer};
let square = |placement| shape(Rect::new(0.0, 0.0, 100.0, 100.0), round(0.0), placement);
let turned = Transform::rotate_degrees(25.0).center_at(50.0, 50.0);
let mut tree = ClipTree::default();
let outer = tree.place(square(turned), None);
let inner = tree.place(square(Transform::IDENTITY), Some(outer));
let rect = Rect::new(-50.0, -50.0, 200.0, 200.0);
let cmd = FlattenedCommand {
command: std::rc::Rc::new(DrawCommand::InputRegion {
rect,
corner_radii: round(0.0),
curvature: 1.0,
takes: true,
}),
world_transform: Transform::IDENTITY,
layer: RenderLayer::Shapes,
opacity: 1.0,
clip: Some(tree.reference(inner)),
};
let rects = shape_of(&cmd, tree.chains(), rect, round(0.0), 1.0);
assert!(covers(&rects, 50, 50), "the middle, inside both");
assert!(
!covers(&rects, 2, 2),
"the upright square's corner, which the turned one does not reach"
);
assert!(
rects
.iter()
.all(|r| r.x >= 0 && r.y >= 0 && r.x + r.width <= 100),
"and nothing outside the upright square"
);
}
}