Skip to main content

sequoia_view/
paint_region.rs

1//! Culling while building quads during `paint`.
2//!
3//! `CullCtx` decides whether an element is visible at all for the current
4//! clip (from `ScrollView`, `Container::clip(true)`, or a `Canvas` with
5//! `clip_self`). If an element is entirely outside the clip, we skip:
6//!
7//! - `paint_self` (text shaping, gradient building, pushing to `RenderList`),
8//! - recursion into children, **only if** the element clips itself
9//!   (`clip_self = true`).
10//!
11//! This is always safe: the clip is authoritative — pixels outside it never
12//! reach the image, so omitting them from the `RenderList` changes nothing.
13//!
14//! `RenderList` also drops fully off-screen items when they are pushed, but
15//! that happens after text shaping and after the view has walked its subtree.
16//! Culling at this stage saves that CPU work and the push itself.
17
18use sequoia_core::Rect;
19
20/// Culling configuration for `Runtime::paint_region`.
21///
22/// `ENABLED` is the default for `paint`, `paint_if_dirty`, and the platform.
23/// `DISABLED` forces a full CPU pass; `RenderList` still clips fully
24/// off-screen items at push time, so the visible output is unchanged.
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct CullCtx {
27    enabled: bool,
28    /// Margin around the element rect -- headroom for AA, shadows, and glow.
29    margin: f32,
30}
31
32impl CullCtx {
33    /// Culling disabled -- walk and paint the full tree.
34    pub const DISABLED: Self = Self {
35        enabled: false,
36        margin: 0.0,
37    };
38
39    /// Culling enabled with an 8 px margin (safe for `BoxStyle` shadows).
40    pub const ENABLED: Self = Self {
41        enabled: true,
42        margin: 8.0,
43    };
44
45    pub const fn new() -> Self {
46        Self::ENABLED
47    }
48
49    pub const fn disabled() -> Self {
50        Self::DISABLED
51    }
52
53    pub fn with_margin(mut self, margin: f32) -> Self {
54        self.margin = if margin.is_finite() {
55            margin.clamp(0.0, 64.0)
56        } else {
57            8.0
58        };
59        self
60    }
61
62    pub fn is_enabled(self) -> bool {
63        self.enabled
64    }
65
66    pub fn margin(self) -> f32 {
67        self.margin
68    }
69
70    /// Does `rect` (expanded by the margin) intersect `clip`?
71    ///
72    /// - Culling disabled → `true`.
73    /// - `clip = None` (no clip on the path) → `true`.
74    /// - Empty `clip` → `false` (nothing is visible).
75    pub fn intersects(self, rect: Rect, clip: Option<Rect>) -> bool {
76        if !self.enabled {
77            return true;
78        }
79        let Some(clip) = clip else {
80            return true;
81        };
82        if clip.is_empty() {
83            return false;
84        }
85        rect.inflate(self.margin, self.margin).intersects(clip)
86    }
87
88    /// Is the whole subtree invisible?
89    ///
90    /// Unlike `!intersects`, this also requires the element to clip itself
91    /// (`clip_self`), so its children are clipped to the parent rect too.
92    /// Without `clip_self`, children may overflow (e.g. `overflow: Visible`)
93    /// and must not be skipped.
94    pub fn fully_clipped(self, rect: Rect, clip: Option<Rect>, clip_self: bool) -> bool {
95        if !self.enabled {
96            return false;
97        }
98        if !clip_self {
99            return false;
100        }
101        !self.intersects(rect, clip)
102    }
103}
104
105impl Default for CullCtx {
106    fn default() -> Self {
107        Self::ENABLED
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
116        Rect::from_xywh(x, y, w, h)
117    }
118
119    #[test]
120    fn disabled_never_culls() {
121        let c = CullCtx::DISABLED;
122        let clip = Some(r(0.0, 0.0, 100.0, 100.0));
123        assert!(c.intersects(r(500.0, 500.0, 10.0, 10.0), clip));
124        assert!(!c.fully_clipped(r(500.0, 500.0, 10.0, 10.0), clip, true));
125    }
126
127    #[test]
128    fn no_clip_never_culls() {
129        let c = CullCtx::ENABLED;
130        assert!(c.intersects(r(0.0, 0.0, 1.0, 1.0), None));
131    }
132
133    #[test]
134    fn empty_clip_culls_everything() {
135        let c = CullCtx::ENABLED;
136        assert!(!c.intersects(r(0.0, 0.0, 1.0, 1.0), Some(Rect::ZERO)));
137        assert!(!c.intersects(r(0.0, 0.0, 1.0, 1.0), Some(r(5.0, 5.0, 0.0, 0.0))));
138    }
139
140    #[test]
141    fn rect_inside_clip_kept() {
142        let c = CullCtx::ENABLED.with_margin(0.0);
143        let clip = Some(r(0.0, 0.0, 100.0, 100.0));
144        assert!(c.intersects(r(10.0, 10.0, 20.0, 20.0), clip));
145        assert!(
146            c.intersects(r(90.0, 90.0, 20.0, 20.0), clip),
147            "partial overlap"
148        );
149        assert!(
150            c.intersects(r(-5.0, -5.0, 20.0, 20.0), clip),
151            "partial overlap top-left"
152        );
153    }
154
155    #[test]
156    fn rect_outside_clip_culled() {
157        let c = CullCtx::ENABLED.with_margin(0.0);
158        let clip = Some(r(0.0, 0.0, 100.0, 100.0));
159        assert!(!c.intersects(r(200.0, 200.0, 20.0, 20.0), clip));
160        assert!(!c.intersects(r(200.0, 0.0, 20.0, 20.0), clip), "right");
161        assert!(!c.intersects(r(0.0, -100.0, 20.0, 20.0), clip), "above");
162    }
163
164    #[test]
165    fn margin_expands_the_visible_band() {
166        let clip = Some(r(0.0, 0.0, 100.0, 100.0));
167        // 2 px past the right edge — outside the clip, but close.
168        let rect = r(102.0, 10.0, 20.0, 20.0);
169        assert!(!CullCtx::ENABLED.with_margin(0.0).intersects(rect, clip));
170        assert!(
171            CullCtx::ENABLED.with_margin(8.0).intersects(rect, clip),
172            "shadow/glow on the border must survive"
173        );
174    }
175
176    #[test]
177    fn fully_clipped_requires_clip_self() {
178        let c = CullCtx::ENABLED.with_margin(0.0);
179        let clip = Some(r(0.0, 0.0, 100.0, 100.0));
180        let far = r(200.0, 200.0, 10.0, 10.0);
181        assert!(!c.fully_clipped(far, clip, false), "children may overflow");
182        assert!(c.fully_clipped(far, clip, true), "clipping element — safe");
183    }
184
185    #[test]
186    fn margin_sanitizes() {
187        assert_eq!(CullCtx::ENABLED.with_margin(-5.0).margin(), 0.0);
188        assert_eq!(CullCtx::ENABLED.with_margin(f32::NAN).margin(), 8.0);
189        assert_eq!(CullCtx::ENABLED.with_margin(1e6).margin(), 64.0);
190    }
191}