Skip to main content

oxiui_render_soft/
clip.rs

1//! Rectangular clip-region stack.
2//!
3//! Clip rectangles use integer pixel bounds `[x0, x1) × [y0, y1)`. Pushing a
4//! new clip intersects it with the current clip, so nested clips never expand
5//! the drawable region.
6
7/// An integer-bounded clip rectangle: `[x0, x1) × [y0, y1)`.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct ClipRect {
10    /// Inclusive left bound.
11    pub x0: i64,
12    /// Inclusive top bound.
13    pub y0: i64,
14    /// Exclusive right bound.
15    pub x1: i64,
16    /// Exclusive bottom bound.
17    pub y1: i64,
18}
19
20impl ClipRect {
21    /// A clip covering the whole framebuffer `[0, width) × [0, height)`.
22    pub fn full(width: u32, height: u32) -> Self {
23        Self {
24            x0: 0,
25            y0: 0,
26            x1: width as i64,
27            y1: height as i64,
28        }
29    }
30
31    /// Construct from a position and size (negative sizes yield an empty clip).
32    pub fn from_rect(x: i64, y: i64, w: i64, h: i64) -> Self {
33        Self {
34            x0: x,
35            y0: y,
36            x1: x + w.max(0),
37            y1: y + h.max(0),
38        }
39    }
40
41    /// Returns `true` if the clip has zero or negative area.
42    pub fn is_empty(&self) -> bool {
43        self.x1 <= self.x0 || self.y1 <= self.y0
44    }
45
46    /// Intersect with `other`, returning the overlapping clip.
47    pub fn intersect(&self, other: &ClipRect) -> ClipRect {
48        ClipRect {
49            x0: self.x0.max(other.x0),
50            y0: self.y0.max(other.y0),
51            x1: self.x1.min(other.x1),
52            y1: self.y1.min(other.y1),
53        }
54    }
55
56    /// Returns `true` if pixel `(x, y)` lies within the clip.
57    pub fn contains(&self, x: i64, y: i64) -> bool {
58        x >= self.x0 && x < self.x1 && y >= self.y0 && y < self.y1
59    }
60}
61
62/// A stack of nested clip rectangles.
63///
64/// The effective clip is always the intersection of every pushed rectangle,
65/// maintained incrementally as [`current`](ClipStack::current).
66#[derive(Clone, Debug)]
67pub struct ClipStack {
68    stack: Vec<ClipRect>,
69}
70
71impl ClipStack {
72    /// Create a stack whose base clip covers the whole framebuffer.
73    pub fn new(width: u32, height: u32) -> Self {
74        Self {
75            stack: vec![ClipRect::full(width, height)],
76        }
77    }
78
79    /// The current effective clip (intersection of all pushed rects).
80    pub fn current(&self) -> ClipRect {
81        // The base is always present; `last` is the running intersection.
82        *self.stack.last().unwrap_or(&ClipRect {
83            x0: 0,
84            y0: 0,
85            x1: 0,
86            y1: 0,
87        })
88    }
89
90    /// Push `clip`, intersecting it with the current effective clip.
91    pub fn push(&mut self, clip: ClipRect) {
92        let next = self.current().intersect(&clip);
93        self.stack.push(next);
94    }
95
96    /// Pop the most recently pushed clip. The base clip is never popped.
97    pub fn pop(&mut self) {
98        if self.stack.len() > 1 {
99            self.stack.pop();
100        }
101    }
102
103    /// Number of clips on the stack (including the base).
104    pub fn depth(&self) -> usize {
105        self.stack.len()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn nested_clips_intersect() {
115        let mut s = ClipStack::new(100, 100);
116        assert_eq!(s.current(), ClipRect::full(100, 100));
117        s.push(ClipRect::from_rect(10, 10, 50, 50)); // [10,60) x [10,60)
118        s.push(ClipRect::from_rect(40, 40, 50, 50)); // [40,90) x [40,90)
119        let c = s.current();
120        assert_eq!(
121            c,
122            ClipRect {
123                x0: 40,
124                y0: 40,
125                x1: 60,
126                y1: 60
127            }
128        );
129        s.pop();
130        assert_eq!(
131            s.current(),
132            ClipRect {
133                x0: 10,
134                y0: 10,
135                x1: 60,
136                y1: 60
137            }
138        );
139    }
140
141    #[test]
142    fn base_clip_never_popped() {
143        let mut s = ClipStack::new(10, 10);
144        s.pop();
145        s.pop();
146        assert_eq!(s.depth(), 1);
147        assert_eq!(s.current(), ClipRect::full(10, 10));
148    }
149
150    #[test]
151    fn contains_and_empty() {
152        let c = ClipRect::from_rect(0, 0, 5, 5);
153        assert!(c.contains(0, 0));
154        assert!(c.contains(4, 4));
155        assert!(!c.contains(5, 5));
156        assert!(ClipRect::from_rect(0, 0, 0, 5).is_empty());
157        let disjoint =
158            ClipRect::from_rect(0, 0, 5, 5).intersect(&ClipRect::from_rect(10, 10, 5, 5));
159        assert!(disjoint.is_empty());
160    }
161}