use std::rc::Rc;
use geometry_core::Rect;
use platform_core::Event;
use crate::component::EventResult;
pub trait OverlaySink {
fn content_rect(&self) -> Rect;
fn dispatch(&self, event: &Event) -> EventResult;
fn blocking(&self) -> bool {
true
}
}
reactive_core::surface_local! {
slot OVERLAYS: OverlayRegistry = OverlayRegistry::new();
access with_overlays, with_overlays_ref;
context OverlayContext, OverlayGuard;
}
struct OverlayRegistry {
entries: Vec<(u64, Rc<dyn OverlaySink>)>,
captured: Option<u64>,
next_id: u64,
}
impl OverlayRegistry {
fn new() -> Self {
Self {
entries: Vec::new(),
captured: None,
next_id: 0,
}
}
}
pub fn register_overlay(sink: Rc<dyn OverlaySink>) -> u64 {
with_overlays(|r| {
let id = r.next_id;
r.next_id += 1;
r.entries.push((id, sink));
id
})
}
pub fn unregister_overlay(id: u64) {
with_overlays(|r| {
r.entries.retain(|(entry_id, _)| *entry_id != id);
if r.captured == Some(id) {
r.captured = None;
}
});
}
fn pointer_pos(event: &Event) -> Option<(f32, f32)> {
match event {
Event::PointerPressed { x, y, .. }
| Event::PointerMoved { x, y, .. }
| Event::PointerReleased { x, y, .. } => Some((*x as f32, *y as f32)),
_ => None,
}
}
pub fn dispatch_overlays(event: &Event) -> EventResult {
let (entries, captured) = with_overlays_ref(|r| (r.entries.clone(), r.captured));
if entries.is_empty() {
return EventResult::Ignored;
}
match event {
Event::PointerPressed { .. } => {
let (x, y) = pointer_pos(event).unwrap();
for (id, sink) in entries.iter().rev() {
if sink.content_rect().contains(x, y) {
let handled = sink.dispatch(event) == EventResult::Handled;
if sink.blocking() || handled {
with_overlays(|r| r.captured = Some(*id));
return EventResult::Handled;
}
}
}
EventResult::Ignored
}
Event::PointerMoved { .. } | Event::PointerReleased { .. } => {
let is_release = matches!(event, Event::PointerReleased { .. });
if let Some(cap_id) = captured {
if let Some((_, sink)) = entries.iter().find(|(id, _)| *id == cap_id) {
sink.dispatch(event);
if is_release {
with_overlays(|r| r.captured = None);
}
return EventResult::Handled;
}
with_overlays(|r| r.captured = None);
}
let (x, y) = pointer_pos(event).unwrap();
for (_, sink) in entries.iter().rev() {
if sink.content_rect().contains(x, y) {
let handled = sink.dispatch(event) == EventResult::Handled;
if sink.blocking() || handled {
return EventResult::Handled;
}
}
}
EventResult::Ignored
}
_ => EventResult::Ignored,
}
}
#[cfg(test)]
fn reset() {
with_overlays(|r| *r = OverlayRegistry::new());
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use platform_core::{PointerButton, PointerSource};
use super::*;
struct RecordingSink {
rect: Rect,
hits: Rc<Cell<u32>>,
blocking: bool,
child_handles: bool,
}
impl OverlaySink for RecordingSink {
fn content_rect(&self) -> Rect {
self.rect
}
fn dispatch(&self, _event: &Event) -> EventResult {
self.hits.set(self.hits.get() + 1);
if self.child_handles {
EventResult::Handled
} else {
EventResult::Ignored
}
}
fn blocking(&self) -> bool {
self.blocking
}
}
fn sink(rect: Rect) -> (Rc<dyn OverlaySink>, Rc<Cell<u32>>) {
configured_sink(rect, true, true)
}
fn configured_sink(
rect: Rect,
blocking: bool,
child_handles: bool,
) -> (Rc<dyn OverlaySink>, Rc<Cell<u32>>) {
let hits = Rc::new(Cell::new(0));
let sink: Rc<dyn OverlaySink> = Rc::new(RecordingSink {
rect,
hits: Rc::clone(&hits),
blocking,
child_handles,
});
(sink, hits)
}
fn press(x: f64, y: f64) -> Event {
Event::PointerPressed {
x,
y,
button: PointerButton::Primary,
source: PointerSource::Mouse,
}
}
fn moved(x: f64, y: f64) -> Event {
Event::PointerMoved {
x,
y,
source: PointerSource::Mouse,
}
}
fn released(x: f64, y: f64) -> Event {
Event::PointerReleased {
x,
y,
button: PointerButton::Primary,
source: PointerSource::Mouse,
}
}
#[test]
fn no_overlays_falls_through() {
reset();
assert_eq!(dispatch_overlays(&press(10.0, 10.0)), EventResult::Ignored);
}
#[test]
fn press_inside_is_consumed_outside_falls_through() {
reset();
let (s, hits) = sink(Rect::new(0.0, 0.0, 100.0, 100.0));
let id = register_overlay(s);
assert_eq!(dispatch_overlays(&press(50.0, 50.0)), EventResult::Handled);
assert_eq!(hits.get(), 1);
dispatch_overlays(&released(50.0, 50.0));
assert_eq!(
dispatch_overlays(&press(500.0, 500.0)),
EventResult::Ignored
);
unregister_overlay(id);
}
#[test]
fn topmost_overlay_wins() {
reset();
let (bottom, bottom_hits) = sink(Rect::new(0.0, 0.0, 100.0, 100.0));
let (top, top_hits) = sink(Rect::new(0.0, 0.0, 100.0, 100.0));
let b = register_overlay(bottom);
let t = register_overlay(top);
dispatch_overlays(&press(50.0, 50.0));
assert_eq!(
top_hits.get(),
1,
"topmost (last registered) receives the press"
);
assert_eq!(
bottom_hits.get(),
0,
"the overlay below must not also get it"
);
unregister_overlay(t);
unregister_overlay(b);
}
#[test]
fn capture_routes_moves_and_release_even_outside() {
reset();
let (s, hits) = sink(Rect::new(0.0, 0.0, 100.0, 100.0));
let id = register_overlay(s);
assert_eq!(dispatch_overlays(&press(50.0, 50.0)), EventResult::Handled);
assert_eq!(
dispatch_overlays(&moved(500.0, 500.0)),
EventResult::Handled
);
assert_eq!(
dispatch_overlays(&released(500.0, 500.0)),
EventResult::Handled
);
assert_eq!(hits.get(), 3);
assert_eq!(
dispatch_overlays(&press(500.0, 500.0)),
EventResult::Ignored
);
unregister_overlay(id);
}
#[test]
fn unregister_stops_routing_and_clears_capture() {
reset();
let (s, _hits) = sink(Rect::new(0.0, 0.0, 100.0, 100.0));
let id = register_overlay(s);
dispatch_overlays(&press(50.0, 50.0));
unregister_overlay(id);
assert_eq!(
dispatch_overlays(&released(50.0, 50.0)),
EventResult::Ignored
);
assert_eq!(dispatch_overlays(&press(50.0, 50.0)), EventResult::Ignored);
}
#[test]
fn blocking_overlay_consumes_press_in_empty_region() {
reset();
let (s, hits) = configured_sink(Rect::new(0.0, 0.0, 100.0, 100.0), true, false);
let id = register_overlay(s);
assert_eq!(dispatch_overlays(&press(50.0, 50.0)), EventResult::Handled);
assert_eq!(
hits.get(),
1,
"the modal is still asked to dispatch the press"
);
unregister_overlay(id);
}
#[test]
fn click_through_overlay_falls_through_when_child_ignores() {
reset();
let (s, hits) = configured_sink(Rect::new(0.0, 0.0, 100.0, 100.0), false, false);
let id = register_overlay(s);
assert_eq!(dispatch_overlays(&press(50.0, 50.0)), EventResult::Ignored);
assert_eq!(
hits.get(),
1,
"the overlay is offered the press before falling through"
);
assert_eq!(dispatch_overlays(&moved(50.0, 50.0)), EventResult::Ignored);
unregister_overlay(id);
}
#[test]
fn click_through_overlay_consumes_when_child_handles() {
reset();
let (s, hits) = configured_sink(Rect::new(0.0, 0.0, 100.0, 100.0), false, true);
let id = register_overlay(s);
assert_eq!(dispatch_overlays(&press(50.0, 50.0)), EventResult::Handled);
assert_eq!(
dispatch_overlays(&released(500.0, 500.0)),
EventResult::Handled
);
assert_eq!(hits.get(), 2);
unregister_overlay(id);
}
}