zng_app/widget/info/
hit.rs

1use std::sync::{Arc, atomic::AtomicUsize};
2
3use zng_layout::unit::{PxBox, PxCornerRadius, PxPoint, PxSize, PxTransform, PxVector};
4use zng_view_api::display_list::{FrameValue, FrameValueUpdate};
5
6use crate::widget::WidgetId;
7
8/// Represents hit-test regions of a widget inner.
9#[derive(Debug, Default)]
10pub(crate) struct HitTestClips {
11    items: Vec<HitTestItem>,
12    segments: ParallelSegmentOffsets,
13}
14impl HitTestClips {
15    /// Returns `true` if any hit-test clip is registered for this widget.
16    pub fn is_hit_testable(&self) -> bool {
17        !self.items.is_empty()
18    }
19
20    pub fn push_rect(&mut self, rect: PxBox) {
21        self.items.push(HitTestItem::Hit(HitTestPrimitive::Rect(rect)));
22    }
23
24    pub fn push_clip_rect(&mut self, clip_rect: PxBox, clip_out: bool) {
25        self.items.push(HitTestItem::Clip(HitTestPrimitive::Rect(clip_rect), clip_out));
26    }
27
28    pub fn push_rounded_rect(&mut self, rect: PxBox, mut radii: PxCornerRadius) {
29        if radii == PxCornerRadius::zero() {
30            self.push_rect(rect);
31        } else {
32            ensure_no_corner_overlap(&mut radii, rect.size());
33            self.items.push(HitTestItem::Hit(HitTestPrimitive::RoundedRect(rect, radii)));
34        }
35    }
36
37    pub fn push_clip_rounded_rect(&mut self, clip_rect: PxBox, mut radii: PxCornerRadius, clip_out: bool) {
38        if radii == PxCornerRadius::zero() {
39            self.push_clip_rect(clip_rect, clip_out);
40        } else {
41            ensure_no_corner_overlap(&mut radii, clip_rect.size());
42            self.items
43                .push(HitTestItem::Clip(HitTestPrimitive::RoundedRect(clip_rect, radii), clip_out));
44        }
45    }
46
47    pub fn push_ellipse(&mut self, center: PxPoint, radii: PxSize) {
48        self.items.push(HitTestItem::Hit(HitTestPrimitive::Ellipse(center, radii)));
49    }
50
51    pub fn push_clip_ellipse(&mut self, center: PxPoint, radii: PxSize, clip_out: bool) {
52        self.items
53            .push(HitTestItem::Clip(HitTestPrimitive::Ellipse(center, radii), clip_out));
54    }
55
56    pub fn pop_clip(&mut self) {
57        self.items.push(HitTestItem::PopClip);
58    }
59
60    pub fn push_transform(&mut self, transform: FrameValue<PxTransform>) {
61        self.items.push(HitTestItem::Transform(transform))
62    }
63
64    pub fn pop_transform(&mut self) {
65        self.items.push(HitTestItem::PopTransform);
66    }
67
68    #[must_use]
69    pub fn push_child(&mut self, widget: WidgetId) -> HitChildIndex {
70        if let Some(HitTestItem::Child(c)) = self.items.last_mut() {
71            *c = widget;
72        } else {
73            self.items.push(HitTestItem::Child(widget));
74        }
75        HitChildIndex(self.segments.id(), self.items.len() - 1)
76    }
77
78    /// Hit-test the `point` against the items, returns the relative Z of the hit.
79    pub fn hit_test_z(&self, inner_transform: &PxTransform, window_point: PxPoint) -> RelativeHitZ {
80        let mut z = RelativeHitZ::NoHit;
81        let mut child = None;
82
83        let mut transform_stack = vec![];
84        let mut current_transform = inner_transform;
85        let mut local_point = match inv_transform_point(current_transform, window_point) {
86            Some(p) => p,
87            None => return RelativeHitZ::NoHit,
88        };
89
90        let mut items = self.items.iter();
91
92        'hit_test: while let Some(item) = items.next() {
93            match item {
94                HitTestItem::Hit(prim) => {
95                    if prim.contains(local_point) {
96                        z = if let Some(inner) = child {
97                            RelativeHitZ::Over(inner)
98                        } else {
99                            RelativeHitZ::Back
100                        };
101                    }
102                }
103
104                HitTestItem::Clip(prim, clip_out) => {
105                    let skip = match clip_out {
106                        true => prim.contains(local_point),
107                        false => !prim.contains(local_point),
108                    };
109
110                    if skip {
111                        // clip excluded point, skip all clipped shapes.
112                        let mut clip_depth = 0;
113                        'skip_clipped: for item in items.by_ref() {
114                            match item {
115                                HitTestItem::Clip(_, _) => {
116                                    clip_depth += 1;
117                                }
118                                HitTestItem::PopClip => {
119                                    if clip_depth == 0 {
120                                        continue 'hit_test;
121                                    }
122                                    clip_depth -= 1;
123                                }
124                                HitTestItem::Child(w) => {
125                                    child = Some(*w);
126                                    continue 'skip_clipped;
127                                }
128                                _ => continue 'skip_clipped,
129                            }
130                        }
131                    }
132                }
133                HitTestItem::PopClip => continue 'hit_test,
134
135                HitTestItem::Transform(t) => {
136                    let t = t.value();
137                    match inv_transform_point(t, local_point) {
138                        Some(p) => {
139                            // transform is valid, push previous transform and replace the local point.
140                            transform_stack.push((current_transform, local_point));
141                            current_transform = t;
142                            local_point = p;
143                        }
144                        None => {
145                            // non-invertible transform, skip all transformed shapes.
146                            let mut transform_depth = 0;
147                            'skip_transformed: for item in items.by_ref() {
148                                match item {
149                                    HitTestItem::Transform(_) => {
150                                        transform_depth += 1;
151                                    }
152                                    HitTestItem::PopTransform => {
153                                        if transform_depth == 0 {
154                                            continue 'hit_test;
155                                        }
156                                        transform_depth -= 1;
157                                    }
158                                    HitTestItem::Child(w) => {
159                                        child = Some(*w);
160                                        continue 'skip_transformed;
161                                    }
162                                    _ => continue 'skip_transformed,
163                                }
164                            }
165                        }
166                    }
167                }
168                HitTestItem::PopTransform => {
169                    (current_transform, local_point) = transform_stack.pop().unwrap();
170                }
171
172                HitTestItem::Child(w) => {
173                    child = Some(*w);
174                }
175            }
176        }
177
178        if let RelativeHitZ::Over(w) = z
179            && let Some(c) = child
180            && w == c
181        {
182            return RelativeHitZ::Front;
183        }
184        z
185    }
186
187    pub fn update_transform(&mut self, value: FrameValueUpdate<PxTransform>) {
188        for item in &mut self.items {
189            if let HitTestItem::Transform(FrameValue::Bind { id, value: t, .. }) = item
190                && *id == value.id
191            {
192                *t = value.value;
193                break;
194            }
195        }
196    }
197
198    /// Returns `true` if a clip that affects the `child` clips out the `window_point`.
199    pub fn clip_child(&self, child: HitChildIndex, inner_transform: &PxTransform, window_point: PxPoint) -> bool {
200        let mut transform_stack = vec![];
201        let mut current_transform = inner_transform;
202        let mut local_point = match inv_transform_point(current_transform, window_point) {
203            Some(p) => p,
204            None => return false,
205        };
206
207        let child = child.1 + self.segments.offset(child.0);
208
209        let mut items = self.items[..child].iter();
210        let mut clip = false;
211
212        'clip: while let Some(item) = items.next() {
213            match item {
214                HitTestItem::Clip(prim, clip_out) => {
215                    clip = match clip_out {
216                        true => prim.contains(local_point),
217                        false => !prim.contains(local_point),
218                    };
219                    if clip {
220                        let mut clip_depth = 0;
221                        'close_clip: for item in items.by_ref() {
222                            match item {
223                                HitTestItem::Clip(_, _) => clip_depth += 1,
224                                HitTestItem::PopClip => {
225                                    if clip_depth == 0 {
226                                        clip = false; // was not a clip that covers the child.
227                                        continue 'clip;
228                                    }
229                                    clip_depth -= 1;
230                                }
231                                _ => continue 'close_clip,
232                            }
233                        }
234                    }
235                }
236                HitTestItem::PopClip => continue 'clip,
237                HitTestItem::Transform(t) => {
238                    let t = t.value();
239                    match inv_transform_point(t, local_point) {
240                        Some(p) => {
241                            // transform is valid, push previous transform and replace the local point.
242                            transform_stack.push((current_transform, local_point));
243                            current_transform = t;
244                            local_point = p;
245                        }
246                        None => {
247                            // non-invertible transform, skip all transformed shapes.
248                            let mut transform_depth = 0;
249                            'skip_transformed: for item in items.by_ref() {
250                                match item {
251                                    HitTestItem::Transform(_) => {
252                                        transform_depth += 1;
253                                    }
254                                    HitTestItem::PopTransform => {
255                                        if transform_depth == 0 {
256                                            continue 'clip;
257                                        }
258                                        transform_depth -= 1;
259                                    }
260                                    _ => continue 'skip_transformed,
261                                }
262                            }
263                        }
264                    }
265                }
266                HitTestItem::PopTransform => {
267                    (current_transform, local_point) = transform_stack.pop().unwrap();
268                }
269                _ => continue 'clip,
270            }
271        }
272
273        clip
274    }
275
276    pub(crate) fn parallel_split(&self) -> Self {
277        Self {
278            items: vec![],
279            segments: self.segments.parallel_split(),
280        }
281    }
282
283    pub(crate) fn parallel_fold(&mut self, mut split: Self) {
284        self.segments.parallel_fold(split.segments, self.items.len());
285
286        if self.items.is_empty() {
287            self.items = split.items;
288        } else {
289            self.items.append(&mut split.items)
290        }
291    }
292}
293
294/// Hit-test result on a widget relative to it's descendants.
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296pub enum RelativeHitZ {
297    /// Widget was not hit.
298    NoHit,
299    /// Widget was hit on a hit-test shape rendered before the widget descendants.
300    Back,
301    /// Widget was hit on a hit-test shape rendered after the child.
302    Over(WidgetId),
303    /// Widget was hit on a hit-test shape rendered after the widget descendants.
304    Front,
305}
306
307#[derive(Debug)]
308enum HitTestPrimitive {
309    Rect(PxBox),
310    RoundedRect(PxBox, PxCornerRadius),
311    Ellipse(PxPoint, PxSize),
312}
313impl HitTestPrimitive {
314    fn contains(&self, point: PxPoint) -> bool {
315        match self {
316            HitTestPrimitive::Rect(r) => r.contains(point),
317            HitTestPrimitive::RoundedRect(rect, radii) => rounded_rect_contains(rect, radii, point),
318            HitTestPrimitive::Ellipse(center, radii) => ellipse_contains(*radii, *center, point),
319        }
320    }
321}
322#[derive(Debug)]
323enum HitTestItem {
324    Hit(HitTestPrimitive),
325
326    Clip(HitTestPrimitive, bool),
327    PopClip,
328
329    Transform(FrameValue<PxTransform>),
330    PopTransform,
331
332    Child(WidgetId),
333}
334
335fn rounded_rect_contains(rect: &PxBox, radii: &PxCornerRadius, point: PxPoint) -> bool {
336    if !rect.contains(point) {
337        return false;
338    }
339
340    let top_left_center = rect.min + radii.top_left.to_vector();
341    if top_left_center.x > point.x && top_left_center.y > point.y && !ellipse_contains(radii.top_left, top_left_center, point) {
342        return false;
343    }
344
345    let bottom_right_center = rect.max - radii.bottom_right.to_vector();
346    if bottom_right_center.x < point.x
347        && bottom_right_center.y < point.y
348        && !ellipse_contains(radii.bottom_right, bottom_right_center, point)
349    {
350        return false;
351    }
352
353    let top_right = PxPoint::new(rect.max.x, rect.min.y);
354    let top_right_center = top_right + PxVector::new(-radii.top_right.width, radii.top_right.height);
355    if top_right_center.x < point.x && top_right_center.y > point.y && !ellipse_contains(radii.top_right, top_right_center, point) {
356        return false;
357    }
358
359    let bottom_left = PxPoint::new(rect.min.x, rect.max.y);
360    let bottom_left_center = bottom_left + PxVector::new(radii.bottom_left.width, -radii.bottom_left.height);
361    if bottom_left_center.x > point.x && bottom_left_center.y < point.y && !ellipse_contains(radii.bottom_left, bottom_left_center, point) {
362        return false;
363    }
364
365    true
366}
367
368fn ellipse_contains(radii: PxSize, center: PxPoint, point: PxPoint) -> bool {
369    let h = center.x.0 as f64;
370    let k = center.y.0 as f64;
371
372    let a = radii.width.0 as f64;
373    let b = radii.height.0 as f64;
374
375    let x = point.x.0 as f64;
376    let y = point.y.0 as f64;
377
378    let p = ((x - h).powi(2) / a.powi(2)) + ((y - k).powi(2) / b.powi(2));
379
380    p <= 1.0
381}
382
383// matches webrender's implementation of the CSS spec:
384// https://github.com/servo/webrender/blob/b198248e2c836ec61c4dfdf443f97684bc281a0c/webrender/src/border.rs#L168
385pub fn ensure_no_corner_overlap(radii: &mut PxCornerRadius, size: PxSize) {
386    let mut ratio = 1.0;
387    let top_left_radius = &mut radii.top_left;
388    let top_right_radius = &mut radii.top_right;
389    let bottom_right_radius = &mut radii.bottom_right;
390    let bottom_left_radius = &mut radii.bottom_left;
391
392    let sum = top_left_radius.width + top_right_radius.width;
393    if size.width < sum {
394        ratio = f32::min(ratio, size.width.0 as f32 / sum.0 as f32);
395    }
396
397    let sum = bottom_left_radius.width + bottom_right_radius.width;
398    if size.width < sum {
399        ratio = f32::min(ratio, size.width.0 as f32 / sum.0 as f32);
400    }
401
402    let sum = top_left_radius.height + bottom_left_radius.height;
403    if size.height < sum {
404        ratio = f32::min(ratio, size.height.0 as f32 / sum.0 as f32);
405    }
406
407    let sum = top_right_radius.height + bottom_right_radius.height;
408    if size.height < sum {
409        ratio = f32::min(ratio, size.height.0 as f32 / sum.0 as f32);
410    }
411
412    if ratio < 1. {
413        top_left_radius.width *= ratio;
414        top_left_radius.height *= ratio;
415
416        top_right_radius.width *= ratio;
417        top_right_radius.height *= ratio;
418
419        bottom_left_radius.width *= ratio;
420        bottom_left_radius.height *= ratio;
421
422        bottom_right_radius.width *= ratio;
423        bottom_right_radius.height *= ratio;
424    }
425}
426
427fn inv_transform_point(t: &PxTransform, point: PxPoint) -> Option<PxPoint> {
428    t.inverse()?.project_point(point)
429}
430
431#[derive(Debug, Clone, Copy)]
432pub(crate) struct HitChildIndex(ParallelSegmentId, usize);
433impl Default for HitChildIndex {
434    fn default() -> Self {
435        Self(ParallelSegmentId::MAX, Default::default())
436    }
437}
438
439/// See [`ParallelSegmentOffsets`].
440pub(crate) type ParallelSegmentId = usize;
441
442/// Tracks the position of a range of items in a list that was built in parallel.
443#[derive(Debug, Clone)]
444pub(crate) struct ParallelSegmentOffsets {
445    id: ParallelSegmentId,
446    id_gen: Arc<AtomicUsize>,
447    used: bool,
448    segments: Vec<(ParallelSegmentId, usize)>,
449}
450impl Default for ParallelSegmentOffsets {
451    fn default() -> Self {
452        Self {
453            id: 0,
454            used: false,
455            id_gen: Arc::new(AtomicUsize::new(1)),
456            segments: vec![],
457        }
458    }
459}
460impl ParallelSegmentOffsets {
461    /// Gets the segment ID and flags the current tracking offsets as used.
462    pub fn id(&mut self) -> ParallelSegmentId {
463        self.used = true;
464        self.id
465    }
466
467    /// Resolve the `id` offset, after build.
468    pub fn offset(&self, id: ParallelSegmentId) -> usize {
469        self.segments
470            .iter()
471            .find_map(|&(i, o)| if i == id { Some(o) } else { None })
472            .unwrap_or_else(|| {
473                if id != 0 {
474                    tracing::error!(target: "parallel", "segment offset for `{id}` not found");
475                }
476                0
477            })
478    }
479
480    /// Start new parallel segment.
481    pub fn parallel_split(&self) -> Self {
482        Self {
483            used: false,
484            id: self.id_gen.fetch_add(1, atomic::Ordering::Relaxed),
485            id_gen: self.id_gen.clone(),
486            segments: vec![],
487        }
488    }
489
490    /// Merge parallel segment at the given `offset`.
491    pub fn parallel_fold(&mut self, mut split: Self, offset: usize) {
492        if !Arc::ptr_eq(&self.id_gen, &split.id_gen) {
493            tracing::error!(target: "parallel", "cannot parallel fold segments not split from the same root");
494            return;
495        }
496
497        if offset > 0 {
498            for (_, o) in &mut split.segments {
499                *o += offset;
500            }
501        }
502
503        if self.segments.is_empty() {
504            self.segments = split.segments;
505        } else {
506            self.segments.append(&mut split.segments);
507        }
508        if split.used {
509            self.segments.push((split.id, offset));
510        }
511    }
512}