use std::cell::Cell;
use geometry_core::Rect;
use platform_core::{Event, PointerButton};
use ui_tree::EventResult;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PointerButtons {
pub primary: bool,
pub secondary: bool,
pub auxiliary: bool,
}
impl PointerButtons {
pub fn any(self) -> bool {
self.primary || self.secondary || self.auxiliary
}
fn slot(&mut self, button: &PointerButton) -> &mut bool {
match button {
PointerButton::Primary => &mut self.primary,
PointerButton::Secondary => &mut self.secondary,
PointerButton::Auxiliary => &mut self.auxiliary,
}
}
pub(crate) fn holds(self, button: &PointerButton) -> bool {
match button {
PointerButton::Primary => self.primary,
PointerButton::Secondary => self.secondary,
PointerButton::Auxiliary => self.auxiliary,
}
}
pub(crate) fn with(mut self, button: &PointerButton) -> Self {
*self.slot(button) = true;
self
}
}
thread_local! {
static OCCLUDED: Cell<bool> = const { Cell::new(false) };
static BUTTONS: Cell<PointerButtons> = const {
Cell::new(PointerButtons { primary: false, secondary: false, auxiliary: false })
};
}
pub fn observe_pointer(event: &Event) {
BUTTONS.with(|b| {
let mut held = b.get();
match event {
Event::PointerPressed { button, .. } => *held.slot(button) = true,
Event::PointerReleased { button, .. } => *held.slot(button) = false,
Event::FocusChanged { is_focused: false } => held = PointerButtons::default(),
_ => return,
}
b.set(held);
});
}
pub fn pointer_buttons() -> PointerButtons {
BUTTONS.with(|b| b.get())
}
pub fn reset_pointer() {
BUTTONS.with(|b| b.set(PointerButtons::default()));
}
pub(crate) fn pointer_occluded() -> bool {
OCCLUDED.with(|c| c.get())
}
struct OccludedGuard(bool);
impl Drop for OccludedGuard {
fn drop(&mut self) {
OCCLUDED.with(|c| c.set(self.0));
}
}
fn occlude() -> OccludedGuard {
OccludedGuard(OCCLUDED.with(|c| c.replace(true)))
}
pub(crate) fn pointer_coords(event: &Event) -> Option<(f64, f64)> {
match event {
Event::PointerMoved { x, y, .. } => Some((*x, *y)),
Event::PointerPressed { x, y, .. } => Some((*x, *y)),
Event::PointerReleased { x, y, .. } => Some((*x, *y)),
Event::Scrolled { x, y, .. } => Some((*x, *y)),
_ => None,
}
}
pub fn transform_pointer(event: &Event, matrix: [f32; 6]) -> Option<Event> {
let inv = geometry_core::Transform::from_array(matrix).invert()?;
let apply = |world_x: f64, world_y: f64| -> (f64, f64) {
let local_x = inv.a as f64 * world_x + inv.c as f64 * world_y + inv.e as f64;
let local_y = inv.b as f64 * world_x + inv.d as f64 * world_y + inv.f as f64;
(local_x, local_y)
};
match event {
Event::PointerMoved { x, y, source } => {
let (local_x, local_y) = apply(*x, *y);
Some(Event::PointerMoved {
x: local_x,
y: local_y,
source: source.clone(),
})
}
Event::PointerPressed {
x,
y,
button,
source,
} => {
let (local_x, local_y) = apply(*x, *y);
Some(Event::PointerPressed {
x: local_x,
y: local_y,
button: button.clone(),
source: source.clone(),
})
}
Event::PointerReleased {
x,
y,
button,
source,
} => {
let (local_x, local_y) = apply(*x, *y);
Some(Event::PointerReleased {
x: local_x,
y: local_y,
button: button.clone(),
source: source.clone(),
})
}
Event::Scrolled { delta, x, y } => {
let (local_x, local_y) = apply(*x, *y);
Some(Event::Scrolled {
delta: delta.clone(),
x: local_x,
y: local_y,
})
}
_ => None,
}
}
pub(crate) fn offset_pointer(event: &Event, dx: f64, dy: f64) -> Option<Event> {
transform_pointer(event, [1.0, 0.0, 0.0, 1.0, dx as f32, dy as f32])
}
pub(crate) fn clip_pointer_event<'a>(event: &'a Event, rect: Rect) -> Option<&'a Event> {
match pointer_coords(event) {
Some((x, y)) if !rect.contains(x as f32, y as f32) => None,
_ => Some(event),
}
}
pub(crate) fn dispatch_container_event(
children: &mut crate::layout_item::TrackedChildren,
event: &Event,
) -> EventResult {
if matches!(
event,
Event::PointerMoved { .. } | Event::PointerReleased { .. }
) {
let over = pointer_coords(event).and_then(|(x, y)| {
children.iter().rposition(|c| {
c.rect
.as_ref()
.is_some_and(|sig| sig.get().contains(x as f32, y as f32))
&& c.item.borrow().pointer_opaque()
})
});
let mut any_handled = false;
for (i, child) in children.iter().enumerate() {
let _covered = (over.is_some_and(|top| top > i)).then(occlude);
if child.item.borrow_mut().on_event(event) == EventResult::Handled {
any_handled = true;
}
}
return if any_handled {
EventResult::Handled
} else {
EventResult::Ignored
};
}
let Some((x, y)) = pointer_coords(event).map(|(x, y)| (x as f32, y as f32)) else {
return dispatch_to_children(children, event);
};
for child in children.iter_mut().rev() {
let rect = child.rect.as_ref().map(|sig| sig.get());
if !rect.is_none_or(|r| r.contains(x, y)) {
continue;
}
let result = child.item.borrow_mut().on_event(event);
if result == EventResult::Handled
|| (rect.is_some() && child.item.borrow().pointer_opaque())
{
return result;
}
}
EventResult::Ignored
}
fn dispatch_to_children(
children: &mut crate::layout_item::TrackedChildren,
event: &Event,
) -> EventResult {
for child in children.iter_mut() {
if child.item.borrow_mut().on_event(event) == EventResult::Handled {
return EventResult::Handled;
}
}
EventResult::Ignored
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::Container;
use crate::context::{compute_layout, reset_layout_runtime};
use crate::layout_item::LayoutItem;
use crate::styled_container::StyledContainer;
use layout_core::{AvailableSpace, LayoutStyle};
use platform_core::ScrollDelta;
use renderer_core::RectStyle;
use std::cell::Cell;
use std::rc::Rc;
use ui_tree::Component;
#[test]
fn a_covering_sibling_takes_the_pointer_from_the_one_beneath() {
reset_layout_runtime();
let pane_wheels = Rc::new(Cell::new(0u32));
let sink = pane_wheels.clone();
let pane = StyledContainer::new(
LayoutStyle::new().width(400.0).height(400.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_scroll(move |_dx, _dy| sink.set(sink.get() + 1));
let panel = Container::new(
LayoutStyle::new()
.absolute()
.inset_end(0.0)
.inset_top(0.0)
.width(100.0)
.height(400.0),
vec![],
)
.unwrap();
let mut root = Container::new(
LayoutStyle::new().flex_row().width(400.0).height(400.0),
vec![Box::new(pane), Box::new(panel)],
)
.unwrap();
compute_layout(
root.layout_node(),
AvailableSpace::Definite(400.0),
AvailableSpace::Definite(400.0),
)
.unwrap();
let wheel = |x: f64| Event::Scrolled {
delta: ScrollDelta::Lines { x: 0.0, y: -3.0 },
x,
y: 200.0,
};
root.on_event(&wheel(50.0));
assert_eq!(pane_wheels.get(), 1, "over the pane it is the pane's");
root.on_event(&wheel(350.0));
assert_eq!(
pane_wheels.get(),
1,
"over the panel it is the panel's, even though the panel ignored it"
);
}
#[test]
fn a_covered_pane_is_not_hovered_by_a_move_over_the_panel() {
use platform_core::PointerSource;
reset_layout_runtime();
let at: Rc<Cell<Option<(f32, f32)>>> = Rc::new(Cell::new(None));
let sink = at.clone();
let pane = StyledContainer::new(
LayoutStyle::new().width(400.0).height(400.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_pointer_move(move |x, y| sink.set(Some((x, y))));
let panel = Container::new(
LayoutStyle::new()
.absolute()
.inset_end(0.0)
.inset_top(0.0)
.width(100.0)
.height(400.0),
vec![],
)
.unwrap();
let mut root = Container::new(
LayoutStyle::new().flex_row().width(400.0).height(400.0),
vec![Box::new(pane), Box::new(panel)],
)
.unwrap();
compute_layout(
root.layout_node(),
AvailableSpace::Definite(400.0),
AvailableSpace::Definite(400.0),
)
.unwrap();
let moved = |x: f64| Event::PointerMoved {
x,
y: 200.0,
source: PointerSource::Mouse,
};
root.on_event(&moved(50.0));
assert_eq!(at.get(), Some((50.0, 200.0)), "over the pane it tracks");
at.set(None);
root.on_event(&moved(350.0));
assert_eq!(at.get(), None, "the panel is in front of it there");
}
#[test]
fn a_click_through_label_does_not_stand_between_the_pointer_and_the_pane() {
use platform_core::PointerSource;
reset_layout_runtime();
let wheels = Rc::new(Cell::new(0u32));
let at: Rc<Cell<Option<(f32, f32)>>> = Rc::new(Cell::new(None));
let (wheel_sink, move_sink) = (wheels.clone(), at.clone());
let pane = StyledContainer::new(
LayoutStyle::new().width(400.0).height(400.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_scroll(move |_dx, _dy| wheel_sink.set(wheel_sink.get() + 1))
.on_pointer_move(move |x, y| move_sink.set(Some((x, y))));
let readout = StyledContainer::new(
LayoutStyle::new()
.absolute()
.inset_end(0.0)
.inset_top(0.0)
.width(100.0)
.height(400.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.click_through(true);
let mut root = Container::new(
LayoutStyle::new().flex_row().width(400.0).height(400.0),
vec![Box::new(pane), Box::new(readout)],
)
.unwrap();
compute_layout(
root.layout_node(),
AvailableSpace::Definite(400.0),
AvailableSpace::Definite(400.0),
)
.unwrap();
root.on_event(&Event::Scrolled {
delta: ScrollDelta::Lines { x: 0.0, y: -3.0 },
x: 350.0,
y: 200.0,
});
assert_eq!(wheels.get(), 1, "the wheel reached the pane underneath");
root.on_event(&Event::PointerMoved {
x: 350.0,
y: 200.0,
source: PointerSource::Mouse,
});
assert_eq!(
at.get(),
Some((350.0, 200.0)),
"and the pane is still the thing the pointer is over"
);
}
#[test]
fn a_control_inside_a_click_through_bar_is_still_hovered() {
use platform_core::PointerSource;
reset_layout_runtime();
let pane_at: Rc<Cell<Option<(f32, f32)>>> = Rc::new(Cell::new(None));
let button_at: Rc<Cell<Option<(f32, f32)>>> = Rc::new(Cell::new(None));
let (pane_sink, button_sink) = (pane_at.clone(), button_at.clone());
let pane = StyledContainer::new(
LayoutStyle::new().width(400.0).height(400.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_pointer_move(move |x, y| pane_sink.set(Some((x, y))));
let button = StyledContainer::new(
LayoutStyle::new().width(60.0).height(30.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.on_pointer_move(move |x, y| button_sink.set(Some((x, y))));
let bar = StyledContainer::new(
LayoutStyle::new()
.absolute()
.inset_top(0.0)
.width(400.0)
.height(40.0),
|_r| RectStyle::default(),
vec![Box::new(button)],
)
.unwrap()
.click_through(true);
let mut root = Container::new(
LayoutStyle::new().flex_row().width(400.0).height(400.0),
vec![Box::new(pane), Box::new(bar)],
)
.unwrap();
compute_layout(
root.layout_node(),
AvailableSpace::Definite(400.0),
AvailableSpace::Definite(400.0),
)
.unwrap();
root.on_event(&Event::PointerMoved {
x: 30.0,
y: 15.0,
source: PointerSource::Mouse,
});
assert_eq!(
button_at.get(),
Some((30.0, 15.0)),
"the button in the bar is hovered"
);
assert_eq!(
pane_at.get(),
Some((30.0, 15.0)),
"and the pane under it goes on tracking"
);
}
#[test]
fn cursor_leaving_the_window_does_not_forget_a_held_button() {
use platform_core::{PointerButton, PointerSource};
reset_pointer();
observe_pointer(&Event::PointerPressed {
x: 10.0,
y: 10.0,
button: PointerButton::Secondary,
source: PointerSource::Mouse,
});
observe_pointer(&Event::CursorLeft);
assert!(
pointer_buttons().secondary,
"the button that armed the drag is still down"
);
observe_pointer(&Event::FocusChanged { is_focused: false });
assert!(!pointer_buttons().any());
}
}