1use alloc::vec::Vec;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct IntersectionRect {
12 pub x: f64,
13 pub y: f64,
14 pub width: f64,
15 pub height: f64,
16}
17
18impl IntersectionRect {
19 pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
20 Self {
21 x,
22 y,
23 width,
24 height,
25 }
26 }
27
28 pub const fn zero() -> Self {
29 Self {
30 x: 0.0,
31 y: 0.0,
32 width: 0.0,
33 height: 0.0,
34 }
35 }
36
37 #[inline]
38 pub fn right(&self) -> f64 {
39 self.x + self.width
40 }
41
42 #[inline]
43 pub fn bottom(&self) -> f64 {
44 self.y + self.height
45 }
46
47 #[inline]
48 pub fn area(&self) -> f64 {
49 self.width.max(0.0) * self.height.max(0.0)
50 }
51
52 pub fn intersect(&self, other: &Self) -> Option<Self> {
54 let x1 = self.x.max(other.x);
55 let y1 = self.y.max(other.y);
56 let x2 = self.right().min(other.right());
57 let y2 = self.bottom().min(other.bottom());
58
59 if x2 >= x1 && y2 >= y1 {
60 Some(Self {
61 x: x1,
62 y: y1,
63 width: x2 - x1,
64 height: y2 - y1,
65 })
66 } else {
67 None
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq)]
74pub struct MarginValue {
75 pub val: f64,
76 pub is_percent: bool,
77}
78
79impl MarginValue {
80 pub const fn px(val: f64) -> Self {
81 Self {
82 val,
83 is_percent: false,
84 }
85 }
86
87 pub const fn percent(val: f64) -> Self {
88 Self {
89 val,
90 is_percent: true,
91 }
92 }
93
94 pub fn resolve(&self, ref_size: f64) -> f64 {
95 if self.is_percent {
96 (self.val / 100.0) * ref_size
97 } else {
98 self.val
99 }
100 }
101
102 fn parse_token(token: &str) -> Result<Self, ()> {
103 let t = token.trim();
104 if let Some(num_str) = t.strip_suffix('%') {
105 let val = num_str.trim().parse::<f64>().map_err(|_| ())?;
106 Ok(Self::percent(val))
107 } else if let Some(num_str) = t.strip_suffix("px") {
108 let val = num_str.trim().parse::<f64>().map_err(|_| ())?;
109 Ok(Self::px(val))
110 } else if let Ok(val) = t.parse::<f64>() {
111 Ok(Self::px(val))
112 } else {
113 Err(())
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq)]
120pub struct RootMargin {
121 pub top: MarginValue,
122 pub right: MarginValue,
123 pub bottom: MarginValue,
124 pub left: MarginValue,
125}
126
127impl RootMargin {
128 pub const fn zero() -> Self {
129 Self {
130 top: MarginValue::px(0.0),
131 right: MarginValue::px(0.0),
132 bottom: MarginValue::px(0.0),
133 left: MarginValue::px(0.0),
134 }
135 }
136
137 pub fn parse(s: &str) -> Result<Self, ()> {
139 let parts: Vec<&str> = s.split_whitespace().collect();
140 if parts.is_empty() {
141 return Ok(Self::zero());
142 }
143
144 let vals: Vec<MarginValue> = parts
145 .iter()
146 .map(|&p| MarginValue::parse_token(p))
147 .collect::<Result<Vec<_>, _>>()?;
148
149 match vals.len() {
150 1 => Ok(Self {
151 top: vals[0],
152 right: vals[0],
153 bottom: vals[0],
154 left: vals[0],
155 }),
156 2 => Ok(Self {
157 top: vals[0],
158 right: vals[1],
159 bottom: vals[0],
160 left: vals[1],
161 }),
162 3 => Ok(Self {
163 top: vals[0],
164 right: vals[1],
165 bottom: vals[2],
166 left: vals[1],
167 }),
168 4 => Ok(Self {
169 top: vals[0],
170 right: vals[1],
171 bottom: vals[2],
172 left: vals[3],
173 }),
174 _ => Err(()),
175 }
176 }
177
178 pub fn apply(&self, rect: &IntersectionRect) -> IntersectionRect {
180 let top_px = self.top.resolve(rect.height);
181 let bottom_px = self.bottom.resolve(rect.height);
182 let left_px = self.left.resolve(rect.width);
183 let right_px = self.right.resolve(rect.width);
184
185 IntersectionRect {
186 x: rect.x - left_px,
187 y: rect.y - top_px,
188 width: rect.width + left_px + right_px,
189 height: rect.height + top_px + bottom_px,
190 }
191 }
192}
193
194#[derive(Debug, Clone, PartialEq)]
196pub struct IntersectionState {
197 pub bounding_client_rect: IntersectionRect,
198 pub root_bounds: Option<IntersectionRect>,
199 pub intersection_rect: IntersectionRect,
200 pub is_intersecting: bool,
201 pub intersection_ratio: f64,
202}
203
204pub fn compute_intersection(
206 target_rect: &IntersectionRect,
207 root_rect: &IntersectionRect,
208 root_margin: &RootMargin,
209) -> IntersectionState {
210 let root_bounds = root_margin.apply(root_rect);
211
212 let touches_x = target_rect.x <= root_bounds.right() && target_rect.right() >= root_bounds.x;
216 let touches_y = target_rect.y <= root_bounds.bottom() && target_rect.bottom() >= root_bounds.y;
217
218 if touches_x && touches_y {
219 let x1 = target_rect.x.max(root_bounds.x);
220 let y1 = target_rect.y.max(root_bounds.y);
221 let x2 = target_rect.right().min(root_bounds.right());
222 let y2 = target_rect.bottom().min(root_bounds.bottom());
223
224 let w = (x2 - x1).max(0.0);
225 let h = (y2 - y1).max(0.0);
226 let intersection_rect = IntersectionRect::new(x1, y1, w, h);
227 let is_intersecting = true;
228
229 let target_area = target_rect.area();
230 let intersection_ratio = if target_area > 0.0 {
231 (intersection_rect.area() / target_area).clamp(0.0, 1.0)
232 } else {
233 1.0
235 };
236
237 IntersectionState {
238 bounding_client_rect: *target_rect,
239 root_bounds: Some(root_bounds),
240 intersection_rect,
241 is_intersecting,
242 intersection_ratio,
243 }
244 } else {
245 IntersectionState {
246 bounding_client_rect: *target_rect,
247 root_bounds: Some(root_bounds),
248 intersection_rect: IntersectionRect::zero(),
249 is_intersecting: false,
250 intersection_ratio: 0.0,
251 }
252 }
253}
254
255pub fn should_trigger_callback(
258 prev_ratio: Option<f64>,
259 prev_is_intersecting: bool,
260 current_ratio: f64,
261 current_is_intersecting: bool,
262 thresholds: &[f64],
263) -> bool {
264 let Some(prev) = prev_ratio else {
265 return true;
267 };
268
269 if prev_is_intersecting != current_is_intersecting {
271 return true;
272 }
273
274 if (prev - current_ratio).abs() < 1e-6 {
275 return false;
276 }
277
278 let min_r = prev.min(current_ratio);
279 let max_r = prev.max(current_ratio);
280
281 for &th in thresholds {
282 if (min_r < th && th <= max_r) || (min_r <= th && th < max_r) {
284 return true;
285 }
286 }
287
288 false
289}