use std::{
cell::{Cell, RefCell},
rc::Rc,
time::{Duration, Instant},
};
use crossterm::event::{Event, KeyModifiers, MouseButton, MouseEventKind};
use ratatui::layout::{Position, Rect};
use super::use_input::new_stamp_handle;
use crate::{
Hook, Hooks, SystemContext, UseContext,
input::{
CurrentLayer, EventOptions, EventPriority, EventResult, LayerId, PointerToken, StampHandle,
rect_contains,
},
};
const DOUBLE_CLICK_WINDOW: Duration = Duration::from_millis(300);
const CLICK_POSITION_TOLERANCE: u16 = 2;
const CLICK_COUNT_MAX: u8 = 3;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PointerKind {
Down(MouseButton),
Up(MouseButton),
Click { button: MouseButton, count: u8 },
Drag(MouseButton),
Move,
Enter,
Leave,
ScrollUp,
ScrollDown,
ScrollLeft,
ScrollRight,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct PointerEvent {
pub kind: PointerKind,
pub position: Position,
pub modifiers: KeyModifiers,
pub area: Rect,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct MouseButtons(u8);
impl Default for MouseButtons {
fn default() -> Self {
Self::LEFT
}
}
impl MouseButtons {
pub const LEFT: Self = Self(0b001);
pub const RIGHT: Self = Self(0b010);
pub const MIDDLE: Self = Self(0b100);
pub const ALL: Self = Self(0b111);
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
#[must_use]
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 != 0
}
}
pub(crate) fn buttons_of(button: MouseButton) -> MouseButtons {
match button {
MouseButton::Left => MouseButtons::LEFT,
MouseButton::Right => MouseButtons::RIGHT,
MouseButton::Middle => MouseButtons::MIDDLE,
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct PointerOptions {
pub buttons: MouseButtons,
pub track_move: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Click {
pub count: u8,
pub button: MouseButton,
pub position: Position,
pub modifiers: KeyModifiers,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DragPhase {
Start,
Move,
End,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct DragUpdate {
pub phase: DragPhase,
pub button: MouseButton,
pub from: Position,
pub to: Position,
pub modifiers: KeyModifiers,
}
pub(crate) struct ClickSynthesizer {
pending: Option<(MouseButton, Instant, u16, u16)>,
last_click: Option<(MouseButton, Instant, u16, u16)>,
count: u8,
}
impl ClickSynthesizer {
pub(crate) fn new() -> Self {
Self {
pending: None,
last_click: None,
count: 0,
}
}
pub(crate) fn on_down(&mut self, button: MouseButton, column: u16, row: u16) {
let now = Instant::now();
let consecutive = self.last_click.is_some_and(|(b, t, c, r)| {
b == button
&& now.duration_since(t) <= DOUBLE_CLICK_WINDOW
&& column.abs_diff(c) <= CLICK_POSITION_TOLERANCE
&& row.abs_diff(r) <= CLICK_POSITION_TOLERANCE
});
self.count = if consecutive {
(self.count + 1).min(CLICK_COUNT_MAX)
} else {
1
};
self.pending = Some((button, now, column, row));
}
pub(crate) fn on_up(&mut self, button: MouseButton, inside: bool) -> Option<u8> {
let matched = self.pending.take().filter(|(b, ..)| *b == button);
match matched {
Some(info) if inside => {
self.last_click = Some(info);
Some(self.count)
}
_ => None,
}
}
}
pub(crate) struct PointerHookBase {
stamp: StampHandle,
token: Cell<Option<PointerToken>>,
}
pub(crate) trait AsPointerBase {
fn as_base(&self) -> &PointerHookBase;
fn new_pointer_hook() -> Self;
}
impl PointerHookBase {
pub(crate) fn new() -> Self {
Self {
stamp: new_stamp_handle(),
token: Cell::new(None),
}
}
pub(crate) fn identity(&self) -> (StampHandle, PointerToken) {
let token = self.token.get().unwrap_or_else(|| {
let token = crate::input::mint_pointer_token();
self.token.set(Some(token));
token
});
(self.stamp.clone(), token)
}
fn pre_draw(&self, area: Rect) {
self.stamp.set(crate::input::draw_clock_stamp(area));
}
}
fn current_layer_id(hooks: &Hooks<'_, '_>) -> LayerId {
hooks
.try_use_context::<CurrentLayer>()
.map(|c| c.0)
.unwrap_or_else(|| hooks.use_context::<SystemContext>().input.root_layer())
}
fn debug_assert_mouse_enabled(sys: &SystemContext) {
debug_assert!(
sys.mouse_enabled(),
"pointer handler registered but mouse capture is not enabled;\n \
enable it at the app entry: \
element!(App).fullscreen_with(RunOptions {{ mouse: true, ..Default::default() }})"
);
}
mod private {
pub trait Sealed {}
impl Sealed for crate::hooks::Hooks<'_, '_> {}
}
pub trait UseClick: private::Sealed {
fn use_click<F>(&mut self, f: F)
where
F: FnMut(Click) + 'static;
}
struct UseClickImpl {
base: PointerHookBase,
synth: Rc<RefCell<ClickSynthesizer>>,
}
impl AsPointerBase for UseClickImpl {
fn as_base(&self) -> &PointerHookBase {
&self.base
}
fn new_pointer_hook() -> Self {
Self {
base: PointerHookBase::new(),
synth: Rc::new(RefCell::new(ClickSynthesizer::new())),
}
}
}
impl Hook for UseClickImpl {
fn pre_component_draw(&mut self, drawer: &mut crate::ComponentDrawer) {
self.base.pre_draw(drawer.area);
}
}
impl UseClick for Hooks<'_, '_> {
fn use_click<F>(&mut self, mut f: F)
where
F: FnMut(Click) + 'static,
{
let hook = self.use_hook(UseClickImpl::new_pointer_hook);
let (stamp, token) = hook.as_base().identity();
let synth = hook.synth.clone();
let hit_area = stamp.clone();
let layer = current_layer_id(self);
let mut sys = self.use_context_mut::<SystemContext>();
debug_assert_mouse_enabled(&sys);
sys.input.register_handler(
Some(layer),
EventPriority::Normal,
EventOptions { hit_test: true },
stamp,
Some(token),
Box::new(move |event| {
let Event::Mouse(m) = event else {
return EventResult::Ignored;
};
match m.kind {
MouseEventKind::Down(button) => {
synth.borrow_mut().on_down(button, m.column, m.row);
EventResult::Consumed
}
MouseEventKind::Up(button) => {
let inside = rect_contains(hit_area.get().area, m.column, m.row);
if let Some(count) = synth.borrow_mut().on_up(button, inside) {
f(Click {
count,
button,
position: Position::new(m.column, m.row),
modifiers: m.modifiers,
});
}
EventResult::Consumed
}
_ => EventResult::Ignored,
}
}),
);
}
}
pub trait UseHover: private::Sealed {
fn use_hover(&mut self) -> bool;
}
struct UseHoverImpl {
base: PointerHookBase,
}
impl AsPointerBase for UseHoverImpl {
fn as_base(&self) -> &PointerHookBase {
&self.base
}
fn new_pointer_hook() -> Self {
Self {
base: PointerHookBase::new(),
}
}
}
impl Hook for UseHoverImpl {
fn pre_component_draw(&mut self, drawer: &mut crate::ComponentDrawer) {
self.base.pre_draw(drawer.area);
}
}
impl UseHover for Hooks<'_, '_> {
fn use_hover(&mut self) -> bool {
let (stamp, token) = self
.use_hook(UseHoverImpl::new_pointer_hook)
.as_base()
.identity();
let layer = current_layer_id(self);
let mut sys = self.use_context_mut::<SystemContext>();
debug_assert_mouse_enabled(&sys);
sys.input.track_hover(layer, token, stamp);
sys.input.hover_contains(token)
}
}
pub trait UseDrag: private::Sealed {
fn use_drag<F>(&mut self, f: F)
where
F: FnMut(DragUpdate) + 'static;
}
struct UseDragImpl {
base: PointerHookBase,
active: Rc<Cell<Option<(MouseButton, Position)>>>,
}
impl AsPointerBase for UseDragImpl {
fn as_base(&self) -> &PointerHookBase {
&self.base
}
fn new_pointer_hook() -> Self {
Self {
base: PointerHookBase::new(),
active: Rc::new(Cell::new(None)),
}
}
}
impl Hook for UseDragImpl {
fn pre_component_draw(&mut self, drawer: &mut crate::ComponentDrawer) {
self.base.pre_draw(drawer.area);
}
}
impl UseDrag for Hooks<'_, '_> {
fn use_drag<F>(&mut self, mut f: F)
where
F: FnMut(DragUpdate) + 'static,
{
let hook = self.use_hook(UseDragImpl::new_pointer_hook);
let (stamp, token) = hook.as_base().identity();
let active = hook.active.clone();
let layer = current_layer_id(self);
let mut sys = self.use_context_mut::<SystemContext>();
debug_assert_mouse_enabled(&sys);
sys.input.register_handler(
Some(layer),
EventPriority::Normal,
EventOptions { hit_test: true },
stamp,
Some(token),
Box::new(move |event| {
let Event::Mouse(m) = event else {
return EventResult::Ignored;
};
let to = Position::new(m.column, m.row);
match m.kind {
MouseEventKind::Down(button) if button == MouseButton::Left => {
active.set(Some((button, to)));
f(DragUpdate {
phase: DragPhase::Start,
button,
from: to,
to,
modifiers: m.modifiers,
});
EventResult::Consumed
}
MouseEventKind::Drag(MouseButton::Left) => {
if let Some((b, last)) = active.get() {
active.set(Some((b, to)));
f(DragUpdate {
phase: DragPhase::Move,
button: b,
from: last,
to,
modifiers: m.modifiers,
});
EventResult::Consumed
} else {
EventResult::Ignored
}
}
MouseEventKind::Up(MouseButton::Left) => {
if let Some((b, last)) = active.take() {
f(DragUpdate {
phase: DragPhase::End,
button: b,
from: last,
to,
modifiers: m.modifiers,
});
EventResult::Consumed
} else {
EventResult::Ignored
}
}
_ => EventResult::Ignored,
}
}),
);
}
}
pub trait UsePointer: private::Sealed {
fn use_pointer<F>(&mut self, options: PointerOptions, f: F)
where
F: FnMut(PointerEvent) -> EventResult + 'static;
}
struct UsePointerImpl {
base: PointerHookBase,
synth: Rc<RefCell<ClickSynthesizer>>,
last_inside: Rc<Cell<bool>>,
}
impl AsPointerBase for UsePointerImpl {
fn as_base(&self) -> &PointerHookBase {
&self.base
}
fn new_pointer_hook() -> Self {
Self {
base: PointerHookBase::new(),
synth: Rc::new(RefCell::new(ClickSynthesizer::new())),
last_inside: Rc::new(Cell::new(false)),
}
}
}
impl Hook for UsePointerImpl {
fn pre_component_draw(&mut self, drawer: &mut crate::ComponentDrawer) {
self.base.pre_draw(drawer.area);
}
}
impl UsePointer for Hooks<'_, '_> {
fn use_pointer<F>(&mut self, options: PointerOptions, f: F)
where
F: FnMut(PointerEvent) -> EventResult + 'static,
{
let f = Rc::new(RefCell::new(f));
let hook = self.use_hook(UsePointerImpl::new_pointer_hook);
let (stamp, token) = hook.as_base().identity();
let synth = hook.synth.clone();
let last_inside = hook.last_inside.clone();
let layer = current_layer_id(self);
let buttons = options.buttons;
{
let hit_area = stamp.clone();
let f = f.clone();
let mut sys = self.use_context_mut::<SystemContext>();
debug_assert_mouse_enabled(&sys);
sys.input.register_handler(
Some(layer),
EventPriority::Normal,
EventOptions { hit_test: true },
stamp.clone(),
Some(token),
Box::new(move |event| {
let Event::Mouse(m) = event else {
return EventResult::Ignored;
};
let area = hit_area.get().area;
let ev = |kind| PointerEvent {
kind,
position: Position::new(m.column, m.row),
modifiers: m.modifiers,
area,
};
match m.kind {
MouseEventKind::Down(button) if buttons.contains(buttons_of(button)) => {
synth.borrow_mut().on_down(button, m.column, m.row);
(f.borrow_mut())(ev(PointerKind::Down(button)))
}
MouseEventKind::Up(button) if buttons.contains(buttons_of(button)) => {
let inside = rect_contains(area, m.column, m.row);
let mut result = (f.borrow_mut())(ev(PointerKind::Up(button)));
if let Some(count) = synth.borrow_mut().on_up(button, inside)
&& (f.borrow_mut())(ev(PointerKind::Click { button, count }))
== EventResult::Consumed
{
result = EventResult::Consumed;
}
result
}
MouseEventKind::Drag(button) if buttons.contains(buttons_of(button)) => {
(f.borrow_mut())(ev(PointerKind::Drag(button)))
}
MouseEventKind::ScrollUp => (f.borrow_mut())(ev(PointerKind::ScrollUp)),
MouseEventKind::ScrollDown => (f.borrow_mut())(ev(PointerKind::ScrollDown)),
MouseEventKind::ScrollLeft => (f.borrow_mut())(ev(PointerKind::ScrollLeft)),
MouseEventKind::ScrollRight => {
(f.borrow_mut())(ev(PointerKind::ScrollRight))
}
_ => EventResult::Ignored,
}
}),
);
}
if options.track_move {
let f = f.clone();
let mut sys = self.use_context_mut::<SystemContext>();
sys.input.request_motion();
sys.input.register_handler(
Some(layer),
EventPriority::Normal,
EventOptions { hit_test: false },
stamp.clone(),
None,
Box::new(move |event| {
let Event::Mouse(m) = event else {
return EventResult::Ignored;
};
if !matches!(m.kind, MouseEventKind::Moved) {
return EventResult::Ignored;
}
let area = stamp.get().area;
let ev = |kind| PointerEvent {
kind,
position: Position::new(m.column, m.row),
modifiers: m.modifiers,
area,
};
let inside = rect_contains(area, m.column, m.row);
match (last_inside.get(), inside) {
(false, true) => {
last_inside.set(true);
(f.borrow_mut())(ev(PointerKind::Enter))
}
(true, false) => {
last_inside.set(false);
(f.borrow_mut())(ev(PointerKind::Leave))
}
(true, true) => (f.borrow_mut())(ev(PointerKind::Move)),
(false, false) => EventResult::Ignored,
}
}),
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
AnyElement, ComponentDrawer, ElementRepr, Hooks,
components::{Text, View},
input::EventOptions,
render::tree::Tree,
terminal::UpdaterTerminal,
};
use crossterm::event::MouseEventKind;
use ratatui::layout::Direction;
use ratatui_kit_macros::{Props, component};
use crate::{
UseEventHandler,
input::EventScope,
mouse_test::{Mounted, TestLog, log_of},
};
#[derive(Clone, Default)]
struct SharedLog(TestLog);
#[derive(Default, Props)]
struct ClickAreaProps {
log: SharedLog,
}
#[allow(clippy::let_underscore_future)]
#[component]
fn ClickArea(props: &ClickAreaProps, mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let log = props.log.clone();
hooks.use_click(move |c| {
log.0
.borrow_mut()
.push(format!("click:{}:{:?}", c.count, c.button));
});
crate::element!(Text(text: "click"))
}
#[derive(Default, Props)]
struct DragAreaProps {
log: SharedLog,
}
#[component]
fn DragArea(props: &DragAreaProps, mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let log = props.log.clone();
hooks.use_drag(move |d| {
log.0
.borrow_mut()
.push(format!("{:?}:{}->{}", d.phase, d.from.x, d.to.x));
});
crate::element!(Text(text: "drag"))
}
#[derive(Default, Props)]
struct HoverAreaProps {
flag: Rc<Cell<bool>>,
}
#[component]
fn HoverArea(props: &HoverAreaProps, mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let hovered = hooks.use_hover();
props.flag.set(hovered);
crate::element!(Text(text: if hovered { "IN" } else { "out" }))
}
#[derive(Default, Props)]
struct PointerAreaProps {
log: SharedLog,
buttons: MouseButtons,
track_move: bool,
}
#[component]
fn PointerArea(props: &PointerAreaProps, mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let log = props.log.clone();
let buttons = props.buttons;
hooks.use_pointer(
PointerOptions {
buttons,
track_move: props.track_move,
},
move |ev| {
log.0.borrow_mut().push(format!("{:?}", ev.kind));
EventResult::Ignored
},
);
crate::element!(Text(text: "ptr"))
}
#[derive(Default, Props)]
struct SplitAppProps {
log: SharedLog,
}
#[component]
fn SplitApp(props: &SplitAppProps, hooks: Hooks) -> impl Into<AnyElement<'static>> {
let log = props.log.clone();
crate::element!(View(flex_direction: Direction::Horizontal) {
ClickArea(log: log)
PlainArea
})
}
#[derive(Default, Props)]
struct SplitHoverProps {
flag: Rc<Cell<bool>>,
}
#[component]
fn SplitHover(props: &SplitHoverProps, hooks: Hooks) -> impl Into<AnyElement<'static>> {
let flag = props.flag.clone();
crate::element!(View(flex_direction: Direction::Horizontal) {
HoverArea(flag: flag)
PlainArea
})
}
#[derive(Default, Props)]
struct SplitPointerProps {
log: SharedLog,
}
#[component]
fn SplitPointer(props: &SplitPointerProps, hooks: Hooks) -> impl Into<AnyElement<'static>> {
let log = props.log.clone();
crate::element!(View(flex_direction: Direction::Horizontal) {
PointerArea(log: log, buttons: MouseButtons::LEFT, track_move: true)
PlainArea
})
}
#[component]
fn PlainArea() -> impl Into<AnyElement<'static>> {
crate::element!(Text(text: "plain"))
}
#[test]
fn click_single_then_double() {
let log = SharedLog(TestLog::default());
let mut m = Mounted::mount(crate::element!(ClickArea(log: log.clone())), true);
m.dispatch(MouseEventKind::Down(MouseButton::Left), 2, 1);
m.dispatch(MouseEventKind::Up(MouseButton::Left), 2, 1);
m.dispatch(MouseEventKind::Down(MouseButton::Left), 2, 1);
m.dispatch(MouseEventKind::Up(MouseButton::Left), 2, 1);
assert_eq!(*log.0.borrow(), log_of(&["click:1:Left", "click:2:Left"]));
}
#[test]
fn click_cross_target_not_synthesized() {
let log = SharedLog(TestLog::default());
let mut m = Mounted::mount(crate::element!(SplitApp(log: log.clone())), true);
m.dispatch(MouseEventKind::Down(MouseButton::Left), 2, 1);
m.dispatch(MouseEventKind::Up(MouseButton::Left), 30, 1);
assert!(log.0.borrow().is_empty());
}
#[test]
fn click_consumes_and_does_not_bubble() {
let log = SharedLog(TestLog::default());
let mut m = Mounted::mount(crate::element!(ClickArea(log: log.clone())), true);
m.dispatch(MouseEventKind::Down(MouseButton::Left), 2, 1);
m.dispatch(MouseEventKind::Up(MouseButton::Left), 2, 1);
assert_eq!(*log.0.borrow(), log_of(&["click:1:Left"]));
}
#[derive(Default, Props)]
struct SplitDragProps {
log: SharedLog,
}
#[component]
fn SplitDrag(props: &SplitDragProps, hooks: Hooks) -> impl Into<AnyElement<'static>> {
let log = props.log.clone();
crate::element!(View(flex_direction: Direction::Horizontal) {
DragArea(log: log)
PlainArea
})
}
#[test]
fn drag_three_phases() {
let log = SharedLog(TestLog::default());
let mut m = Mounted::mount(crate::element!(SplitDrag(log: log.clone())), true);
m.dispatch(MouseEventKind::Down(MouseButton::Left), 1, 1);
m.dispatch(MouseEventKind::Drag(MouseButton::Left), 5, 5);
m.dispatch(MouseEventKind::Drag(MouseButton::Left), 30, 5); m.dispatch(MouseEventKind::Up(MouseButton::Left), 39, 9); assert_eq!(
*log.0.borrow(),
log_of(&["Start:1->1", "Move:1->5", "Move:5->30", "End:30->39"])
);
}
#[test]
fn hover_enter_leave_state() {
let flag = Rc::new(Cell::new(false));
let mut m = Mounted::mount(crate::element!(SplitHover(flag: flag.clone())), true);
assert!(!flag.get());
m.dispatch(MouseEventKind::Moved, 2, 2); assert!(flag.get());
m.dispatch(MouseEventKind::Moved, 39, 9); assert!(!flag.get());
}
#[test]
fn pointer_kinds_mapping() {
let log = SharedLog(TestLog::default());
let mut m = Mounted::mount(
crate::element!(PointerArea(
log: log.clone(),
buttons: MouseButtons::ALL,
track_move: false,
)),
true,
);
m.dispatch(MouseEventKind::Down(MouseButton::Left), 2, 1);
m.dispatch(MouseEventKind::Up(MouseButton::Left), 2, 1);
m.dispatch(MouseEventKind::ScrollDown, 2, 1);
assert_eq!(
*log.0.borrow(),
log_of(&[
"Down(Left)",
"Up(Left)",
"Click { button: Left, count: 1 }",
"ScrollDown"
])
);
}
#[test]
fn pointer_default_options_receive_left_click_only() {
let log = SharedLog(TestLog::default());
let mut m = Mounted::mount(crate::element!(PointerArea(log: log.clone())), true);
m.dispatch(MouseEventKind::Down(MouseButton::Right), 2, 1);
m.dispatch(MouseEventKind::Up(MouseButton::Right), 2, 1);
m.dispatch(MouseEventKind::Down(MouseButton::Left), 2, 1);
m.dispatch(MouseEventKind::Up(MouseButton::Left), 2, 1);
assert_eq!(
*log.0.borrow(),
log_of(&["Down(Left)", "Up(Left)", "Click { button: Left, count: 1 }"])
);
}
#[test]
fn pointer_buttons_filter() {
let log = SharedLog(TestLog::default());
let mut m = Mounted::mount(
crate::element!(PointerArea(
log: log.clone(),
buttons: MouseButtons::RIGHT,
track_move: false,
)),
true,
);
m.dispatch(MouseEventKind::Down(MouseButton::Left), 2, 1);
m.dispatch(MouseEventKind::Up(MouseButton::Left), 2, 1);
assert!(log.0.borrow().is_empty());
m.dispatch(MouseEventKind::Down(MouseButton::Right), 2, 1);
m.dispatch(MouseEventKind::Up(MouseButton::Right), 2, 1);
assert_eq!(
*log.0.borrow(),
log_of(&[
"Down(Right)",
"Up(Right)",
"Click { button: Right, count: 1 }"
])
);
}
#[test]
fn pointer_move_enter_leave() {
let log = SharedLog(TestLog::default());
let mut m = Mounted::mount(crate::element!(SplitPointer(log: log.clone())), true);
m.dispatch(MouseEventKind::Moved, 2, 2);
m.dispatch(MouseEventKind::Moved, 3, 3);
m.dispatch(MouseEventKind::Moved, 39, 9);
m.dispatch(MouseEventKind::Moved, 38, 9);
assert_eq!(*log.0.borrow(), log_of(&["Enter", "Move", "Leave"]));
}
#[derive(Default, Props)]
struct BubbleAppProps {
log: SharedLog,
}
#[component]
fn BubbleApp(props: &BubbleAppProps, mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let outer_log = props.log.clone();
hooks.use_event_handler_with_options(
EventScope::Current,
EventPriority::Normal,
EventOptions { hit_test: false },
move |event| {
if let Event::Mouse(m) = event {
outer_log.0.borrow_mut().push(format!("outer:{:?}", m.kind));
return EventResult::Consumed;
}
EventResult::Ignored
},
);
crate::element!(PointerArea(
log: props.log.clone(),
buttons: MouseButtons::ALL,
track_move: false,
))
}
#[test]
fn pointer_ignored_bubbles_to_outer_handler() {
let log = SharedLog(TestLog::default());
let mut m = Mounted::mount(crate::element!(BubbleApp(log: log.clone())), true);
m.dispatch(MouseEventKind::ScrollDown, 2, 1);
assert_eq!(*log.0.borrow(), log_of(&["ScrollDown", "outer:ScrollDown"]));
}
#[test]
fn button_press_consumes_but_scroll_bubbles() {
use crate::components::Button;
use std::sync::{Arc, Mutex};
type SyncLog = Arc<Mutex<Vec<String>>>;
#[derive(Clone, Default, Props)]
struct OuterButtonProps {
raw: SyncLog,
}
#[component]
fn OuterButton(
props: &OuterButtonProps,
mut hooks: Hooks,
) -> impl Into<AnyElement<'static>> {
let raw = props.raw.clone();
hooks.use_event_handler_with_options(
EventScope::Current,
EventPriority::Normal,
EventOptions { hit_test: false },
move |event| {
if let Event::Mouse(m) = event {
raw.lock().unwrap().push(format!("{:?}", m.kind));
return EventResult::Consumed;
}
EventResult::Ignored
},
);
crate::element!(Button(label: "OK".to_string(), on_press: |()| {}))
}
let raw: SyncLog = Default::default();
let mut m = Mounted::mount(crate::element!(OuterButton(raw: raw.clone())), true);
m.dispatch(MouseEventKind::Down(MouseButton::Left), 2, 1);
assert!(raw.lock().unwrap().is_empty());
m.dispatch(MouseEventKind::ScrollDown, 2, 1);
assert_eq!(*raw.lock().unwrap(), vec!["ScrollDown".to_string()]);
}
#[test]
#[should_panic(expected = "mouse capture is not enabled")]
fn pointer_hook_without_mouse_panics_in_debug() {
let log = SharedLog(TestLog::default());
let _ = Mounted::mount(crate::element!(ClickArea(log: log)), false);
}
}