use crossterm::event::{KeyModifiers, MouseButton, MouseEvent as CtMouseEvent, MouseEventKind};
use rdom_core::NodeId;
use crate::TuiDom;
use crate::layout::{Overflow, Size};
use crate::render::{LayoutExt, Rect};
use crate::runtime::hit_test::HitTestExt;
use crate::runtime::router::Router;
use crate::runtime::scrollbar::{ScrollAxis, ScrollbarPart, hit};
use crate::style::{CascadeExt, Stylesheet, TuiStyle};
fn vertical_scrollbar_dom() -> (TuiDom, NodeId) {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let c = dom.create_element("c");
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(6))
.overflow_y(Overflow::Scroll),
);
dom.cascade(&sheet);
dom.layout_dom(Rect::new(0, 0, 12, 8));
if let Some(ext) = dom.node_mut(c).ext_mut() {
ext.scroll_content_height = 30;
}
(dom, c)
}
fn mouse_at(kind: MouseEventKind, x: u16, y: u16) -> CtMouseEvent {
CtMouseEvent {
kind,
column: x,
row: y,
modifiers: KeyModifiers::empty(),
}
}
fn down(x: u16, y: u16) -> crossterm::event::Event {
crossterm::event::Event::Mouse(mouse_at(MouseEventKind::Down(MouseButton::Left), x, y))
}
fn up(x: u16, y: u16) -> crossterm::event::Event {
crossterm::event::Event::Mouse(mouse_at(MouseEventKind::Up(MouseButton::Left), x, y))
}
fn drag(x: u16, y: u16) -> crossterm::event::Event {
crossterm::event::Event::Mouse(mouse_at(MouseEventKind::Drag(MouseButton::Left), x, y))
}
#[test]
fn hit_detects_vertical_thumb() {
let (dom, c) = vertical_scrollbar_dom();
let path = dom.hit_test_path(9, 0);
let result = hit(&dom, &path, 9, 0).expect("should hit");
assert_eq!(result.element, c);
assert_eq!(result.axis, ScrollAxis::Vertical);
assert_eq!(result.part, ScrollbarPart::Thumb);
}
#[test]
fn hit_detects_track_after_thumb() {
let (dom, c) = vertical_scrollbar_dom();
let path = dom.hit_test_path(9, 3);
let result = hit(&dom, &path, 9, 3).expect("should hit");
assert_eq!(result.element, c);
assert_eq!(result.part, ScrollbarPart::TrackAfter);
}
#[test]
fn hit_returns_none_off_scrollbar() {
let (dom, _) = vertical_scrollbar_dom();
let path = dom.hit_test_path(3, 3);
assert!(hit(&dom, &path, 3, 3).is_none());
}
#[test]
fn hit_respects_auto_visibility() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let c = dom.create_element("c");
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(6))
.overflow_y(Overflow::Auto),
);
dom.cascade(&sheet);
dom.layout_dom(Rect::new(0, 0, 12, 8));
if let Some(ext) = dom.node_mut(c).ext_mut() {
ext.scroll_content_height = 3; }
let path = dom.hit_test_path(9, 2);
assert!(hit(&dom, &path, 9, 2).is_none());
}
#[test]
fn track_click_after_thumb_pages_scroll_forward() {
let (mut dom, c) = vertical_scrollbar_dom();
let mut router = Router::new();
router.route(&mut dom, down(9, 4));
let scroll = dom.node(c).ext().unwrap().scroll_y;
assert_eq!(scroll, 6);
}
#[test]
fn track_click_before_thumb_pages_scroll_back() {
let (mut dom, c) = vertical_scrollbar_dom();
if let Some(ext) = dom.node_mut(c).ext_mut() {
ext.scroll_y = 15;
}
let mut router = Router::new();
router.route(&mut dom, down(9, 1));
let scroll = dom.node(c).ext().unwrap().scroll_y;
assert_eq!(scroll, 9);
}
#[test]
fn track_click_clamps_at_zero() {
let (mut dom, c) = vertical_scrollbar_dom();
let mut router = Router::new();
router.route(&mut dom, down(9, 1));
let scroll = dom.node(c).ext().unwrap().scroll_y;
assert_eq!(scroll, 6);
}
#[test]
fn track_click_clamps_at_bottom() {
let (mut dom, c) = vertical_scrollbar_dom();
if let Some(ext) = dom.node_mut(c).ext_mut() {
ext.scroll_y = 22; }
let mut router = Router::new();
router.route(&mut dom, down(9, 5));
let scroll = dom.node(c).ext().unwrap().scroll_y;
assert_eq!(scroll, 24);
}
#[test]
fn thumb_mousedown_engages_pointer_capture_and_records_drag() {
let (mut dom, c) = vertical_scrollbar_dom();
let mut router = Router::new();
router.route(&mut dom, down(9, 0)); assert_eq!(dom.pointer_capture(), Some(c));
assert!(router.scrollbar_drag.is_some());
}
#[test]
fn dragging_thumb_updates_scroll_proportionally() {
let (mut dom, c) = vertical_scrollbar_dom();
let mut router = Router::new();
router.route(&mut dom, down(9, 0)); router.route(&mut dom, drag(9, 3));
let scroll = dom.node(c).ext().unwrap().scroll_y;
assert_eq!(scroll, 14);
}
#[test]
fn drag_past_track_end_clamps_at_max_scroll() {
let (mut dom, c) = vertical_scrollbar_dom();
let mut router = Router::new();
router.route(&mut dom, down(9, 0));
router.route(&mut dom, drag(9, 100)); let scroll = dom.node(c).ext().unwrap().scroll_y;
assert_eq!(scroll, 24); }
#[test]
fn drag_past_track_start_clamps_at_zero() {
let (mut dom, c) = vertical_scrollbar_dom();
if let Some(ext) = dom.node_mut(c).ext_mut() {
ext.scroll_y = 24; }
let mut router = Router::new();
router.route(&mut dom, down(9, 5));
router.route(&mut dom, drag(9, 0));
let scroll = dom.node(c).ext().unwrap().scroll_y;
assert_eq!(scroll, 0);
}
#[test]
fn mouseup_clears_drag_state_and_releases_capture() {
let (mut dom, _c) = vertical_scrollbar_dom();
let mut router = Router::new();
router.route(&mut dom, down(9, 0));
router.route(&mut dom, up(9, 3));
assert!(router.scrollbar_drag.is_none());
assert!(dom.pointer_capture().is_none());
}
#[test]
fn reset_clears_drag_state() {
let (mut dom, _c) = vertical_scrollbar_dom();
let mut router = Router::new();
router.route(&mut dom, down(9, 0));
router.reset();
assert!(router.scrollbar_drag.is_none());
}