use std::cell::OnceCell;
use std::rc::Rc;
use tiny_skia::{FillRule, Mask, Path, Transform};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct Bounds {
pub x0: i32,
pub y0: i32,
pub x1: i32,
pub y1: i32,
}
impl Bounds {
pub const EMPTY: Bounds = Bounds {
x0: 0,
y0: 0,
x1: 0,
y1: 0,
};
pub fn surface(width: u32, height: u32) -> Bounds {
Bounds {
x0: 0,
y0: 0,
x1: width.min(i32::MAX as u32) as i32,
y1: height.min(i32::MAX as u32) as i32,
}
}
pub fn from_xywh(x: i32, y: i32, w: u32, h: u32) -> Bounds {
let clamp = |v: i64| v.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
Bounds {
x0: x,
y0: y,
x1: clamp(x as i64 + w as i64),
y1: clamp(y as i64 + h as i64),
}
}
pub fn covering(left: f32, top: f32, right: f32, bottom: f32, pad: f32) -> Bounds {
let clamp = |v: f32| v.clamp(-(1 << 30) as f32, (1 << 30) as f32) as i32;
if !(left <= right && top <= bottom) {
return Bounds::EMPTY;
}
Bounds {
x0: clamp((left - pad).floor()),
y0: clamp((top - pad).floor()),
x1: clamp((right + pad).ceil()),
y1: clamp((bottom + pad).ceil()),
}
}
pub fn is_empty(&self) -> bool {
self.x1 <= self.x0 || self.y1 <= self.y0
}
pub fn width(&self) -> usize {
(self.x1 - self.x0).max(0) as usize
}
pub fn height(&self) -> usize {
(self.y1 - self.y0).max(0) as usize
}
pub fn intersect(&self, other: &Bounds) -> Bounds {
let out = Bounds {
x0: self.x0.max(other.x0),
y0: self.y0.max(other.y0),
x1: self.x1.min(other.x1),
y1: self.y1.min(other.y1),
};
if out.is_empty() { Bounds::EMPTY } else { out }
}
pub fn union(&self, other: &Bounds) -> Bounds {
if self.is_empty() {
return *other;
}
if other.is_empty() {
return *self;
}
Bounds {
x0: self.x0.min(other.x0),
y0: self.y0.min(other.y0),
x1: self.x1.max(other.x1),
y1: self.y1.max(other.y1),
}
}
pub fn contains(&self, other: &Bounds) -> bool {
other.x0 >= self.x0 && other.y0 >= self.y0 && other.x1 <= self.x1 && other.y1 <= self.y1
}
}
pub(super) enum Skia<'a> {
Skip,
Draw(Option<&'a Mask>),
}
pub(super) struct Clip {
pub bounds: Bounds,
pub mask: Option<Rc<Mask>>,
exact: OnceCell<Mask>,
}
impl Clip {
fn new(bounds: Bounds, mask: Option<Rc<Mask>>) -> Clip {
Clip {
bounds,
mask,
exact: OnceCell::new(),
}
}
pub fn rect(
width: u32,
height: u32,
parent: Option<&Clip>,
(left, top, right, bottom): (f32, f32, f32, f32),
) -> Clip {
let surface = Bounds::surface(width, height);
let below = parent.map_or(surface, |clip| clip.bounds);
let snapped = if left <= right && top <= bottom {
let round = |v: f32| v.round().clamp(-(1 << 30) as f32, (1 << 30) as f32) as i32;
Bounds {
x0: round(left),
y0: round(top),
x1: round(right),
y1: round(bottom),
}
} else {
Bounds::EMPTY
};
let bounds = snapped.intersect(&below);
let mask = parent.and_then(|clip| clip.mask.clone());
Clip::new(bounds, if bounds.is_empty() { None } else { mask })
}
pub fn path(
width: u32,
height: u32,
parent: Option<&Clip>,
path: Option<&Path>,
transform: Transform,
) -> Option<Clip> {
let surface = Bounds::surface(width, height);
let below = parent.map_or(surface, |clip| clip.bounds);
let Some(path) = path else {
return Some(Clip::new(Bounds::EMPTY, None));
};
let shape = path.clone().transform(transform).map(|p| p.bounds());
let bounds = match shape {
Some(rect) => {
Bounds::covering(rect.left(), rect.top(), rect.right(), rect.bottom(), 1.0)
.intersect(&below)
}
None => Bounds::EMPTY,
};
if bounds.is_empty() {
return Some(Clip::new(Bounds::EMPTY, None));
}
let mut mask = Mask::new(width, height)?;
mask.fill_path(path, FillRule::Winding, true, transform);
if let Some(below) = parent.and_then(|clip| clip.mask.as_deref()) {
let (data, under) = (mask.data_mut(), below.data());
let stride = width as usize;
for y in bounds.y0 as usize..bounds.y1 as usize {
let (from, to) = (
y * stride + bounds.x0 as usize,
y * stride + bounds.x1 as usize,
);
for (a, b) in data[from..to].iter_mut().zip(&under[from..to]) {
*a = scale255(*a as u32, *b as u32) as u8;
}
}
}
Some(Clip::new(bounds, Some(Rc::new(mask))))
}
#[inline]
pub fn row(&self, y: i32, x0: i32, x1: i32) -> Option<&[u8]> {
self.mask.as_deref().map(|mask| {
let start = y as usize * mask.width() as usize;
&mask.data()[start + x0 as usize..start + x1 as usize]
})
}
pub fn for_skia(&self, area: Bounds, width: u32, height: u32) -> Skia<'_> {
let area = area.intersect(&Bounds::surface(width, height));
if area.intersect(&self.bounds).is_empty() {
return Skia::Skip;
}
if self.bounds.contains(&area) {
return Skia::Draw(self.mask.as_deref());
}
match self.exact(width, height) {
Some(mask) => Skia::Draw(Some(mask)),
None => Skia::Skip,
}
}
fn exact(&self, width: u32, height: u32) -> Option<&Mask> {
if self.exact.get().is_none() {
let mut mask = Mask::new(width, height)?;
let stride = width as usize;
let (x0, x1) = (self.bounds.x0 as usize, self.bounds.x1 as usize);
let data = mask.data_mut();
for y in self.bounds.y0 as usize..self.bounds.y1 as usize {
let row = &mut data[y * stride + x0..y * stride + x1];
match self.mask.as_deref() {
Some(own) => row.copy_from_slice(&own.data()[y * stride + x0..y * stride + x1]),
None => row.fill(255),
}
}
let _ = self.exact.set(mask);
}
self.exact.get()
}
}
pub(super) fn skia_clip(clip: Option<&Clip>, area: Bounds, width: u32, height: u32) -> Skia<'_> {
match clip {
Some(clip) => clip.for_skia(area, width, height),
None if area.intersect(&Bounds::surface(width, height)).is_empty() => Skia::Skip,
None => Skia::Draw(None),
}
}
pub(super) fn visible(clip: Option<&Clip>, area: Bounds, width: u32, height: u32) -> Bounds {
let area = area.intersect(&Bounds::surface(width, height));
match clip {
Some(clip) => area.intersect(&clip.bounds),
None => area,
}
}
#[inline]
pub(super) fn scale255(x: u32, a: u32) -> u32 {
let t = x * a + 128;
(t + (t >> 8)) >> 8
}
#[cfg(test)]
mod tests {
use super::*;
use tiny_skia::PathBuilder;
#[test]
fn bounds_intersect_and_union() {
let a = Bounds::from_xywh(0, 0, 10, 10);
let b = Bounds::from_xywh(5, 5, 10, 10);
assert_eq!(a.intersect(&b), Bounds::from_xywh(5, 5, 5, 5));
assert_eq!(a.union(&b), Bounds::from_xywh(0, 0, 15, 15));
assert!(a.intersect(&Bounds::from_xywh(20, 20, 1, 1)).is_empty());
assert_eq!(Bounds::EMPTY.union(&a), a);
assert!(a.contains(&Bounds::from_xywh(2, 2, 3, 3)));
assert!(!a.contains(&b));
}
#[test]
fn rect_clips_snap_and_nest_without_masks() {
let outer = Clip::rect(100, 100, None, (10.4, 10.6, 50.2, 60.0));
assert_eq!(outer.bounds, Bounds::from_xywh(10, 11, 40, 49));
assert!(outer.mask.is_none());
let inner = Clip::rect(100, 100, Some(&outer), (0.0, 0.0, 30.0, 200.0));
assert_eq!(inner.bounds, Bounds::from_xywh(10, 11, 20, 49));
let off = Clip::rect(100, 100, Some(&outer), (70.0, 70.0, 80.0, 80.0));
assert!(off.bounds.is_empty());
let wide = Clip::rect(100, 100, None, (-10.0, -10.0, 500.0, 500.0));
assert_eq!(wide.bounds, Bounds::surface(100, 100));
}
#[test]
fn path_clips_carry_a_mask_inside_their_parent() {
let mut pb = PathBuilder::new();
pb.push_circle(50.0, 50.0, 20.0);
let circle = pb.finish().unwrap();
let clip = Clip::path(100, 100, None, Some(&circle), Transform::identity()).unwrap();
let mask = clip.mask.as_deref().unwrap();
assert_eq!(mask.data()[50 * 100 + 50], 255);
assert_eq!(mask.data()[50 * 100 + 25], 0);
assert!(clip.bounds.contains(&Bounds::from_xywh(30, 30, 40, 40)));
let half = Clip::rect(100, 100, None, (0.0, 0.0, 50.0, 100.0));
let nested =
Clip::path(100, 100, Some(&half), Some(&circle), Transform::identity()).unwrap();
assert!(nested.bounds.x1 <= 50);
let gone = Clip::path(100, 100, None, None, Transform::identity()).unwrap();
assert!(gone.bounds.is_empty());
}
#[test]
fn skia_gets_a_mask_only_across_the_edge() {
let clip = Clip::rect(100, 100, None, (10.0, 10.0, 90.0, 90.0));
assert!(matches!(
clip.for_skia(Bounds::from_xywh(20, 20, 10, 10), 100, 100),
Skia::Draw(None)
));
assert!(matches!(
clip.for_skia(Bounds::from_xywh(95, 95, 3, 3), 100, 100),
Skia::Skip
));
match clip.for_skia(Bounds::from_xywh(0, 0, 50, 50), 100, 100) {
Skia::Draw(Some(mask)) => {
assert_eq!(mask.data()[5 * 100 + 5], 0);
assert_eq!(mask.data()[50 * 100 + 50], 255);
}
_ => panic!("expected an exact mask"),
}
}
}