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());
}
fn scroller_above_text_dom() -> (TuiDom, NodeId, NodeId) {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let c = dom.create_element("c");
dom.append_child(root, c).unwrap();
let p = dom.create_element("p");
let t = dom.create_text_node("hello world");
dom.append_child(p, t).unwrap();
dom.append_child(root, p).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, p)
}
#[test]
fn press_on_text_after_lost_thumb_mouseup_does_not_drag_the_thumb() {
let (mut dom, c, p) = scroller_above_text_dom();
assert_eq!(dom.hit_test(1, 6), Some(p), "fixture: text at row 6");
let mut router = Router::new();
router.route(&mut dom, down(9, 0)); router.route(&mut dom, drag(9, 3));
assert_eq!(dom.node(c).ext().unwrap().scroll_y, 14);
router.route(&mut dom, down(1, 6));
router.route(&mut dom, drag(2, 7));
assert_eq!(
dom.node(c).ext().unwrap().scroll_y,
14,
"the thumb stays put"
);
assert!(
router.scrollbar_drag.is_none(),
"the press ended the thumb drag"
);
assert_eq!(
dom.pointer_capture(),
None,
"the press released the capture the thumb drag took"
);
}
#[test]
fn press_after_lost_mouseup_keeps_an_author_pointer_capture() {
let (mut dom, _c, p) = scroller_above_text_dom();
let mut router = Router::new();
dom.set_pointer_capture(p).unwrap();
router.route(&mut dom, down(1, 6));
assert_eq!(dom.pointer_capture(), Some(p));
}
fn horizontal_scroller_dom() -> (TuiDom, NodeId) {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let c = dom.create_element("c");
let w = dom.create_element("w");
dom.append_child(c, w).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(3))
.overflow_x(Overflow::Auto),
)
.rule_unchecked(
"w",
TuiStyle::new()
.width(Size::Fixed(30))
.height(Size::Fixed(1)),
);
dom.cascade(&sheet);
dom.layout_dom(Rect::new(0, 0, 20, 5));
(dom, c)
}
#[test]
fn autoscroll_step_has_a_horizontal_band() {
use crate::runtime::scrollbar::autoscroll_step_for;
let (mut dom, c) = horizontal_scroller_dom();
let (axis, step) = autoscroll_step_for(&dom, c, (9, 1)).expect("right band");
assert_eq!(axis, ScrollAxis::Horizontal);
assert!(step > 0, "{step}");
assert_eq!(autoscroll_step_for(&dom, c, (0, 1)), None);
dom.node_mut(c).ext_mut().unwrap().scroll_x = 5;
let (axis, step) = autoscroll_step_for(&dom, c, (0, 1)).expect("left band");
assert_eq!(axis, ScrollAxis::Horizontal);
assert!(step < 0, "{step}");
assert_eq!(autoscroll_step_for(&dom, c, (5, 1)), None);
}