1use std::cell::Cell;
2
3use geometry_core::Rect;
4use platform_core::{Event, PointerButton};
5use ui_tree::EventResult;
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub struct PointerButtons {
15 pub primary: bool,
16 pub secondary: bool,
17 pub auxiliary: bool,
18}
19
20impl PointerButtons {
21 pub fn any(self) -> bool {
23 self.primary || self.secondary || self.auxiliary
24 }
25
26 fn slot(&mut self, button: &PointerButton) -> &mut bool {
27 match button {
28 PointerButton::Primary => &mut self.primary,
29 PointerButton::Secondary => &mut self.secondary,
30 PointerButton::Auxiliary => &mut self.auxiliary,
31 }
32 }
33
34 pub(crate) fn holds(self, button: &PointerButton) -> bool {
35 match button {
36 PointerButton::Primary => self.primary,
37 PointerButton::Secondary => self.secondary,
38 PointerButton::Auxiliary => self.auxiliary,
39 }
40 }
41
42 pub(crate) fn with(mut self, button: &PointerButton) -> Self {
43 *self.slot(button) = true;
44 self
45 }
46}
47
48thread_local! {
49 static OCCLUDED: Cell<bool> = const { Cell::new(false) };
52 static BUTTONS: Cell<PointerButtons> = const {
53 Cell::new(PointerButtons { primary: false, secondary: false, auxiliary: false })
54 };
55}
56
57pub fn observe_pointer(event: &Event) {
60 BUTTONS.with(|b| {
61 let mut held = b.get();
62 match event {
63 Event::PointerPressed { button, .. } => *held.slot(button) = true,
64 Event::PointerReleased { button, .. } => *held.slot(button) = false,
65 Event::FocusChanged { is_focused: false } => held = PointerButtons::default(),
67 _ => return,
68 }
69 b.set(held);
70 });
71}
72
73pub fn pointer_buttons() -> PointerButtons {
75 BUTTONS.with(|b| b.get())
76}
77
78pub fn reset_pointer() {
80 BUTTONS.with(|b| b.set(PointerButtons::default()));
81}
82
83pub(crate) fn pointer_occluded() -> bool {
91 OCCLUDED.with(|c| c.get())
92}
93
94struct OccludedGuard(bool);
95
96impl Drop for OccludedGuard {
97 fn drop(&mut self) {
98 OCCLUDED.with(|c| c.set(self.0));
99 }
100}
101
102fn occlude() -> OccludedGuard {
105 OccludedGuard(OCCLUDED.with(|c| c.replace(true)))
106}
107
108pub(crate) fn pointer_coords(event: &Event) -> Option<(f64, f64)> {
109 match event {
110 Event::PointerMoved { x, y, .. } => Some((*x, *y)),
111 Event::PointerPressed { x, y, .. } => Some((*x, *y)),
112 Event::PointerReleased { x, y, .. } => Some((*x, *y)),
113 Event::Scrolled { x, y, .. } => Some((*x, *y)),
114 _ => None,
115 }
116}
117
118pub fn transform_pointer(event: &Event, matrix: [f32; 6]) -> Option<Event> {
125 let inv = geometry_core::Transform::from_array(matrix).invert()?;
126 let apply = |world_x: f64, world_y: f64| -> (f64, f64) {
128 let local_x = inv.a as f64 * world_x + inv.c as f64 * world_y + inv.e as f64;
129 let local_y = inv.b as f64 * world_x + inv.d as f64 * world_y + inv.f as f64;
130 (local_x, local_y)
131 };
132 match event {
133 Event::PointerMoved { x, y, source } => {
134 let (local_x, local_y) = apply(*x, *y);
135 Some(Event::PointerMoved {
136 x: local_x,
137 y: local_y,
138 source: source.clone(),
139 })
140 }
141 Event::PointerPressed {
142 x,
143 y,
144 button,
145 source,
146 } => {
147 let (local_x, local_y) = apply(*x, *y);
148 Some(Event::PointerPressed {
149 x: local_x,
150 y: local_y,
151 button: button.clone(),
152 source: source.clone(),
153 })
154 }
155 Event::PointerReleased {
156 x,
157 y,
158 button,
159 source,
160 } => {
161 let (local_x, local_y) = apply(*x, *y);
162 Some(Event::PointerReleased {
163 x: local_x,
164 y: local_y,
165 button: button.clone(),
166 source: source.clone(),
167 })
168 }
169 Event::Scrolled { delta, x, y } => {
172 let (local_x, local_y) = apply(*x, *y);
173 Some(Event::Scrolled {
174 delta: delta.clone(),
175 x: local_x,
176 y: local_y,
177 })
178 }
179 _ => None,
180 }
181}
182
183pub(crate) fn offset_pointer(event: &Event, dx: f64, dy: f64) -> Option<Event> {
184 transform_pointer(event, [1.0, 0.0, 0.0, 1.0, dx as f32, dy as f32])
185}
186
187pub(crate) fn clip_pointer_event<'a>(event: &'a Event, rect: Rect) -> Option<&'a Event> {
189 match pointer_coords(event) {
190 Some((x, y)) if !rect.contains(x as f32, y as f32) => None,
191 _ => Some(event),
192 }
193}
194
195pub(crate) fn dispatch_container_event(
196 children: &mut crate::layout_item::TrackedChildren,
197 event: &Event,
198) -> EventResult {
199 if matches!(
205 event,
206 Event::PointerMoved { .. } | Event::PointerReleased { .. }
207 ) {
208 let over = pointer_coords(event).and_then(|(x, y)| {
211 children.iter().rposition(|c| {
212 c.rect
213 .as_ref()
214 .is_some_and(|sig| sig.get().contains(x as f32, y as f32))
215 && c.item.borrow().pointer_opaque()
216 })
217 });
218 let mut any_handled = false;
219 for (i, child) in children.iter().enumerate() {
220 let _covered = (over.is_some_and(|top| top > i)).then(occlude);
226 if child.item.borrow_mut().on_event(event) == EventResult::Handled {
227 any_handled = true;
228 }
229 }
230 return if any_handled {
231 EventResult::Handled
232 } else {
233 EventResult::Ignored
234 };
235 }
236 let Some((x, y)) = pointer_coords(event).map(|(x, y)| (x as f32, y as f32)) else {
237 return dispatch_to_children(children, event);
238 };
239 for child in children.iter_mut().rev() {
245 let rect = child.rect.as_ref().map(|sig| sig.get());
247 if !rect.is_none_or(|r| r.contains(x, y)) {
248 continue;
249 }
250 let result = child.item.borrow_mut().on_event(event);
251 if result == EventResult::Handled
254 || (rect.is_some() && child.item.borrow().pointer_opaque())
255 {
256 return result;
257 }
258 }
259 EventResult::Ignored
260}
261
262fn dispatch_to_children(
263 children: &mut crate::layout_item::TrackedChildren,
264 event: &Event,
265) -> EventResult {
266 for child in children.iter_mut() {
267 if child.item.borrow_mut().on_event(event) == EventResult::Handled {
268 return EventResult::Handled;
269 }
270 }
271 EventResult::Ignored
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use crate::container::Container;
278 use crate::context::{compute_layout, reset_layout_runtime};
279 use crate::layout_item::LayoutItem;
280 use crate::styled_container::StyledContainer;
281 use layout_core::{AvailableSpace, LayoutStyle};
282 use platform_core::ScrollDelta;
283 use renderer_core::RectStyle;
284 use std::cell::Cell;
285 use std::rc::Rc;
286 use ui_tree::Component;
287
288 #[test]
292 fn a_covering_sibling_takes_the_pointer_from_the_one_beneath() {
293 reset_layout_runtime();
294 let pane_wheels = Rc::new(Cell::new(0u32));
295 let sink = pane_wheels.clone();
296 let pane = StyledContainer::new(
297 LayoutStyle::new().width(400.0).height(400.0),
298 |_r| RectStyle::default(),
299 vec![],
300 )
301 .unwrap()
302 .on_scroll(move |_dx, _dy| sink.set(sink.get() + 1));
303 let panel = Container::new(
305 LayoutStyle::new()
306 .absolute()
307 .inset_end(0.0)
308 .inset_top(0.0)
309 .width(100.0)
310 .height(400.0),
311 vec![],
312 )
313 .unwrap();
314 let mut root = Container::new(
315 LayoutStyle::new().flex_row().width(400.0).height(400.0),
316 vec![Box::new(pane), Box::new(panel)],
317 )
318 .unwrap();
319 compute_layout(
320 root.layout_node(),
321 AvailableSpace::Definite(400.0),
322 AvailableSpace::Definite(400.0),
323 )
324 .unwrap();
325
326 let wheel = |x: f64| Event::Scrolled {
327 delta: ScrollDelta::Lines { x: 0.0, y: -3.0 },
328 x,
329 y: 200.0,
330 };
331 root.on_event(&wheel(50.0));
332 assert_eq!(pane_wheels.get(), 1, "over the pane it is the pane's");
333 root.on_event(&wheel(350.0));
334 assert_eq!(
335 pane_wheels.get(),
336 1,
337 "over the panel it is the panel's, even though the panel ignored it"
338 );
339 }
340
341 #[test]
344 fn a_covered_pane_is_not_hovered_by_a_move_over_the_panel() {
345 use platform_core::PointerSource;
346 reset_layout_runtime();
347 let at: Rc<Cell<Option<(f32, f32)>>> = Rc::new(Cell::new(None));
348 let sink = at.clone();
349 let pane = StyledContainer::new(
350 LayoutStyle::new().width(400.0).height(400.0),
351 |_r| RectStyle::default(),
352 vec![],
353 )
354 .unwrap()
355 .on_pointer_move(move |x, y| sink.set(Some((x, y))));
356 let panel = Container::new(
357 LayoutStyle::new()
358 .absolute()
359 .inset_end(0.0)
360 .inset_top(0.0)
361 .width(100.0)
362 .height(400.0),
363 vec![],
364 )
365 .unwrap();
366 let mut root = Container::new(
367 LayoutStyle::new().flex_row().width(400.0).height(400.0),
368 vec![Box::new(pane), Box::new(panel)],
369 )
370 .unwrap();
371 compute_layout(
372 root.layout_node(),
373 AvailableSpace::Definite(400.0),
374 AvailableSpace::Definite(400.0),
375 )
376 .unwrap();
377
378 let moved = |x: f64| Event::PointerMoved {
379 x,
380 y: 200.0,
381 source: PointerSource::Mouse,
382 };
383 root.on_event(&moved(50.0));
384 assert_eq!(at.get(), Some((50.0, 200.0)), "over the pane it tracks");
385 at.set(None);
386 root.on_event(&moved(350.0));
387 assert_eq!(at.get(), None, "the panel is in front of it there");
388 }
389
390 #[test]
395 fn a_click_through_label_does_not_stand_between_the_pointer_and_the_pane() {
396 use platform_core::PointerSource;
397 reset_layout_runtime();
398 let wheels = Rc::new(Cell::new(0u32));
399 let at: Rc<Cell<Option<(f32, f32)>>> = Rc::new(Cell::new(None));
400 let (wheel_sink, move_sink) = (wheels.clone(), at.clone());
401 let pane = StyledContainer::new(
402 LayoutStyle::new().width(400.0).height(400.0),
403 |_r| RectStyle::default(),
404 vec![],
405 )
406 .unwrap()
407 .on_scroll(move |_dx, _dy| wheel_sink.set(wheel_sink.get() + 1))
408 .on_pointer_move(move |x, y| move_sink.set(Some((x, y))));
409 let readout = StyledContainer::new(
410 LayoutStyle::new()
411 .absolute()
412 .inset_end(0.0)
413 .inset_top(0.0)
414 .width(100.0)
415 .height(400.0),
416 |_r| RectStyle::default(),
417 vec![],
418 )
419 .unwrap()
420 .click_through(true);
421 let mut root = Container::new(
422 LayoutStyle::new().flex_row().width(400.0).height(400.0),
423 vec![Box::new(pane), Box::new(readout)],
424 )
425 .unwrap();
426 compute_layout(
427 root.layout_node(),
428 AvailableSpace::Definite(400.0),
429 AvailableSpace::Definite(400.0),
430 )
431 .unwrap();
432
433 root.on_event(&Event::Scrolled {
434 delta: ScrollDelta::Lines { x: 0.0, y: -3.0 },
435 x: 350.0,
436 y: 200.0,
437 });
438 assert_eq!(wheels.get(), 1, "the wheel reached the pane underneath");
439 root.on_event(&Event::PointerMoved {
440 x: 350.0,
441 y: 200.0,
442 source: PointerSource::Mouse,
443 });
444 assert_eq!(
445 at.get(),
446 Some((350.0, 200.0)),
447 "and the pane is still the thing the pointer is over"
448 );
449 }
450
451 #[test]
456 fn a_control_inside_a_click_through_bar_is_still_hovered() {
457 use platform_core::PointerSource;
458 reset_layout_runtime();
459 let pane_at: Rc<Cell<Option<(f32, f32)>>> = Rc::new(Cell::new(None));
460 let button_at: Rc<Cell<Option<(f32, f32)>>> = Rc::new(Cell::new(None));
461 let (pane_sink, button_sink) = (pane_at.clone(), button_at.clone());
462 let pane = StyledContainer::new(
463 LayoutStyle::new().width(400.0).height(400.0),
464 |_r| RectStyle::default(),
465 vec![],
466 )
467 .unwrap()
468 .on_pointer_move(move |x, y| pane_sink.set(Some((x, y))));
469 let button = StyledContainer::new(
470 LayoutStyle::new().width(60.0).height(30.0),
471 |_r| RectStyle::default(),
472 vec![],
473 )
474 .unwrap()
475 .on_pointer_move(move |x, y| button_sink.set(Some((x, y))));
476 let bar = StyledContainer::new(
477 LayoutStyle::new()
478 .absolute()
479 .inset_top(0.0)
480 .width(400.0)
481 .height(40.0),
482 |_r| RectStyle::default(),
483 vec![Box::new(button)],
484 )
485 .unwrap()
486 .click_through(true);
487 let mut root = Container::new(
488 LayoutStyle::new().flex_row().width(400.0).height(400.0),
489 vec![Box::new(pane), Box::new(bar)],
490 )
491 .unwrap();
492 compute_layout(
493 root.layout_node(),
494 AvailableSpace::Definite(400.0),
495 AvailableSpace::Definite(400.0),
496 )
497 .unwrap();
498
499 root.on_event(&Event::PointerMoved {
500 x: 30.0,
501 y: 15.0,
502 source: PointerSource::Mouse,
503 });
504 assert_eq!(
505 button_at.get(),
506 Some((30.0, 15.0)),
507 "the button in the bar is hovered"
508 );
509 assert_eq!(
510 pane_at.get(),
511 Some((30.0, 15.0)),
512 "and the pane under it goes on tracking"
513 );
514 }
515
516 #[test]
521 fn cursor_leaving_the_window_does_not_forget_a_held_button() {
522 use platform_core::{PointerButton, PointerSource};
523
524 reset_pointer();
525 observe_pointer(&Event::PointerPressed {
526 x: 10.0,
527 y: 10.0,
528 button: PointerButton::Secondary,
529 source: PointerSource::Mouse,
530 });
531 observe_pointer(&Event::CursorLeft);
532 assert!(
533 pointer_buttons().secondary,
534 "the button that armed the drag is still down"
535 );
536
537 observe_pointer(&Event::FocusChanged { is_focused: false });
538 assert!(!pointer_buttons().any());
539 }
540}