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), Some(c)) = (z, child) {
179            if w == c {
180                return RelativeHitZ::Front;
181            }
182        }
183        z
184    }
185
186    pub fn update_transform(&mut self, value: FrameValueUpdate<PxTransform>) {
187        for item in &mut self.items {
188            if let HitTestItem::Transform(FrameValue::Bind { id, value: t, .. }) = item {
189                if *id == value.id {
190                    *t = value.value;
191                    break;
192                }
193            }
194        }
195    }
196
197    /// Returns `true` if a clip that affects the `child` clips out the `window_point`.
198    pub fn clip_child(&self, child: HitChildIndex, inner_transform: &PxTransform, window_point: PxPoint) -> bool {
199        let mut transform_stack = vec![];
200        let mut current_transform = inner_transform;
201        let mut local_point = match inv_transform_point(current_transform, window_point) {
202            Some(p) => p,
203            None => return false,
204        };
205
206        let child = child.1 + self.segments.offset(child.0);
207
208        let mut items = self.items[..child].iter();
209        let mut clip = false;
210
211        'clip: while let Some(item) = items.next() {
212            match item {
213                HitTestItem::Clip(prim, clip_out) => {
214                    clip = match clip_out {
215                        true => prim.contains(local_point),
216                        false => !prim.contains(local_point),
217                    };
218                    if clip {
219                        let mut clip_depth = 0;
220                        'close_clip: for item in items.by_ref() {
221                            match item {
222                                HitTestItem::Clip(_, _) => clip_depth += 1,
223                                HitTestItem::PopClip => {
224                                    if clip_depth == 0 {
225                                        clip = false; // was not a clip that covers the child.
226                                        continue 'clip;
227                                    }
228                                    clip_depth -= 1;
229                                }
230                                _ => continue 'close_clip,
231                            }
232                        }
233                    }
234                }
235                HitTestItem::PopClip => continue 'clip,
236                HitTestItem::Transform(t) => {
237                    let t = t.value();
238                    match inv_transform_point(t, local_point) {
239                        Some(p) => {
240                            // transform is valid, push previous transform and replace the local point.
241                            transform_stack.push((current_transform, local_point));
242                            current_transform = t;
243                            local_point = p;
244                        }
245                        None => {
246                            // non-invertible transform, skip all transformed shapes.
247                            let mut transform_depth = 0;
248                            'skip_transformed: for item in items.by_ref() {
249                                match item {
250                                    HitTestItem::Transform(_) => {
251                                        transform_depth += 1;
252                                    }
253                                    HitTestItem::PopTransform => {
254                                        if transform_depth == 0 {
255                                            continue 'clip;
256                                        }
257                                        transform_depth -= 1;
258                                    }
259                                    _ => continue 'skip_transformed,
260                                }
261                            }
262                        }
263                    }
264                }
265                HitTestItem::PopTransform => {
266                    (current_transform, local_point) = transform_stack.pop().unwrap();
267                }
268                _ => continue 'clip,
269            }
270        }
271
272        clip
273    }
274
275    pub(crate) fn parallel_split(&self) -> Self {
276        Self {
277            items: vec![],
278            segments: self.segments.parallel_split(),
279        }
280    }
281
282    pub(crate) fn parallel_fold(&mut self, mut split: Self) {
283        self.segments.parallel_fold(split.segments, self.items.len());
284
285        if self.items.is_empty() {
286            self.items = split.items;
287        } else {
288            self.items.append(&mut split.items)
289        }
290    }
291}
292
293/// Hit-test result on a widget relative to it's descendants.
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295pub enum RelativeHitZ {
296    /// Widget was not hit.
297    NoHit,
298    /// Widget was hit on a hit-test shape rendered before the widget descendants.
299    Back,
300    /// Widget was hit on a hit-test shape rendered after the child.
301    Over(WidgetId),
302    /// Widget was hit on a hit-test shape rendered after the widget descendants.
303    Front,
304}
305
306#[derive(Debug)]
307enum HitTestPrimitive {
308    Rect(PxBox),
309    RoundedRect(PxBox, PxCornerRadius),
310    Ellipse(PxPoint, PxSize),
311}
312impl HitTestPrimitive {
313    fn contains(&self, point: PxPoint) -> bool {
314        match self {
315            HitTestPrimitive::Rect(r) => r.contains(point),
316            HitTestPrimitive::RoundedRect(rect, radii) => rounded_rect_contains(rect, radii, point),
317            HitTestPrimitive::Ellipse(center, radii) => ellipse_contains(*radii, *center, point),
318        }
319    }
320}
321#[derive(Debug)]
322enum HitTestItem {
323    Hit(HitTestPrimitive),
324
325    Clip(HitTestPrimitive, bool),
326    PopClip,
327
328    Transform(FrameValue<PxTransform>),
329    PopTransform,
330
331    Child(WidgetId),
332}
333
334fn rounded_rect_contains(rect: &PxBox, radii: &PxCornerRadius, point: PxPoint) -> bool {
335    if !rect.contains(point) {
336        return false;
337    }
338
339    let top_left_center = rect.min + radii.top_left.to_vector();
340    if top_left_center.x > point.x && top_left_center.y > point.y && !ellipse_contains(radii.top_left, top_left_center, point) {
341        return false;
342    }
343
344    let bottom_right_center = rect.max - radii.bottom_right.to_vector();
345    if bottom_right_center.x < point.x
346        && bottom_right_center.y < point.y
347        && !ellipse_contains(radii.bottom_right, bottom_right_center, point)
348    {
349        return false;
350    }
351
352    let top_right = PxPoint::new(rect.max.x, rect.min.y);
353    let top_right_center = top_right + PxVector::new(-radii.top_right.width, radii.top_right.height);
354    if top_right_center.x < point.x && top_right_center.y > point.y && !ellipse_contains(radii.top_right, top_right_center, point) {
355        return false;
356    }
357
358    let bottom_left = PxPoint::new(rect.min.x, rect.max.y);
359    let bottom_left_center = bottom_left + PxVector::new(radii.bottom_left.width, -radii.bottom_left.height);
360    if bottom_left_center.x > point.x && bottom_left_center.y < point.y && !ellipse_contains(radii.bottom_left, bottom_left_center, point) {
361        return false;
362    }
363
364    true
365}
366
367fn ellipse_contains(radii: PxSize, center: PxPoint, point: PxPoint) -> bool {
368    let h = center.x.0 as f64;
369    let k = center.y.0 as f64;
370
371    let a = radii.width.0 as f64;
372    let b = radii.height.0 as f64;
373
374    let x = point.x.0 as f64;
375    let y = point.y.0 as f64;
376
377    let p = ((x - h).powi(2) / a.powi(2)) + ((y - k).powi(2) / b.powi(2));
378
379    p <= 1.0
380}
381
382// matches webrender's implementation of the CSS spec:
383// https://github.com/servo/webrender/blob/b198248e2c836ec61c4dfdf443f97684bc281a0c/webrender/src/border.rs#L168
384pub fn ensure_no_corner_overlap(radii: &mut PxCornerRadius, size: PxSize) {
385    let mut ratio = 1.0;
386    let top_left_radius = &mut radii.top_left;
387    let top_right_radius = &mut radii.top_right;
388    let bottom_right_radius = &mut radii.bottom_right;
389    let bottom_left_radius = &mut radii.bottom_left;
390
391    let sum = top_left_radius.width + top_right_radius.width;
392    if size.width < sum {
393        ratio = f32::min(ratio, size.width.0 as f32 / sum.0 as f32);
394    }
395
396    let sum = bottom_left_radius.width + bottom_right_radius.width;
397    if size.width < sum {
398        ratio = f32::min(ratio, size.width.0 as f32 / sum.0 as f32);
399    }
400
401    let sum = top_left_radius.height + bottom_left_radius.height;
402    if size.height < sum {
403        ratio = f32::min(ratio, size.height.0 as f32 / sum.0 as f32);
404    }
405
406    let sum = top_right_radius.height + bottom_right_radius.height;
407    if size.height < sum {
408        ratio = f32::min(ratio, size.height.0 as f32 / sum.0 as f32);
409    }
410
411    if ratio < 1. {
412        top_left_radius.width *= ratio;
413        top_left_radius.height *= ratio;
414
415        top_right_radius.width *= ratio;
416        top_right_radius.height *= ratio;
417
418        bottom_left_radius.width *= ratio;
419        bottom_left_radius.height *= ratio;
420
421        bottom_right_radius.width *= ratio;
422        bottom_right_radius.height *= ratio;
423    }
424}
425
426fn inv_transform_point(t: &PxTransform, point: PxPoint) -> Option<PxPoint> {
427    t.inverse()?.project_point(point)
428}
429
430#[derive(Debug, Clone, Copy)]
431pub(crate) struct HitChildIndex(ParallelSegmentId, usize);
432impl Default for HitChildIndex {
433    fn default() -> Self {
434        Self(ParallelSegmentId::MAX, Default::default())
435    }
436}
437
438/// See [`ParallelSegmentOffsets`].
439pub(crate) type ParallelSegmentId = usize;
440
441/// Tracks the position of a range of items in a list that was built in parallel.
442#[derive(Debug, Clone)]
443pub(crate) struct ParallelSegmentOffsets {
444    id: ParallelSegmentId,
445    id_gen: Arc<AtomicUsize>,
446    used: bool,
447    segments: Vec<(ParallelSegmentId, usize)>,
448}
449impl Default for ParallelSegmentOffsets {
450    fn default() -> Self {
451        Self {
452            id: 0,
453            used: false,
454            id_gen: Arc::new(AtomicUsize::new(1)),
455            segments: vec![],
456        }
457    }
458}
459impl ParallelSegmentOffsets {
460    /// Gets the segment ID and flags the current tracking offsets as used.
461    pub fn id(&mut self) -> ParallelSegmentId {
462        self.used = true;
463        self.id
464    }
465
466    /// Resolve the `id` offset, after build.
467    pub fn offset(&self, id: ParallelSegmentId) -> usize {
468        self.segments
469            .iter()
470            .find_map(|&(i, o)| if i == id { Some(o) } else { None })
471            .unwrap_or_else(|| {
472                if id != 0 {
473                    tracing::error!(target: "parallel", "segment offset for `{id}` not found");
474                }
475                0
476            })
477    }
478
479    /// Start new parallel segment.
480    pub fn parallel_split(&self) -> Self {
481        Self {
482            used: false,
483            id: self.id_gen.fetch_add(1, atomic::Ordering::Relaxed),
484            id_gen: self.id_gen.clone(),
485            segments: vec![],
486        }
487    }
488
489    /// Merge parallel segment at the given `offset`.
490    pub fn parallel_fold(&mut self, mut split: Self, offset: usize) {
491        if !Arc::ptr_eq(&self.id_gen, &split.id_gen) {
492            tracing::error!(target: "parallel", "cannot parallel fold segments not split from the same root");
493            return;
494        }
495
496        if offset > 0 {
497            for (_, o) in &mut split.segments {
498                *o += offset;
499            }
500        }
501
502        if self.segments.is_empty() {
503            self.segments = split.segments;
504        } else {
505            self.segments.append(&mut split.segments);
506        }
507        if split.used {
508            self.segments.push((split.id, offset));
509        }
510    }
511}