use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
use crate::layout::Overflow;
use crate::node::TuiNodeExt;
use crate::runtime::hit_test::HitTestExt;
use crate::style::ComputedStyle;
use crate::{TuiDispatchExt, TuiDom, TuiEvent};
use super::{RouteOutcome, Router};
fn dispatch(router: &mut Router, dom: &mut TuiDom, target: crate::NodeId, ev: &mut TuiEvent) {
let _ = dom.dispatch_tui_event(target, ev);
router.pending_redraw |= ev.event.redraw_requested();
}
pub(super) fn route_mouse(
router: &mut Router,
dom: &mut TuiDom,
mouse: MouseEvent,
) -> RouteOutcome {
match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => handle_down(router, dom, mouse),
MouseEventKind::Up(MouseButton::Left) => handle_up(router, dom, mouse),
MouseEventKind::Down(MouseButton::Right) => handle_right_down(router, dom, mouse),
MouseEventKind::Up(MouseButton::Right) => handle_nonleft_up(router, dom, mouse),
MouseEventKind::Down(MouseButton::Middle) => handle_nonleft_down(router, dom, mouse),
MouseEventKind::Up(MouseButton::Middle) => handle_nonleft_up(router, dom, mouse),
MouseEventKind::Moved | MouseEventKind::Drag(MouseButton::Left) => {
handle_move(router, dom, mouse)
}
MouseEventKind::ScrollUp
| MouseEventKind::ScrollDown
| MouseEventKind::ScrollLeft
| MouseEventKind::ScrollRight => handle_wheel(router, dom, mouse),
MouseEventKind::Drag(_) => handle_move(router, dom, mouse),
}
}
fn handle_right_down(router: &mut Router, dom: &mut TuiDom, mouse: MouseEvent) -> RouteOutcome {
let Some(target) = dom.hit_test(mouse.column, mouse.row) else {
return RouteOutcome::default();
};
let mut tui_down = TuiEvent::mousedown(mouse);
dispatch(router, dom, target, &mut tui_down);
let mut tui_ctx = TuiEvent::contextmenu(mouse);
dispatch(router, dom, target, &mut tui_ctx);
RouteOutcome::default()
}
fn handle_nonleft_down(router: &mut Router, dom: &mut TuiDom, mouse: MouseEvent) -> RouteOutcome {
let Some(target) = dom.hit_test(mouse.column, mouse.row) else {
return RouteOutcome::default();
};
let mut tui = TuiEvent::mousedown(mouse);
dispatch(router, dom, target, &mut tui);
RouteOutcome::default()
}
fn handle_nonleft_up(router: &mut Router, dom: &mut TuiDom, mouse: MouseEvent) -> RouteOutcome {
let Some(target) = dom.hit_test(mouse.column, mouse.row) else {
return RouteOutcome::default();
};
let mut tui = TuiEvent::mouseup(mouse);
dispatch(router, dom, target, &mut tui);
RouteOutcome::default()
}
fn handle_down(router: &mut Router, dom: &mut TuiDom, mouse: MouseEvent) -> RouteOutcome {
router.register_click(&mouse);
let hit = dom.hit_test(mouse.column, mouse.row);
router.down_target = hit;
crate::rdom_trace!(
"handle_down: hit_test({}, {}) = {:?}; pre-capture={:?}",
mouse.column,
mouse.row,
hit,
dom.pointer_capture()
);
let Some(target) = hit else {
return RouteOutcome::default();
};
let mut tui = TuiEvent::mousedown(mouse);
dispatch(router, dom, target, &mut tui);
let mut redraw = false;
if !tui.event.default_prevented() {
let path = dom.hit_test_path(mouse.column, mouse.row);
if let Some(sb_hit) = crate::runtime::scrollbar::hit(dom, &path, mouse.column, mouse.row)
&& crate::runtime::scrollbar::handle_mousedown(router, dom, sb_hit)
{
return RouteOutcome {
redraw_requested: true,
quit_requested: false,
};
}
if let Some(focusable) = crate::runtime::focus::nearest_focusable_ancestor(dom, target) {
let prev = dom.focused();
crate::runtime::focus::focus_node(dom, Some(focusable));
if prev != Some(focusable) {
redraw = true;
}
}
let drag_started = crate::runtime::selection::drag::begin(router, dom, mouse);
crate::rdom_trace!(
"handle_down: selection::drag::begin returned {drag_started}; \
post-capture={:?} selection_drag={:?}",
dom.pointer_capture(),
router.selection_drag.is_some()
);
if drag_started {
redraw = true;
let count = router.last_click.map(|c| c.count).unwrap_or(1);
let changed = match count {
2 => crate::runtime::selection::multiclick::expand_to_word(dom),
3 => crate::runtime::selection::multiclick::expand_to_line(dom),
_ => false,
};
if changed {
redraw = true;
}
}
}
RouteOutcome {
redraw_requested: redraw,
quit_requested: false,
}
}
fn handle_up(router: &mut Router, dom: &mut TuiDom, mouse: MouseEvent) -> RouteOutcome {
let captured = dom.pointer_capture();
let hit = dom.hit_test(mouse.column, mouse.row);
let down_target = router.down_target.take();
crate::rdom_trace!(
"handle_up: hit={hit:?} down_target={down_target:?} captured={captured:?} \
selection_drag={} scrollbar_drag={}",
router.selection_drag.is_some(),
router.scrollbar_drag.is_some()
);
let up_target = captured.or(hit);
if let Some(target) = up_target {
let mut tui_up = TuiEvent::mouseup(mouse);
dispatch(router, dom, target, &mut tui_up);
}
let click_target = if let Some(cap) = captured {
Some(cap)
} else if let (Some(down), Some(up)) = (down_target, hit) {
dom.common_ancestor(down, up)
} else {
None
};
if let Some(target) = click_target {
let mut tui_click = TuiEvent::click(mouse);
tui_click.event = tui_click.event.clone().with_synthetic(true);
dispatch(router, dom, target, &mut tui_click);
let count = router.last_click.map(|c| c.count).unwrap_or(0);
if count == 2 {
let mut tui_dbl = TuiEvent::dblclick(mouse);
tui_dbl.event = tui_dbl.event.clone().with_synthetic(true);
dispatch(router, dom, target, &mut tui_dbl);
}
}
if captured.is_some() {
crate::rdom_trace!("handle_up: auto-releasing pointer_capture (was {captured:?})");
dom.release_pointer_capture();
}
crate::runtime::selection::drag::end(router);
crate::runtime::scrollbar::end_drag(router);
crate::rdom_trace!(
"handle_up: end of fn — capture={:?} hovered={:?}",
dom.pointer_capture(),
dom.hovered()
);
RouteOutcome::default()
}
fn handle_move(router: &mut Router, dom: &mut TuiDom, mouse: MouseEvent) -> RouteOutcome {
if dom.pointer_capture().is_some() && matches!(mouse.kind, MouseEventKind::Moved) {
crate::rdom_trace!(
"handle_move: stale capture detected (Moved with capture={:?}) — releasing",
dom.pointer_capture()
);
dom.release_pointer_capture();
crate::runtime::selection::drag::end(router);
crate::runtime::scrollbar::end_drag(router);
}
if let Some(captured) = dom.pointer_capture() {
crate::rdom_trace!("handle_move: capture branch — routing to {captured:?}, hover skipped");
let mut tui = TuiEvent::mousemove(mouse);
dispatch(router, dom, captured, &mut tui);
let mut redraw = false;
if let Some(anchor_flow) = router.selection_drag
&& crate::runtime::selection::drag::extend(dom, mouse, anchor_flow)
{
redraw = true;
}
if router.scrollbar_drag.is_some()
&& crate::runtime::scrollbar::extend_drag(router, dom, mouse.column, mouse.row)
{
redraw = true;
}
return RouteOutcome {
redraw_requested: redraw,
quit_requested: false,
};
}
let hit = dom.hit_test(mouse.column, mouse.row);
if crate::runtime::trace::enabled() {
let info = hit
.map(|id| {
let n = dom.node(id);
let tag = n.tag_name().unwrap_or("?");
let cls = n.get_attribute("class").unwrap_or("");
format!("{id:?} <{tag} class=\"{cls}\">")
})
.unwrap_or_else(|| "None".to_string());
crate::rdom_trace!(
"handle_move: hit_test({}, {}) = {info} (prev hover_target={:?})",
mouse.column,
mouse.row,
router.hover_target
);
}
if let Some(target) = hit {
let mut tui = TuiEvent::mousemove(mouse);
dispatch(router, dom, target, &mut tui);
}
let changed = hit != router.hover_target;
if !changed {
crate::rdom_trace!(
"handle_move: no hover change ({:?} == hover_target); returning no-op",
hit
);
return RouteOutcome::default();
}
let prev = router.hover_target;
router.hover_target = hit;
crate::rdom_trace!(
"handle_move: hover transition {prev:?} -> {hit:?}; calling set_hovered + redraw"
);
if let Some(old) = prev {
let mut tui_out = TuiEvent::mouseout(mouse);
tui_out.event = tui_out.event.clone().with_synthetic(true);
dispatch(router, dom, old, &mut tui_out);
}
if let Some(new) = hit {
let mut tui_over = TuiEvent::mouseover(mouse);
tui_over.event = tui_over.event.clone().with_synthetic(true);
dispatch(router, dom, new, &mut tui_over);
}
dom.set_hovered(hit);
RouteOutcome {
redraw_requested: true,
quit_requested: false,
}
}
fn handle_wheel(router: &mut Router, dom: &mut TuiDom, mouse: MouseEvent) -> RouteOutcome {
let Some(target) = dom.hit_test(mouse.column, mouse.row) else {
return RouteOutcome::default();
};
let mut tui = TuiEvent::wheel(mouse);
dispatch(router, dom, target, &mut tui);
if tui.event.default_prevented() {
return RouteOutcome::default();
}
let (dx, dy): (i32, i32) = match mouse.kind {
MouseEventKind::ScrollUp => (0, -1),
MouseEventKind::ScrollDown => (0, 1),
MouseEventKind::ScrollLeft => (-1, 0),
MouseEventKind::ScrollRight => (1, 0),
_ => return RouteOutcome::default(),
};
let wants_y = dy != 0;
let wants_x = dx != 0;
let mut cur = Some(target);
while let Some(id) = cur {
let computed = dom
.node(id)
.computed()
.cloned()
.unwrap_or_else(ComputedStyle::initial);
let y_scrollable = matches!(computed.overflow_y, Overflow::Scroll | Overflow::Auto);
let x_scrollable = matches!(computed.overflow_x, Overflow::Scroll | Overflow::Auto);
if (wants_y && y_scrollable) || (wants_x && x_scrollable) {
let border = dom
.node(id)
.computed()
.map(|c| c.border)
.unwrap_or_default();
let (old_x, old_y, new_x, new_y) = if let Some(ext) = dom.node_mut(id).ext_mut() {
let pb = rdom_style::layout::compute_padding_box(ext.layout, border);
let old_x = ext.scroll_x;
let old_y = ext.scroll_y;
if wants_y && y_scrollable {
let max_y = ext.scroll_content_height.saturating_sub(pb.height as usize);
apply_scroll(&mut ext.scroll_y, dy, max_y);
}
if wants_x && x_scrollable {
let max_x = ext.scroll_content_width.saturating_sub(pb.width as usize);
apply_scroll(&mut ext.scroll_x, dx, max_x);
}
(old_x, old_y, ext.scroll_x, ext.scroll_y)
} else {
(0, 0, 0, 0)
};
if old_x != new_x || old_y != new_y {
let mut tui_scroll = TuiEvent::new("scroll");
tui_scroll.event.cancelable = false;
dispatch(router, dom, id, &mut tui_scroll);
return RouteOutcome {
redraw_requested: true,
quit_requested: false,
};
}
return RouteOutcome::default();
}
cur = dom.node(id).parent_node().map(|p| p.id());
}
RouteOutcome::default()
}
fn apply_scroll(offset: &mut usize, delta: i32, max: usize) {
if delta > 0 {
*offset = offset.saturating_add(delta as usize).min(max);
} else if delta < 0 {
*offset = offset.saturating_sub((-delta) as usize);
}
}
#[cfg(test)]
mod tests;