use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
use std::time::{Duration, Instant};
use super::effect_event::use_effect_event;
use super::event::use_event;
use super::r#ref::use_ref;
use super::state::use_state;
pub fn use_mouse<F>(handler: F)
where
F: Fn(MouseEvent) + Send + Sync + 'static,
{
let stable_handler = use_effect_event(move |mouse_event: MouseEvent| {
handler(mouse_event);
});
if let Some(Event::Mouse(mouse_event)) = use_event() {
stable_handler.call(mouse_event);
}
}
pub fn use_mouse_click<F>(handler: F)
where
F: Fn(MouseButton, u16, u16) + Send + Sync + 'static,
{
use_mouse(move |mouse_event| {
if let MouseEventKind::Down(button) = mouse_event.kind {
handler(button, mouse_event.column, mouse_event.row);
}
});
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct DragInfo {
pub button: Option<MouseButton>,
pub start: (u16, u16),
pub current: (u16, u16),
pub is_dragging: bool,
pub is_start: bool,
pub is_end: bool,
}
pub fn use_mouse_drag() -> (DragInfo, impl Fn() + Clone) {
let (drag_info, set_drag_info) = use_state(DragInfo::default);
let drag_state = use_ref(|| None::<(MouseButton, u16, u16)>);
let set_info_clone = set_drag_info;
let state_clone = drag_state.clone();
use_mouse(move |mouse_event| {
match mouse_event.kind {
MouseEventKind::Down(button) => {
state_clone.set(Some((button, mouse_event.column, mouse_event.row)));
set_info_clone.set(DragInfo {
button: Some(button),
start: (mouse_event.column, mouse_event.row),
current: (mouse_event.column, mouse_event.row),
is_dragging: true,
is_start: true,
is_end: false,
});
}
MouseEventKind::Drag(button) => {
if let Some((drag_button, start_x, start_y)) = state_clone.get() {
if button == drag_button {
set_info_clone.set(DragInfo {
button: Some(button),
start: (start_x, start_y),
current: (mouse_event.column, mouse_event.row),
is_dragging: true,
is_start: false,
is_end: false,
});
}
}
}
MouseEventKind::Up(button) => {
if let Some((drag_button, start_x, start_y)) = state_clone.get() {
if button == drag_button {
set_info_clone.set(DragInfo {
button: Some(button),
start: (start_x, start_y),
current: (mouse_event.column, mouse_event.row),
is_dragging: false,
is_start: false,
is_end: true,
});
state_clone.set(None);
}
}
}
_ => {}
}
});
let reset = {
let set_info = set_drag_info;
let state = drag_state.clone();
move || {
set_info.set(DragInfo::default());
state.set(None);
}
};
(drag_info, reset)
}
pub fn use_double_click<F>(max_delay: Duration, handler: F)
where
F: Fn(MouseButton, u16, u16) + Send + Sync + 'static,
{
let last_click = use_ref(|| None::<(MouseButton, u16, u16, Instant)>);
use_mouse(move |mouse_event| {
if let MouseEventKind::Down(button) = mouse_event.kind {
let now = Instant::now();
let current_pos = (mouse_event.column, mouse_event.row);
if let Some((last_button, last_x, last_y, last_time)) = last_click.get() {
let time_diff = now.duration_since(last_time);
let same_button = button == last_button;
let same_position = current_pos == (last_x, last_y);
if same_button && same_position && time_diff <= max_delay {
handler(button, mouse_event.column, mouse_event.row);
last_click.set(None);
return;
}
}
last_click.set(Some((button, mouse_event.column, mouse_event.row, now)));
}
});
}
pub fn use_mouse_position() -> (u16, u16) {
let (position, set_position) = use_state(|| (0u16, 0u16));
use_mouse({
move |mouse_event| {
let new_pos = (mouse_event.column, mouse_event.row);
if new_pos != position {
set_position.set(new_pos);
}
}
});
position
}
pub fn use_mouse_hover(area: Rect) -> bool {
let (is_hovering, set_hovering) = use_state(|| false);
use_mouse({
move |mouse_event| {
let is_inside = mouse_event.column >= area.x
&& mouse_event.column < area.x + area.width
&& mouse_event.row >= area.y
&& mouse_event.row < area.y + area.height;
if is_inside != is_hovering {
set_hovering.set(is_inside);
}
}
});
is_hovering
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{clear_current_event, set_current_event};
use crate::fiber_tree::{FiberTree, clear_fiber_tree, set_fiber_tree, with_fiber_tree_mut};
use crossterm::event::KeyModifiers;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, Ordering};
static TEST_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
fn setup_test_fiber() -> crate::fiber::FiberId {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.begin_render(fiber_id);
set_fiber_tree(tree);
fiber_id
}
fn cleanup_test() {
with_fiber_tree_mut(|tree| {
tree.end_render();
});
clear_fiber_tree();
clear_current_event();
crate::scheduler::batch::clear_state_batch();
}
fn create_mouse_event(kind: MouseEventKind, column: u16, row: u16) -> Event {
Event::Mouse(MouseEvent {
kind,
column,
row,
modifiers: KeyModifiers::NONE,
})
}
#[test]
fn test_use_mouse_receives_mouse_event() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let _fiber_id = setup_test_fiber();
let call_count = Arc::new(AtomicI32::new(0));
let call_count_clone = call_count.clone();
let event = create_mouse_event(MouseEventKind::Down(MouseButton::Left), 10, 20);
set_current_event(Some(Arc::new(event)));
use_mouse(move |mouse_event| {
assert_eq!(mouse_event.column, 10);
assert_eq!(mouse_event.row, 20);
call_count_clone.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(call_count.load(Ordering::SeqCst), 1);
cleanup_test();
}
#[test]
fn test_use_mouse_ignores_non_mouse_events() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let _fiber_id = setup_test_fiber();
let call_count = Arc::new(AtomicI32::new(0));
let call_count_clone = call_count.clone();
let event = Event::Key(crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Char('a'),
KeyModifiers::NONE,
));
set_current_event(Some(Arc::new(event)));
use_mouse(move |_mouse_event| {
call_count_clone.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(call_count.load(Ordering::SeqCst), 0);
cleanup_test();
}
#[test]
fn test_use_mouse_click_only_handles_down() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let call_count = Arc::new(AtomicI32::new(0));
let down_event = create_mouse_event(MouseEventKind::Down(MouseButton::Left), 10, 20);
set_current_event(Some(Arc::new(down_event)));
let call_count_clone = call_count.clone();
use_mouse_click(move |button, x, y| {
assert_eq!(button, MouseButton::Left);
assert_eq!(x, 10);
assert_eq!(y, 20);
call_count_clone.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(call_count.load(Ordering::SeqCst), 1);
with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
clear_current_event();
let move_event = create_mouse_event(MouseEventKind::Moved, 15, 25);
set_current_event(Some(Arc::new(move_event)));
let call_count_clone2 = call_count.clone();
use_mouse_click(move |_, _, _| {
call_count_clone2.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(call_count.load(Ordering::SeqCst), 1);
cleanup_test();
}
#[test]
fn test_use_mouse_no_event() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let _fiber_id = setup_test_fiber();
let call_count = Arc::new(AtomicI32::new(0));
let call_count_clone = call_count.clone();
clear_current_event();
use_mouse(move |_| {
call_count_clone.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(call_count.load(Ordering::SeqCst), 0);
cleanup_test();
}
#[test]
fn test_use_mouse_hover_inside_area() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let event = create_mouse_event(MouseEventKind::Moved, 15, 7);
set_current_event(Some(Arc::new(event)));
let area = Rect::new(10, 5, 20, 10);
let is_hovering = use_mouse_hover(area);
assert!(!is_hovering);
with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
tree.begin_render(fiber_id);
});
clear_current_event();
let is_hovering = use_mouse_hover(area);
assert!(is_hovering);
cleanup_test();
}
#[test]
fn test_use_mouse_hover_outside_area() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let _fiber_id = setup_test_fiber();
let event = create_mouse_event(MouseEventKind::Moved, 5, 3);
set_current_event(Some(Arc::new(event)));
let area = Rect::new(10, 5, 20, 10);
let is_hovering = use_mouse_hover(area);
assert!(!is_hovering);
cleanup_test();
}
#[test]
fn test_use_mouse_position() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let event = create_mouse_event(MouseEventKind::Moved, 42, 24);
set_current_event(Some(Arc::new(event)));
let (x, y) = use_mouse_position();
assert_eq!(x, 0);
assert_eq!(y, 0);
with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
tree.begin_render(fiber_id);
});
clear_current_event();
let (x, y) = use_mouse_position();
assert_eq!(x, 42);
assert_eq!(y, 24);
cleanup_test();
}
#[test]
fn test_use_mouse_position_default() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let _fiber_id = setup_test_fiber();
clear_current_event();
let (x, y) = use_mouse_position();
assert_eq!(x, 0);
assert_eq!(y, 0);
cleanup_test();
}
#[test]
fn test_drag_info_default() {
let info = DragInfo::default();
assert_eq!(info.button, None);
assert_eq!(info.start, (0, 0));
assert_eq!(info.current, (0, 0));
assert!(!info.is_dragging);
assert!(!info.is_start);
assert!(!info.is_end);
}
}