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}