#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ClipRect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl ClipRect {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self { x, y, w, h }
}
pub fn intersect(&self, other: ClipRect) -> Option<ClipRect> {
let x0 = self.x.max(other.x);
let y0 = self.y.max(other.y);
let x1 = (self.x + self.w).min(other.x + other.w);
let y1 = (self.y + self.h).min(other.y + other.h);
if x1 > x0 && y1 > y0 {
Some(ClipRect {
x: x0,
y: y0,
w: x1 - x0,
h: y1 - y0,
})
} else {
None
}
}
}
#[derive(Debug, Default)]
pub struct ClipStack {
stack: Vec<ClipRect>,
}
impl ClipStack {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, rect: ClipRect) {
let effective = match self.stack.last() {
None => rect,
Some(&top) => {
top.intersect(rect)
.unwrap_or(ClipRect::new(0.0, 0.0, 0.0, 0.0))
}
};
self.stack.push(effective);
}
pub fn pop(&mut self) {
self.stack.pop();
}
pub fn current(&self) -> Option<&ClipRect> {
self.stack.last()
}
pub fn as_scissor(&self) -> Option<[u32; 4]> {
let clip = self.stack.last()?;
let x = clip.x.floor().max(0.0) as u32;
let y = clip.y.floor().max(0.0) as u32;
let right = (clip.x + clip.w).ceil().max(0.0) as u32;
let bottom = (clip.y + clip.h).ceil().max(0.0) as u32;
let w = right.saturating_sub(x);
let h = bottom.saturating_sub(y);
Some([x, y, w, h])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clip_push_pop_intersection() {
let mut stack = ClipStack::new();
stack.push(ClipRect::new(0.0, 0.0, 100.0, 100.0));
stack.push(ClipRect::new(10.0, 10.0, 50.0, 50.0));
let cur = stack.current().copied().expect("stack must not be empty");
assert!((cur.x - 10.0).abs() < 0.001);
assert!((cur.y - 10.0).abs() < 0.001);
assert!((cur.w - 50.0).abs() < 0.001);
assert!((cur.h - 50.0).abs() < 0.001);
stack.pop();
let after_pop = stack.current().copied().expect("stack must not be empty");
assert!((after_pop.x - 0.0).abs() < 0.001);
assert!((after_pop.w - 100.0).abs() < 0.001);
}
#[test]
fn clip_underflow_is_noop() {
let mut stack = ClipStack::new();
stack.pop();
stack.pop();
assert!(stack.current().is_none());
stack.push(ClipRect::new(0.0, 0.0, 10.0, 10.0));
assert!(stack.current().is_some());
}
#[test]
fn clip_as_scissor_rounds_outward() {
let mut stack = ClipStack::new();
stack.push(ClipRect::new(1.2, 2.7, 10.1, 5.3));
let scissor = stack.as_scissor().expect("scissor must be Some");
assert_eq!(scissor[0], 1, "x should be floor(1.2)=1");
assert_eq!(scissor[1], 2, "y should be floor(2.7)=2");
assert_eq!(scissor[2], 11, "w should be ceil(11.3)-1=11");
assert_eq!(scissor[3], 6, "h should be ceil(8.0)-2=6");
}
}