use std::cell::Cell;
use std::rc::Rc;
use crate::shape::PlacedShape;
use crate::widgets::Rect;
pub const NO_CHAIN: u32 = u32::MAX;
pub(crate) const MAX_CHAIN: u32 = 64;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ResolvedClip {
pub first: PlacedShape,
pub rest: u32,
pub open: Rect,
}
impl ResolvedClip {
pub(crate) fn alone(first: PlacedShape) -> Self {
Self {
first,
rest: NO_CHAIN,
open: first.world_aabb(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct ClipLink {
pub(crate) shape: PlacedShape,
pub(crate) next: u32,
depth: u32,
}
#[derive(Debug, Default)]
pub struct ClipChains {
links: Vec<ClipLink>,
}
impl ClipChains {
pub(crate) fn links(&self) -> &[ClipLink] {
&self.links
}
pub(crate) fn shapes<'a>(
&'a self,
clip: &'a ResolvedClip,
) -> impl Iterator<Item = &'a PlacedShape> + 'a {
std::iter::once(&clip.first).chain(self.chain(clip.rest))
}
fn chain(&self, rest: u32) -> impl Iterator<Item = &PlacedShape> + '_ {
let mut at = rest;
std::iter::from_fn(move || {
let link = self.links.get(at as usize)?;
at = link.next;
Some(&link.shape)
})
}
fn push(&mut self, shape: PlacedShape, next: u32) -> u32 {
let index = self.links.len() as u32;
let depth = 1 + self.depth(next);
if depth == MAX_CHAIN + 1 {
log::warn!(
"clips nested {depth} deep without merging: the shaders walk {MAX_CHAIN} \
of them, and the outermost are not cut by"
);
}
self.links.push(ClipLink { shape, next, depth });
index
}
pub(crate) fn depth(&self, rest: u32) -> u32 {
self.links.get(rest as usize).map_or(0, |link| link.depth)
}
}
#[derive(Debug, Clone)]
pub struct ClipRef(Rc<Cell<ResolvedClip>>);
impl ClipRef {
pub fn resolved(&self) -> ResolvedClip {
self.0.get()
}
#[cfg(test)]
pub(crate) fn placed(shape: PlacedShape) -> Self {
Self::holding(ResolvedClip::alone(shape))
}
fn holding(resolved: ResolvedClip) -> Self {
Self(Rc::new(Cell::new(resolved)))
}
fn place(&self, resolved: ResolvedClip) {
self.0.set(resolved);
}
pub(crate) fn is(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ClipIndex(usize);
struct Placed {
reference: ClipRef,
own: PlacedShape,
under: Option<ClipIndex>,
link: u32,
}
#[derive(Default)]
pub(crate) struct ClipTree {
placed: Vec<Placed>,
chains: ClipChains,
}
impl ClipTree {
pub(crate) fn mark(&self) -> usize {
self.placed.len()
}
pub(crate) fn clear(&mut self) {
let Self { placed, chains } = self;
placed.clear();
chains.links.clear();
}
pub(crate) fn capacity(&self) -> usize {
self.placed.capacity()
}
pub(crate) fn link_capacity(&self) -> usize {
self.chains.links.capacity()
}
pub(crate) fn chains(&self) -> &ClipChains {
&self.chains
}
pub(crate) fn reference(&self, index: ClipIndex) -> ClipRef {
self.placed[index.0].reference.clone()
}
fn cut(&mut self, own: PlacedShape, under: Option<ClipIndex>) -> ResolvedClip {
let Some(parent) = under else {
return ResolvedClip::alone(own);
};
let above = &mut self.placed[parent.0];
let parent = above.reference.resolved();
let open = parent
.open
.intersection(&own.world_aabb())
.unwrap_or_default();
match merge_exact(&parent.first, &own) {
Some(first) => ResolvedClip {
first,
rest: parent.rest,
open,
},
None => {
if above.link == NO_CHAIN {
above.link = self.chains.push(parent.first, parent.rest);
}
ResolvedClip {
first: own,
rest: above.link,
open,
}
}
}
}
fn push(
&mut self,
reference: ClipRef,
own: PlacedShape,
under: Option<ClipIndex>,
) -> ClipIndex {
let index = ClipIndex(self.placed.len());
self.placed.push(Placed {
reference,
own,
under,
link: NO_CHAIN,
});
index
}
pub(crate) fn place(&mut self, own: PlacedShape, under: Option<ClipIndex>) -> ClipIndex {
let held = self.cut(own, under);
self.push(ClipRef::holding(held), own, under)
}
pub(crate) fn replay(
&mut self,
clips: &[CachedClip],
dx: f32,
dy: f32,
inherited: Option<ClipIndex>,
) {
let start = self.placed.len();
for clip in clips {
let own = clip.own.translated(dx, dy);
let under = match clip.parent {
CachedParent::Within(offset) => Some(ClipIndex(start + offset)),
CachedParent::Inherited => inherited,
};
let held = self.cut(own, under);
clip.reference.place(held);
self.push(clip.reference.clone(), own, under);
}
}
pub(crate) fn since(&self, mark: usize) -> Vec<CachedClip> {
self.placed[mark..]
.iter()
.map(|placed| CachedClip {
reference: placed.reference.clone(),
own: placed.own,
parent: match placed.under {
Some(ClipIndex(index)) if index >= mark => CachedParent::Within(index - mark),
_ => CachedParent::Inherited,
},
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct CachedClip {
pub(crate) reference: ClipRef,
pub(crate) own: PlacedShape,
pub(crate) parent: CachedParent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CachedParent {
Within(usize),
Inherited,
}
const SAME_POINT: f32 = 1e-3;
pub(crate) fn merge_exact(parent: &PlacedShape, own: &PlacedShape) -> Option<PlacedShape> {
let own = rebase_exactly(own, parent)?;
let rect = parent.rect.intersection(&own.rect).unwrap_or_default();
let merged = |radii: [f32; 4], curvature: f32| PlacedShape {
rect,
radii: radii.into(),
curvature,
placement: parent.placement,
};
let clips = [parent, &own].map(|clip| {
let radii = clip.clamped_radii().to_array();
(
clip,
corners(clip.rect),
radii,
radii.iter().any(|&r| r > 0.0),
)
});
if rect.width <= 0.0 || rect.height <= 0.0 || clips.iter().all(|&(.., rounded)| !rounded) {
return Some(merged([0.0; 4], parent.curvature));
}
let half = rect.width.min(rect.height) * 0.5;
let mut radii = [0.0; 4];
let mut curvature = None;
for (corner, (px, py)) in corners(rect).into_iter().enumerate() {
let mut cut: Option<(f32, f32)> = None;
for &(clip, clip_corners, clip_radii, rounded) in &clips {
if !rounded || clip.distance(px, py) <= SAME_POINT {
continue;
}
let (cx, cy) = clip_corners[corner];
if (cx - px).abs() > SAME_POINT || (cy - py).abs() > SAME_POINT {
return None;
}
let radius = clip_radii[corner];
if radius > half {
return None;
}
let this = (radius, clip.curvature);
cut = Some(match cut {
None => this,
Some(other) => deeper(other, this)?,
});
}
if let Some((radius, k)) = cut {
match curvature {
None => curvature = Some(k),
Some(seen) if same_curve(seen, k) => {}
Some(_) => return None,
}
radii[corner] = radius;
}
}
Some(merged(radii, curvature.unwrap_or(parent.curvature)))
}
fn corners(rect: Rect) -> [(f32, f32); 4] {
let (x1, y1) = (rect.x + rect.width, rect.y + rect.height);
[(rect.x, rect.y), (x1, rect.y), (x1, y1), (rect.x, y1)]
}
fn deeper(a: (f32, f32), b: (f32, f32)) -> Option<(f32, f32)> {
let ((ra, ka), (rb, kb)) = (a, b);
match (ka < 0.0, kb < 0.0) {
(true, true) => Some(if ra >= rb { a } else { b }),
(false, false) if ra >= rb && ka <= kb => Some(a),
(false, false) if rb >= ra && kb <= ka => Some(b),
_ => None,
}
}
fn same_curve(a: f32, b: f32) -> bool {
a == b || (a < 0.0 && b < 0.0)
}
fn rebase_exactly(clip: &PlacedShape, onto: &PlacedShape) -> Option<PlacedShape> {
if clip.placement == onto.placement {
return Some(*clip);
}
let between = onto.placement.inverse()?.then(&clip.placement);
if !between.rect_stays_rect() {
return None;
}
let (sx, sy) = between.extract_scale_components();
if (sx - sy).abs() > sx.max(sy) * 1e-5 {
return None;
}
Some(PlacedShape {
rect: between.map_rect(clip.rect),
radii: clip.radii.scaled(sx).permuted(between.corner_permutation()),
curvature: clip.curvature,
placement: onto.placement,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::renderer::CornerRadii;
use crate::transform::Transform;
fn clip(rect: Rect, radii: f32, curvature: f32) -> PlacedShape {
PlacedShape::placed(
rect,
CornerRadii::uniform(radii),
curvature,
Transform::IDENTITY,
)
}
fn square(x: f32, y: f32, side: f32) -> Rect {
Rect::new(x, y, side, side)
}
fn one_corner(top_left: f32, bottom_right: f32) -> CornerRadii {
CornerRadii {
top_left,
top_right: 0.0,
bottom_right,
bottom_left: 0.0,
}
}
#[test]
fn a_square_clip_over_a_rounded_one_keeps_its_corners() {
let rounded = clip(square(0.0, 0.0, 100.0), 20.0, 1.0);
let squared = clip(square(0.0, 0.0, 100.0), 0.0, 1.0);
for (parent, own) in [(rounded, squared), (squared, rounded)] {
let merged = merge_exact(&parent, &own).expect("one rounded rect says it exactly");
assert_eq!(merged.rect, square(0.0, 0.0, 100.0));
assert_eq!(
merged.radii,
CornerRadii::uniform(20.0),
"the square clip cuts none of the corners the rounded one does"
);
}
}
#[test]
fn a_larger_square_clip_leaves_the_rounded_one_as_it_is() {
let rounded = clip(square(0.0, 0.0, 100.0), 20.0, 1.0);
let larger = clip(square(-50.0, -50.0, 200.0), 0.0, 1.0);
for (parent, own) in [(rounded, larger), (larger, rounded)] {
let merged = merge_exact(&parent, &own).expect("exact");
assert_eq!(merged.rect, square(0.0, 0.0, 100.0));
assert_eq!(merged.radii, CornerRadii::uniform(20.0));
}
}
#[test]
fn a_square_clip_inside_the_rounded_one_stays_square() {
let rounded = clip(square(0.0, 0.0, 100.0), 20.0, 1.0);
let inside = clip(square(10.0, 10.0, 80.0), 0.0, 1.0);
let merged = merge_exact(&rounded, &inside).expect("exact");
assert_eq!(merged.rect, square(10.0, 10.0, 80.0));
assert_eq!(merged.radii, CornerRadii::uniform(0.0));
}
#[test]
fn a_circle_over_a_squircle_on_the_same_box_is_the_circle() {
let circle = clip(square(0.0, 0.0, 100.0), 50.0, 1.0);
for radius in [12.0, 40.0] {
let squircle = clip(square(0.0, 0.0, 100.0), radius, 2.0);
for (parent, own) in [(circle, squircle), (squircle, circle)] {
let merged = merge_exact(&parent, &own).expect("the circle nests the squircle");
assert_eq!(merged.radii, CornerRadii::uniform(50.0), "radius {radius}");
assert_eq!(merged.curvature, 1.0);
}
}
}
#[test]
fn a_squircle_inset_in_a_circle_is_chained() {
let circle = clip(square(0.0, 0.0, 100.0), 50.0, 1.0);
let inset = clip(square(10.0, 10.0, 80.0), 12.0, 2.0);
assert_eq!(merge_exact(&circle, &inset), None);
assert_eq!(merge_exact(&inset, &circle), None);
}
#[test]
fn crossing_corners_are_chained() {
let squircle = clip(square(0.0, 0.0, 100.0), 30.0, 2.0);
let circle = clip(square(0.0, 0.0, 100.0), 20.0, 1.0);
assert_eq!(merge_exact(&squircle, &circle), None);
assert_eq!(merge_exact(&circle, &squircle), None);
}
#[test]
fn two_curvatures_among_the_corners_are_chained() {
let top_left = PlacedShape {
radii: one_corner(20.0, 0.0),
..clip(square(0.0, 0.0, 100.0), 0.0, 1.0)
};
let bottom_right = PlacedShape {
radii: one_corner(0.0, 20.0),
..clip(square(0.0, 0.0, 100.0), 0.0, 2.0)
};
assert_eq!(merge_exact(&top_left, &bottom_right), None);
let same_curve = PlacedShape {
curvature: 1.0,
..bottom_right
};
let merged = merge_exact(&top_left, &same_curve).expect("one curve, two corners");
assert_eq!(merged.radii, one_corner(20.0, 20.0));
}
#[test]
fn a_corner_larger_than_the_intersection_holds_is_chained() {
let rounded = clip(square(0.0, 0.0, 100.0), 40.0, 1.0);
let strip = clip(Rect::new(0.0, 0.0, 100.0, 50.0), 0.0, 1.0);
assert_eq!(merge_exact(&rounded, &strip), None);
}
#[test]
fn a_turn_a_skew_or_an_uneven_scale_between_the_two_is_chained() {
let outer = clip(square(0.0, 0.0, 100.0), 20.0, 1.0);
for placement in [
Transform::rotate_degrees(25.0),
Transform::rotate_degrees(25.0).then(&Transform::scale_xy(3.0, 1.0)),
Transform::scale_xy(0.5, 1.0),
] {
let inner = PlacedShape {
placement,
..clip(square(10.0, 10.0, 40.0), 0.0, 1.0)
};
assert_eq!(merge_exact(&outer, &inner), None, "{placement:?}");
}
}
#[test]
fn a_quarter_turn_and_a_uniform_scale_are_merged() {
let outer = clip(square(0.0, 0.0, 100.0), 0.0, 1.0);
let inner = PlacedShape {
radii: one_corner(10.0, 0.0),
placement: Transform::translate(100.0, 0.0)
.then(&Transform::rotate_degrees(90.0))
.then(&Transform::scale(2.0)),
..clip(square(0.0, 0.0, 50.0), 0.0, 1.0)
};
let merged = merge_exact(&outer, &inner).expect("a rect, and circles, either way");
assert!(
(merged.rect.width - 100.0).abs() < 1e-3,
"{:?}",
merged.rect
);
assert!(
(merged.radii.top_right - 20.0).abs() < 1e-3,
"turned a quarter clockwise and doubled: {:?}",
merged.radii
);
}
#[test]
fn two_clips_in_one_turned_space_merge_in_it() {
let placement = Transform::rotate_degrees(30.0);
let outer = PlacedShape {
placement,
..clip(square(0.0, 0.0, 100.0), 8.0, 1.0)
};
let inner = PlacedShape {
placement,
..clip(Rect::new(20.0, 0.0, 100.0, 60.0), 16.0, 1.0)
};
let both = merge_exact(&outer, &inner).expect("one space, one rounded rect");
assert_eq!(both.placement, placement, "still turned by the same thing");
assert_eq!(both.rect, Rect::new(20.0, 0.0, 80.0, 60.0));
assert_eq!(
both.radii,
CornerRadii {
top_left: 16.0,
top_right: 8.0,
bottom_right: 0.0,
bottom_left: 16.0,
},
"the inner clip's own corners on the left, the outer's on the right, \
and a square corner where an edge of each meets"
);
}
#[test]
fn a_clip_is_merged_through_a_translation() {
let outer = clip(square(0.0, 0.0, 100.0), 0.0, 1.0);
let inner = PlacedShape {
placement: Transform::translate(40.0, 0.0),
..outer
};
let both = merge_exact(&outer, &inner).expect("a translation loses nothing");
assert_eq!(
both.placement,
Transform::IDENTITY,
"rebased onto the first"
);
assert_eq!((both.rect.x, both.rect.width), (40.0, 60.0));
}
#[test]
fn a_mirrored_clip_is_merged_with_its_corners_moved() {
let outer = PlacedShape {
placement: Transform::rotate_degrees(20.0),
..clip(square(0.0, 0.0, 100.0), 20.0, 1.0)
};
let inner = PlacedShape {
radii: CornerRadii {
top_left: 9.0,
top_right: 0.0,
bottom_right: 0.0,
bottom_left: 5.0,
},
placement: outer
.placement
.then(&Transform::translate(100.0, 0.0).then(&Transform::scale_xy(-1.0, 1.0))),
..clip(square(0.0, 0.0, 80.0), 0.0, 1.0)
};
let both = merge_exact(&outer, &inner).expect("a mirror loses nothing");
assert_eq!(both.placement, outer.placement);
assert!(
(both.radii.bottom_right - 5.0).abs() < 1e-3,
"the inner's bottom-left, mirrored: {:?}",
both.radii
);
assert!(
(both.radii.top_right - 20.0).abs() < 1e-3,
"and where the inner's mirrored 9 shares a corner with the outer's \
20, the 20 cuts deeper: {:?}",
both.radii
);
}
#[test]
fn a_region_under_two_clips_a_quarter_turn_apart_rounds_the_right_corner() {
let covers = |rects: &[crate::region::RegionRect], x: i32, y: i32| {
rects
.iter()
.any(|r| x >= r.x && y >= r.y && x < r.x + r.width && y < r.y + r.height)
};
let outer = clip(square(0.0, 0.0, 100.0), 0.0, 1.0);
let inner = PlacedShape {
radii: CornerRadii {
top_left: 40.0,
top_right: 0.0,
bottom_right: 0.0,
bottom_left: 0.0,
},
placement: Transform::translate(100.0, 0.0).then(&Transform::rotate_degrees(90.0)),
..outer
};
let both = merge_exact(&outer, &inner).expect("a quarter turn loses nothing");
let rects = crate::region::placed_shape_to_rects(both, &[]);
assert!(
covers(&rects, 3, 3),
"the rounding turned a quarter clockwise off the top-left, so this \
corner is square and the surface still claims it"
);
assert!(
!covers(&rects, 96, 3),
"and the top-right is where it landed, so the region gives that \
wedge up"
);
}
#[test]
fn two_clips_that_miss_merge_into_nothing() {
let here = clip(square(0.0, 0.0, 50.0), 10.0, 1.0);
let far = clip(square(500.0, 500.0, 50.0), 0.0, 1.0);
let merged = merge_exact(&here, &far).expect("nothing is exact");
assert_eq!((merged.rect.width, merged.rect.height), (0.0, 0.0));
}
#[test]
fn a_clip_that_cannot_merge_is_chained_to_its_parent() {
let mut tree = ClipTree::default();
let card = clip(square(0.0, 0.0, 100.0), 50.0, 1.0);
let inset = clip(square(10.0, 10.0, 80.0), 12.0, 2.0);
let row = clip(Rect::new(10.0, 40.0, 80.0, 20.0), 0.0, 1.0);
let outer = tree.place(card, None);
let middle = tree.place(inset, Some(outer));
let inner = tree.place(row, Some(middle));
assert_eq!(tree.reference(outer).resolved(), ResolvedClip::alone(card));
let chained = tree.reference(middle).resolved();
assert_eq!(chained.first, inset);
assert_eq!(
tree.chains()
.chain(chained.rest)
.copied()
.collect::<Vec<_>>(),
vec![card]
);
let merged = tree.reference(inner).resolved();
assert_eq!(
merged.first.rect, row.rect,
"inside the squircle, so merged"
);
assert_eq!(merged.rest, chained.rest, "and under the same chain");
assert_eq!(tree.chains().links().len(), 1, "one link, shared");
}
#[test]
fn a_clip_under_nothing_has_no_chain() {
let mut tree = ClipTree::default();
let ripple = clip(square(10.0, 10.0, 80.0), 12.0, 2.0);
let alone = tree.place(ripple, None);
assert_eq!(
tree.reference(alone).resolved(),
ResolvedClip::alone(ripple)
);
assert!(tree.chains().links().is_empty());
}
#[test]
fn a_replay_chains_its_clips_again() {
let card = clip(square(0.0, 0.0, 100.0), 50.0, 1.0);
let inset = clip(square(10.0, 10.0, 80.0), 12.0, 2.0);
let mut tree = ClipTree::default();
let outer = tree.place(card, None);
let mark = tree.mark();
let inner = tree.place(inset, Some(outer));
let reference = tree.reference(inner);
let cached = tree.since(mark);
tree.clear();
assert!(tree.chains().links().is_empty());
let other = tree.place(card, None);
tree.place(inset, Some(other));
let moved = card.translated(5.0, 0.0);
let outer = tree.place(moved, None);
tree.replay(&cached, 5.0, 0.0, Some(outer));
let resolved = reference.resolved();
assert_eq!(resolved.first, inset.translated(5.0, 0.0));
assert_eq!(
tree.chains()
.chain(resolved.rest)
.copied()
.collect::<Vec<_>>(),
vec![moved],
"under the card where it is now, through this frame's link"
);
}
#[test]
fn siblings_chained_to_one_clip_share_its_link() {
let mut tree = ClipTree::default();
let card = tree.place(clip(square(0.0, 0.0, 100.0), 50.0, 1.0), None);
let rests: Vec<u32> = (0..3)
.map(|_| {
let inset = tree.place(clip(square(10.0, 10.0, 80.0), 12.0, 2.0), Some(card));
tree.reference(inset).resolved().rest
})
.collect();
assert_eq!(rests, vec![0, 0, 0]);
assert_eq!(tree.chains().links().len(), 1);
}
#[test]
fn a_chain_leaves_open_the_box_all_its_shapes_do() {
let mut tree = ClipTree::default();
let card = clip(square(0.0, 0.0, 100.0), 50.0, 1.0);
let wide = clip(Rect::new(10.0, 10.0, 200.0, 80.0), 12.0, 2.0);
let outer = tree.place(card, None);
let inner = tree.place(wide, Some(outer));
let resolved = tree.reference(inner).resolved();
assert_ne!(resolved.rest, NO_CHAIN, "chained, so the box is a question");
assert_eq!(resolved.open, Rect::new(10.0, 10.0, 90.0, 80.0));
assert_eq!(tree.chains().shapes(&resolved).count(), 2);
}
#[test]
fn a_chain_counts_its_links() {
let mut tree = ClipTree::default();
let mut parent = None;
let mut last = None;
for step in 0..=MAX_CHAIN + 1 {
let inset = step as f32;
let shape = clip(square(inset, inset, 1000.0 - 2.0 * inset), 400.0, 1.0);
let index = tree.place(shape, parent);
parent = Some(index);
last = Some(index);
}
let rest = tree.reference(last.unwrap()).resolved().rest;
assert_eq!(tree.chains().depth(rest), MAX_CHAIN + 1);
assert_eq!(tree.chains().depth(NO_CHAIN), 0);
}
#[test]
fn the_cap_is_the_shaders() {
let shader = include_str!("shader.wgsl");
assert!(
shader.contains(&format!("const MAX_CHAIN: u32 = {MAX_CHAIN}u;")),
"shader.wgsl walks a different number of links than MAX_CHAIN says"
);
}
#[test]
fn clearing_keeps_the_links_room() {
let mut tree = ClipTree::default();
for _ in 0..8 {
let outer = tree.place(clip(square(0.0, 0.0, 100.0), 50.0, 1.0), None);
tree.place(clip(square(10.0, 10.0, 80.0), 12.0, 2.0), Some(outer));
}
let room = tree.link_capacity();
assert!(room >= 8);
tree.clear();
assert_eq!(tree.link_capacity(), room);
}
}