Skip to main content

repose_ui/
hit_testing.rs

1use std::cell::{Cell, RefCell};
2use std::collections::hash_map::DefaultHasher;
3use std::collections::{HashMap, HashSet};
4use std::hash::{Hash, Hasher};
5use std::sync::Arc;
6
7use repose_core::{
8    ClipOp, Frame, HitRegion, ImeAction, KeyboardActions, Rect, Sp, TextInputConfig, Transform,
9    Vec2,
10};
11
12use crate::textfield::TextFieldMetrics;
13
14#[derive(Clone)]
15pub(crate) struct HitClip {
16    world_to_local: [f64; 9],
17    rect: Rect,
18    op: ClipOp,
19}
20
21#[derive(Clone)]
22struct HitClipLink {
23    clip: HitClip,
24    parent: Option<Arc<HitClipLink>>,
25}
26
27#[derive(Clone, Default)]
28struct HitClipChain {
29    head: Option<Arc<HitClipLink>>,
30}
31
32impl HitClipChain {
33    fn iter(&self) -> HitClipIter<'_> {
34        HitClipIter {
35            next: self.head.as_deref(),
36        }
37    }
38}
39
40struct HitClipIter<'a> {
41    next: Option<&'a HitClipLink>,
42}
43
44impl<'a> Iterator for HitClipIter<'a> {
45    type Item = &'a HitClip;
46
47    fn next(&mut self) -> Option<Self::Item> {
48        let link = self.next?;
49        self.next = link.parent.as_deref();
50        Some(&link.clip)
51    }
52}
53
54#[derive(Clone)]
55pub(crate) struct HitContext {
56    local_to_world: [f64; 9],
57    world_to_local: [f64; 9],
58    clips: HitClipChain,
59    layer_depth: u32,
60    valid: bool,
61    cull_safe: bool,
62    defer_safe: bool,
63}
64
65impl Default for HitContext {
66    fn default() -> Self {
67        Self::root()
68    }
69}
70
71impl HitContext {
72    pub(crate) fn root() -> Self {
73        Self {
74            local_to_world: identity_matrix(),
75            world_to_local: identity_matrix(),
76            clips: HitClipChain::default(),
77            layer_depth: 0,
78            valid: true,
79            cull_safe: true,
80            defer_safe: true,
81        }
82    }
83
84    pub(crate) fn with_transform(&self, transform: Transform) -> Self {
85        let local = transform.projective_matrix();
86        let local_to_world =
87            Transform::compose_projective(&self.local_to_world.map(|v| v as f32), &local)
88                .map(|v| v as f64);
89        let Some(world_to_local) = invert_matrix(local_to_world) else {
90            return Self {
91                local_to_world,
92                world_to_local: self.world_to_local,
93                clips: self.clips.clone(),
94                layer_depth: self.layer_depth,
95                valid: false,
96                cull_safe: false,
97                defer_safe: false,
98            };
99        };
100        Self {
101            local_to_world,
102            world_to_local,
103            clips: self.clips.clone(),
104            layer_depth: self.layer_depth,
105            valid: self.valid,
106            cull_safe: self.cull_safe && !transform.has_perspective(),
107            defer_safe: self.defer_safe && matrix_is_identity(local),
108        }
109    }
110
111    pub(crate) fn can_defer(&self) -> bool {
112        self.valid && self.defer_safe
113    }
114
115    pub(crate) fn with_layer(&self) -> Self {
116        Self {
117            local_to_world: self.local_to_world,
118            world_to_local: self.world_to_local,
119            clips: self.clips.clone(),
120            layer_depth: self.layer_depth.saturating_add(1),
121            valid: self.valid,
122            cull_safe: self.cull_safe,
123            defer_safe: false,
124        }
125    }
126
127    pub(crate) fn with_clip(&self, rect: Rect, op: ClipOp) -> Self {
128        let link = Arc::new(HitClipLink {
129            clip: HitClip {
130                world_to_local: self.world_to_local,
131                rect,
132                op,
133            },
134            parent: self.clips.head.clone(),
135        });
136        Self {
137            local_to_world: self.local_to_world,
138            world_to_local: self.world_to_local,
139            clips: HitClipChain { head: Some(link) },
140            layer_depth: self.layer_depth,
141            valid: self.valid,
142            cull_safe: self.cull_safe,
143            defer_safe: false,
144        }
145    }
146
147    pub(crate) fn project_rect(&self, rect: Rect) -> Rect {
148        project_rect(self.local_to_world, rect)
149    }
150
151    pub(crate) fn to_local(&self, point: Vec2) -> Vec2 {
152        apply_matrix(self.world_to_local, point)
153    }
154
155    pub(crate) fn intersects(&self, rect: Rect, world: Rect) -> bool {
156        if !self.valid || !self.cull_safe {
157            return true;
158        }
159        intersects_rect(self.project_rect(rect), world)
160            && self
161                .clips
162                .iter()
163                .filter(|clip| clip.op == ClipOp::Intersect)
164                .all(|clip| {
165                    invert_matrix(clip.world_to_local)
166                        .map(|clip_to_world| {
167                            intersects_rect(project_rect(clip_to_world, clip.rect), world)
168                        })
169                        .unwrap_or(false)
170                })
171    }
172
173    pub(crate) fn cache_key(&self) -> u64 {
174        let mut hasher = DefaultHasher::new();
175        for value in self.local_to_world {
176            value.to_bits().hash(&mut hasher);
177        }
178        for value in self.world_to_local {
179            value.to_bits().hash(&mut hasher);
180        }
181        let mut clip_count = 0usize;
182        for clip in self.clips.iter() {
183            clip_count += 1;
184            for value in clip.world_to_local {
185                value.to_bits().hash(&mut hasher);
186            }
187            clip.rect.x.to_bits().hash(&mut hasher);
188            clip.rect.y.to_bits().hash(&mut hasher);
189            clip.rect.w.to_bits().hash(&mut hasher);
190            clip.rect.h.to_bits().hash(&mut hasher);
191            match clip.op {
192                ClipOp::Intersect => 0u8.hash(&mut hasher),
193                ClipOp::Difference => 1u8.hash(&mut hasher),
194            }
195        }
196        clip_count.hash(&mut hasher);
197        self.layer_depth.hash(&mut hasher);
198        self.valid.hash(&mut hasher);
199        self.cull_safe.hash(&mut hasher);
200        self.defer_safe.hash(&mut hasher);
201        hasher.finish()
202    }
203}
204
205#[derive(Clone)]
206pub(crate) struct HitRegionMetadata {
207    local_rect: Rect,
208    world_rect: Rect,
209    local_to_world: [f64; 9],
210    world_to_local: [f64; 9],
211    clips: HitClipChain,
212    textfield_metrics: Option<TextFieldMetrics>,
213    keyboard_actions: Option<KeyboardActions>,
214    textfield_transform_id: Option<u64>,
215}
216
217#[derive(Clone)]
218pub struct HitRegionSnapshot {
219    hit: HitRegion,
220    metadata: Option<Arc<HitRegionMetadata>>,
221}
222
223impl HitRegionSnapshot {
224    pub fn new(hit: &HitRegion) -> Self {
225        Self {
226            hit: hit.clone(),
227            metadata: metadata_for(hit),
228        }
229    }
230
231    pub fn id(&self) -> u64 {
232        self.hit.id
233    }
234
235    pub fn hit(&self) -> &HitRegion {
236        &self.hit
237    }
238
239    pub fn contains(&self, position: Vec2) -> bool {
240        contains(&self.hit, self.metadata.as_deref(), position)
241    }
242
243    pub fn to_local(&self, position: Vec2) -> Vec2 {
244        to_local(&self.hit, self.metadata.as_deref(), position)
245    }
246
247    pub fn pointer_coordinates(&self, position: Vec2) -> (Vec2, Vec2) {
248        pointer_coordinates(&self.hit, self.metadata.as_deref(), position)
249    }
250}
251
252thread_local! {
253    static HIT_METADATA: RefCell<HashMap<u64, (Arc<HitRegionMetadata>, u64)>> =
254        RefCell::new(HashMap::new());
255    static HIT_METADATA_GENERATION: Cell<u64> = const { Cell::new(0) };
256}
257
258pub(crate) fn begin_hit_metadata_frame() {
259    HIT_METADATA_GENERATION.with(|generation| {
260        generation.set(generation.get().wrapping_add(1));
261    });
262}
263
264pub(crate) fn prune_hit_metadata() {
265    let generation = HIT_METADATA_GENERATION.with(Cell::get);
266    HIT_METADATA.with(|metadata| {
267        metadata
268            .borrow_mut()
269            .retain(|_, (_, stamp)| *stamp == generation);
270    });
271}
272
273pub(crate) fn register_hit(
274    hit: &mut HitRegion,
275    context: &HitContext,
276    text_input: Option<&TextInputConfig>,
277) {
278    let local_rect = hit.rect;
279    let mut metadata = HitRegionMetadata {
280        local_rect,
281        world_rect: Rect::default(),
282        local_to_world: context.local_to_world,
283        world_to_local: context.world_to_local,
284        clips: context.clips.clone(),
285        textfield_metrics: text_input.map(TextFieldMetrics::from_config),
286        keyboard_actions: text_input.and_then(|input| input.keyboard_actions.clone()),
287        textfield_transform_id: text_input
288            .and_then(|input| input.visual_transformation.as_ref())
289            .and_then(crate::textfield::textfield_transform_id),
290    };
291    metadata.world_rect = metadata.compute_world_rect();
292    hit.rect = metadata.world_rect;
293    let generation = HIT_METADATA_GENERATION.with(Cell::get);
294    HIT_METADATA.with(|all| {
295        all.borrow_mut()
296            .insert(hit.id, (Arc::new(metadata), generation));
297    });
298}
299
300fn with_metadata<R>(hit: &HitRegion, f: impl FnOnce(&HitRegionMetadata) -> R) -> Option<R> {
301    HIT_METADATA.with(|all| {
302        let all = all.borrow();
303        let (metadata, _) = all.get(&hit.id)?;
304        (metadata.world_rect == hit.rect).then(|| f(metadata))
305    })
306}
307
308pub(crate) fn metadata_for(hit: &HitRegion) -> Option<Arc<HitRegionMetadata>> {
309    HIT_METADATA.with(|all| {
310        let all = all.borrow();
311        let (metadata, _) = all.get(&hit.id)?;
312        (metadata.world_rect == hit.rect).then(|| Arc::clone(metadata))
313    })
314}
315
316pub(crate) fn restore_metadata(id: u64, metadata: Arc<HitRegionMetadata>) {
317    let generation = HIT_METADATA_GENERATION.with(Cell::get);
318    HIT_METADATA.with(|all| {
319        all.borrow_mut().insert(id, (metadata, generation));
320    });
321}
322
323pub fn hit_test_frame(frame: &Frame, position: Vec2) -> Option<&HitRegion> {
324    frame_hit_order(frame, position, true)
325        .first()
326        .copied()
327        .and_then(|index| frame.hit_regions.get(index))
328}
329
330pub fn hit_test_enabled_frame(frame: &Frame, position: Vec2) -> Option<&HitRegion> {
331    frame_hit_order(frame, position, false)
332        .first()
333        .copied()
334        .and_then(|index| frame.hit_regions.get(index))
335}
336
337pub fn hit_test_frame_path(frame: &Frame, position: Vec2) -> Vec<u64> {
338    hit_test_path(frame, position, false)
339}
340
341pub fn hit_test_frame_regions(frame: &Frame, position: Vec2) -> Vec<u64> {
342    frame_hit_order(frame, position, false)
343        .into_iter()
344        .map(|index| frame.hit_regions[index].id)
345        .collect()
346}
347
348pub fn hit_region_contains(hit: &HitRegion, position: Vec2) -> bool {
349    with_metadata(hit, |metadata| contains(hit, Some(metadata), position))
350        .unwrap_or_else(|| hit.rect.contains(position))
351}
352
353pub fn hit_region_to_local(hit: &HitRegion, position: Vec2) -> Vec2 {
354    with_metadata(hit, |metadata| to_local(hit, Some(metadata), position)).unwrap_or(position)
355}
356
357pub fn hit_region_pointer_coordinates(hit: &HitRegion, position: Vec2) -> (Vec2, Vec2) {
358    with_metadata(hit, |metadata| {
359        pointer_coordinates(hit, Some(metadata), position)
360    })
361    .unwrap_or_else(|| {
362        let origin = Vec2 {
363            x: hit.rect.x,
364            y: hit.rect.y,
365        };
366        (origin, position - origin)
367    })
368}
369
370pub fn hit_region_local_rect(hit: &HitRegion) -> Rect {
371    with_metadata(hit, |metadata| metadata.local_rect).unwrap_or(hit.rect)
372}
373
374pub fn hit_region_transformed_origin(hit: &HitRegion) -> Vec2 {
375    with_metadata(hit, |metadata| {
376        apply_matrix(
377            metadata.local_to_world,
378            Vec2 {
379                x: metadata.local_rect.x,
380                y: metadata.local_rect.y,
381            },
382        )
383    })
384    .unwrap_or(Vec2 {
385        x: hit.rect.x,
386        y: hit.rect.y,
387    })
388}
389
390pub fn hit_region_world_rect(hit: &HitRegion) -> Rect {
391    with_metadata(hit, |metadata| metadata.world_rect).unwrap_or(hit.rect)
392}
393
394pub fn dispatch_keyboard_action(hit: &HitRegion, action: ImeAction, default: &dyn Fn()) -> bool {
395    let callback = with_metadata(hit, |metadata| {
396        metadata.keyboard_actions.as_ref().and_then(|actions| {
397            let callback = match action {
398                ImeAction::Done => &actions.on_done,
399                ImeAction::Go => &actions.on_go,
400                ImeAction::Next => &actions.on_next,
401                ImeAction::Previous => &actions.on_previous,
402                ImeAction::Search => &actions.on_search,
403                ImeAction::Send => &actions.on_send,
404                ImeAction::Unspecified | ImeAction::None | ImeAction::Default => return None,
405            };
406            callback.clone()
407        })
408    });
409    let Some(callback) = callback else {
410        default();
411        return true;
412    };
413    if let Some(callback) = callback {
414        let scope = DefaultKeyboardActionScope { action, default };
415        callback(&scope);
416    } else {
417        default();
418    }
419    true
420}
421
422pub(crate) fn textfield_metrics(hit: &HitRegion) -> TextFieldMetrics {
423    with_metadata(hit, |metadata| metadata.textfield_metrics.clone())
424        .flatten()
425        .unwrap_or_else(|| {
426            let mut metrics = TextFieldMetrics::default();
427            if hit.tf_font_size != Sp::ZERO {
428                metrics.font_px = hit.tf_font_size.to_px().0;
429                metrics.line_height_px = metrics.font_px;
430            }
431            metrics
432        })
433}
434
435pub(crate) fn textfield_transform_id(hit: &HitRegion) -> Option<u64> {
436    with_metadata(hit, |metadata| metadata.textfield_transform_id).flatten()
437}
438
439fn contains(hit: &HitRegion, metadata: Option<&HitRegionMetadata>, position: Vec2) -> bool {
440    let Some(metadata) = metadata else {
441        return hit.rect.contains(position);
442    };
443    let local = apply_matrix(metadata.world_to_local, position);
444    metadata.local_rect.contains(local)
445        && metadata
446            .clips
447            .iter()
448            .all(|clip| clip_allows(clip, position))
449}
450
451fn to_local(_hit: &HitRegion, metadata: Option<&HitRegionMetadata>, position: Vec2) -> Vec2 {
452    metadata.map_or(position, |metadata| {
453        apply_matrix(metadata.world_to_local, position)
454    })
455}
456
457fn pointer_coordinates(
458    hit: &HitRegion,
459    metadata: Option<&HitRegionMetadata>,
460    position: Vec2,
461) -> (Vec2, Vec2) {
462    let Some(metadata) = metadata else {
463        let origin = Vec2 {
464            x: hit.rect.x,
465            y: hit.rect.y,
466        };
467        return (origin, position - origin);
468    };
469    let local = apply_matrix(metadata.world_to_local, position);
470    let local_origin = Vec2 {
471        x: metadata.local_rect.x,
472        y: metadata.local_rect.y,
473    };
474    let local_position = local - local_origin;
475    let origin = apply_matrix(metadata.local_to_world, local_origin);
476    (origin, local_position)
477}
478
479fn hit_test_path(frame: &Frame, position: Vec2, include_disabled: bool) -> Vec<u64> {
480    let order = frame_hit_order(frame, position, include_disabled);
481    let Some(&top) = order.first() else {
482        return Vec::new();
483    };
484    let exact: HashSet<u64> = order
485        .iter()
486        .map(|index| frame.hit_regions[*index].id)
487        .collect();
488    let by_id: HashMap<u64, &HitRegion> =
489        frame.hit_regions.iter().map(|hit| (hit.id, hit)).collect();
490    let mut path = Vec::new();
491    let mut current = Some(frame.hit_regions[top].id);
492    let mut seen = HashSet::new();
493    while let Some(id) = current {
494        if !seen.insert(id) {
495            break;
496        }
497        let Some(hit) = by_id.get(&id) else {
498            break;
499        };
500        if exact.contains(&id) {
501            path.push(id);
502        }
503        current = hit.parent;
504    }
505    path
506}
507
508fn frame_hit_order(frame: &Frame, position: Vec2, include_disabled: bool) -> Vec<usize> {
509    let mut order = HIT_METADATA.with(|all| {
510        let all = all.borrow();
511        frame
512            .hit_regions
513            .iter()
514            .enumerate()
515            .filter(|(_, hit)| include_disabled || !hit.disabled)
516            .filter(|(_, hit)| {
517                all.get(&hit.id)
518                    .filter(|(metadata, _)| metadata.world_rect == hit.rect)
519                    .map(|(metadata, _)| contains(hit, Some(metadata), position))
520                    .unwrap_or_else(|| hit.rect.contains(position))
521            })
522            .map(|(index, _)| index)
523            .collect::<Vec<_>>()
524    });
525    order.sort_unstable_by(|left, right| {
526        let left_hit = &frame.hit_regions[*left];
527        let right_hit = &frame.hit_regions[*right];
528        right_hit
529            .z_index
530            .partial_cmp(&left_hit.z_index)
531            .unwrap_or(std::cmp::Ordering::Equal)
532            .then_with(|| right.cmp(left))
533    });
534    order
535}
536
537struct DefaultKeyboardActionScope<'a> {
538    action: ImeAction,
539    default: &'a dyn Fn(),
540}
541
542impl repose_core::KeyboardActionScope for DefaultKeyboardActionScope<'_> {
543    fn default_keyboard_action(&self, action: ImeAction) {
544        if action == self.action {
545            (self.default)();
546        }
547    }
548}
549
550impl HitRegionMetadata {
551    fn compute_world_rect(&self) -> Rect {
552        let mut world = project_rect(self.local_to_world, self.local_rect);
553        for clip in self
554            .clips
555            .iter()
556            .filter(|clip| clip.op == ClipOp::Intersect)
557        {
558            let Some(clip_to_world) = invert_matrix(clip.world_to_local) else {
559                continue;
560            };
561            let projected = project_rect(clip_to_world, clip.rect);
562            world = intersect_rect(world, projected).unwrap_or(Rect {
563                x: world.x,
564                y: world.y,
565                w: 0.0,
566                h: 0.0,
567            });
568            if world.w <= 0.0 || world.h <= 0.0 {
569                break;
570            }
571        }
572        world
573    }
574}
575
576fn intersects_rect(a: Rect, b: Rect) -> bool {
577    let x = a.x.max(b.x);
578    let y = a.y.max(b.y);
579    let right = (a.x + a.w).min(b.x + b.w);
580    let bottom = (a.y + a.h).min(b.y + b.h);
581    right > x && bottom > y
582}
583
584fn intersect_rect(a: Rect, b: Rect) -> Option<Rect> {
585    let x = a.x.max(b.x);
586    let y = a.y.max(b.y);
587    let right = (a.x + a.w).min(b.x + b.w);
588    let bottom = (a.y + a.h).min(b.y + b.h);
589    (right > x && bottom > y).then_some(Rect {
590        x,
591        y,
592        w: right - x,
593        h: bottom - y,
594    })
595}
596
597fn clip_allows(clip: &HitClip, position: Vec2) -> bool {
598    let local = apply_matrix(clip.world_to_local, position);
599    if clip.op == ClipOp::Difference {
600        !clip.rect.contains(local)
601    } else {
602        clip.rect.contains(local)
603    }
604}
605
606fn project_rect(matrix: [f64; 9], rect: Rect) -> Rect {
607    let corners = [
608        Vec2 {
609            x: rect.x,
610            y: rect.y,
611        },
612        Vec2 {
613            x: rect.x + rect.w,
614            y: rect.y,
615        },
616        Vec2 {
617            x: rect.x + rect.w,
618            y: rect.y + rect.h,
619        },
620        Vec2 {
621            x: rect.x,
622            y: rect.y + rect.h,
623        },
624    ];
625    let mut min_x = f64::MAX;
626    let mut min_y = f64::MAX;
627    let mut max_x = f64::MIN;
628    let mut max_y = f64::MIN;
629    for corner in corners {
630        let point = apply_matrix(matrix, corner);
631        min_x = min_x.min(point.x as f64);
632        min_y = min_y.min(point.y as f64);
633        max_x = max_x.max(point.x as f64);
634        max_y = max_y.max(point.y as f64);
635    }
636    Rect {
637        x: min_x as f32,
638        y: min_y as f32,
639        w: (max_x - min_x) as f32,
640        h: (max_y - min_y) as f32,
641    }
642}
643
644fn identity_matrix() -> [f64; 9] {
645    [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
646}
647
648fn matrix_is_identity(matrix: [f32; 9]) -> bool {
649    matrix
650        .iter()
651        .zip([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0])
652        .all(|(left, right)| *left == right)
653}
654
655fn apply_matrix(matrix: [f64; 9], point: Vec2) -> Vec2 {
656    let x = point.x as f64;
657    let y = point.y as f64;
658    let w = matrix[6] * x + matrix[7] * y + matrix[8];
659    let w = if w.abs() < 1e-9 {
660        if w < 0.0 { -1e-9 } else { 1e-9 }
661    } else {
662        w
663    };
664    Vec2 {
665        x: ((matrix[0] * x + matrix[1] * y + matrix[2]) / w) as f32,
666        y: ((matrix[3] * x + matrix[4] * y + matrix[5]) / w) as f32,
667    }
668}
669
670fn invert_matrix(matrix: [f64; 9]) -> Option<[f64; 9]> {
671    let a = matrix[0];
672    let b = matrix[1];
673    let c = matrix[2];
674    let d = matrix[3];
675    let e = matrix[4];
676    let f = matrix[5];
677    let g = matrix[6];
678    let h = matrix[7];
679    let i = matrix[8];
680    let c00 = e * i - f * h;
681    let c01 = -(d * i - f * g);
682    let c02 = d * h - e * g;
683    let det = a * c00 + b * c01 + c * c02;
684    if !det.is_finite() || det.abs() < 1e-12 {
685        return None;
686    }
687    let inv = 1.0 / det;
688    Some([
689        c00 * inv,
690        -(b * i - c * h) * inv,
691        (b * f - c * e) * inv,
692        c01 * inv,
693        (a * i - c * g) * inv,
694        -(a * f - c * d) * inv,
695        c02 * inv,
696        -(a * h - b * g) * inv,
697        (a * e - b * d) * inv,
698    ])
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704
705    #[test]
706    fn pointer_coordinates_preserve_global_position_with_transform() {
707        let mut hit = HitRegion {
708            id: 9_876_543,
709            rect: Rect {
710                x: 10.0,
711                y: 20.0,
712                w: 100.0,
713                h: 50.0,
714            },
715            ..Default::default()
716        };
717        let context = HitContext::root().with_transform(Transform::translate(30.0, 40.0));
718        register_hit(&mut hit, &context, None);
719
720        let global = Vec2 { x: 50.0, y: 70.0 };
721        let (origin, local) = hit_region_pointer_coordinates(&hit, global);
722        assert_eq!(origin, Vec2 { x: 40.0, y: 60.0 });
723        assert_eq!(origin + local, global);
724
725        let mut event = crate::PointerEvent::new(
726            crate::PointerId(1),
727            crate::PointerKind::Mouse,
728            crate::PointerEventKind::Move,
729            global,
730            1.0,
731            crate::Modifiers::default(),
732        );
733        event.origin = origin;
734        event.position = local;
735        assert_eq!(event.position_in_window(), global);
736
737        let mut scaled = HitRegion {
738            id: 9_876_544,
739            rect: Rect {
740                x: 10.0,
741                y: 20.0,
742                w: 100.0,
743                h: 50.0,
744            },
745            ..Default::default()
746        };
747        let scaled_context = HitContext::root().with_transform(Transform {
748            scale_x: 2.0,
749            scale_y: 2.0,
750            ..Transform::identity()
751        });
752        register_hit(&mut scaled, &scaled_context, None);
753        let (origin, local) = hit_region_pointer_coordinates(&scaled, global);
754        let mut event = crate::PointerEvent::new(
755            crate::PointerId(2),
756            crate::PointerKind::Mouse,
757            crate::PointerEventKind::Move,
758            global,
759            1.0,
760            crate::Modifiers::default(),
761        );
762        event.origin = origin;
763        event.position = local;
764        assert_eq!(event.position_in_window(), global);
765    }
766}