1use sequoia_core::Rect;
19
20#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct CullCtx {
27 enabled: bool,
28 margin: f32,
30}
31
32impl CullCtx {
33 pub const DISABLED: Self = Self {
35 enabled: false,
36 margin: 0.0,
37 };
38
39 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 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 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 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}