Skip to main content

oxiui_render_wgpu/
clip.rs

1//! Nested clip-rectangle stack with intersection and integer-scissor output.
2//!
3//! The `ClipStack` tracks a hierarchy of rectangular clip regions.  Pushing a
4//! new rect intersects it with the current top so the effective clip region is
5//! always the intersection of all active rects.  The stack never panics on
6//! underflow — extra pops are silently ignored.
7
8// ── ClipRect ─────────────────────────────────────────────────────────────────
9
10/// An axis-aligned clip rectangle in logical (floating-point) coordinates.
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct ClipRect {
13    /// Left edge in logical pixels.
14    pub x: f32,
15    /// Top edge in logical pixels.
16    pub y: f32,
17    /// Width in logical pixels.
18    pub w: f32,
19    /// Height in logical pixels.
20    pub h: f32,
21}
22
23impl ClipRect {
24    /// Construct a [`ClipRect`] from origin and dimensions.
25    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
26        Self { x, y, w, h }
27    }
28
29    /// Compute the intersection of `self` and `other`.
30    ///
31    /// Returns `None` if the rectangles are disjoint.
32    pub fn intersect(&self, other: ClipRect) -> Option<ClipRect> {
33        let x0 = self.x.max(other.x);
34        let y0 = self.y.max(other.y);
35        let x1 = (self.x + self.w).min(other.x + other.w);
36        let y1 = (self.y + self.h).min(other.y + other.h);
37        if x1 > x0 && y1 > y0 {
38            Some(ClipRect {
39                x: x0,
40                y: y0,
41                w: x1 - x0,
42                h: y1 - y0,
43            })
44        } else {
45            None
46        }
47    }
48}
49
50// ── ClipStack ─────────────────────────────────────────────────────────────────
51
52/// A push-down stack of intersecting clip rectangles.
53///
54/// Each `push` intersects the new rect with the current top and stores the
55/// result.  Callers are therefore always looking at the *effective* clip, never
56/// the raw per-layer rect.
57#[derive(Debug, Default)]
58pub struct ClipStack {
59    /// Stack of intersected (effective) clip rects; top is the last element.
60    stack: Vec<ClipRect>,
61}
62
63impl ClipStack {
64    /// Construct an empty [`ClipStack`].
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// Push a new clip rect, intersecting it with the current top.
70    ///
71    /// If the stack is empty, `rect` is pushed directly.  If the intersection
72    /// with the current top is empty, nothing is pushed and the stack is
73    /// unchanged (the new layer would clip everything away anyway; the caller
74    /// should balance pushes with pops regardless).
75    pub fn push(&mut self, rect: ClipRect) {
76        let effective = match self.stack.last() {
77            None => rect,
78            Some(&top) => {
79                // If there is no intersection the new region is fully outside
80                // the current clip — push an empty rect so pop is still balanced.
81                top.intersect(rect)
82                    .unwrap_or(ClipRect::new(0.0, 0.0, 0.0, 0.0))
83            }
84        };
85        self.stack.push(effective);
86    }
87
88    /// Pop the topmost clip rect.  Does nothing (no panic) if the stack is empty.
89    pub fn pop(&mut self) {
90        self.stack.pop();
91    }
92
93    /// Return the current (topmost, effective) clip rect, or `None` if the
94    /// stack is empty.
95    pub fn current(&self) -> Option<&ClipRect> {
96        self.stack.last()
97    }
98
99    /// Return the current clip as integer `[x, y, w, h]` rounded **outward**
100    /// (floor on origin, ceil on extent).
101    ///
102    /// Returns `None` if the stack is empty.  Negative components are clamped
103    /// to zero before the conversion.
104    pub fn as_scissor(&self) -> Option<[u32; 4]> {
105        let clip = self.stack.last()?;
106        let x = clip.x.floor().max(0.0) as u32;
107        let y = clip.y.floor().max(0.0) as u32;
108        // Extent rounded outward.
109        let right = (clip.x + clip.w).ceil().max(0.0) as u32;
110        let bottom = (clip.y + clip.h).ceil().max(0.0) as u32;
111        let w = right.saturating_sub(x);
112        let h = bottom.saturating_sub(y);
113        Some([x, y, w, h])
114    }
115}
116
117// ── Tests ─────────────────────────────────────────────────────────────────────
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn clip_push_pop_intersection() {
125        let mut stack = ClipStack::new();
126        stack.push(ClipRect::new(0.0, 0.0, 100.0, 100.0));
127        stack.push(ClipRect::new(10.0, 10.0, 50.0, 50.0));
128        let cur = stack.current().copied().expect("stack must not be empty");
129        assert!((cur.x - 10.0).abs() < 0.001);
130        assert!((cur.y - 10.0).abs() < 0.001);
131        assert!((cur.w - 50.0).abs() < 0.001);
132        assert!((cur.h - 50.0).abs() < 0.001);
133        stack.pop();
134        let after_pop = stack.current().copied().expect("stack must not be empty");
135        assert!((after_pop.x - 0.0).abs() < 0.001);
136        assert!((after_pop.w - 100.0).abs() < 0.001);
137    }
138
139    #[test]
140    fn clip_underflow_is_noop() {
141        let mut stack = ClipStack::new();
142        // Pop on empty must not panic.
143        stack.pop();
144        stack.pop();
145        assert!(stack.current().is_none());
146        // After spurious pops we can still use the stack normally.
147        stack.push(ClipRect::new(0.0, 0.0, 10.0, 10.0));
148        assert!(stack.current().is_some());
149    }
150
151    #[test]
152    fn clip_as_scissor_rounds_outward() {
153        let mut stack = ClipStack::new();
154        // Fractional rect: x=1.2, y=2.7, w=10.1, h=5.3 → right=11.3, bottom=8.0
155        // Expected scissor: x=floor(1.2)=1, y=floor(2.7)=2,
156        //                   right=ceil(11.3)=12, bottom=ceil(8.0)=8
157        //                   w=12-1=11, h=8-2=6
158        stack.push(ClipRect::new(1.2, 2.7, 10.1, 5.3));
159        let scissor = stack.as_scissor().expect("scissor must be Some");
160        assert_eq!(scissor[0], 1, "x should be floor(1.2)=1");
161        assert_eq!(scissor[1], 2, "y should be floor(2.7)=2");
162        assert_eq!(scissor[2], 11, "w should be ceil(11.3)-1=11");
163        assert_eq!(scissor[3], 6, "h should be ceil(8.0)-2=6");
164    }
165}