use super::{
classify_mouse_event, classify_mouse_events, copy_mode_mouse_context,
copy_mode_mouse_context_with_line_numbers, layout_for_session, MouseDragHandler,
MouseForwardEvent, MouseLayout, MouseLocation, PaneBorderStatus, PaneMouseTarget,
PaneScrollbar, PaneScrollbarsMode, ScrollbarPosition, StatusLineLayout, StatusRange,
StatusRangeType,
};
use crate::copy_mode::CopyModeLineNumberLayout;
use crate::pane_terminals::HandlerState;
use rmux_core::{key_string_lookup_string, PaneGeometry, PaneId};
use rmux_proto::{
OptionName, PaneTarget, ScopeSelector, SessionName, SetOptionMode, TerminalSize, WindowTarget,
};
use std::time::Instant;
fn pane_target(index: u32) -> PaneTarget {
PaneTarget::with_window(SessionName::new("alpha").expect("valid session"), 0, index)
}
fn layout() -> MouseLayout {
MouseLayout {
session_id: 1,
status_at: Some(0),
status_lines: 1,
status: Some(StatusLineLayout {
ranges: vec![
StatusRange {
line: 0,
x: 0..=3,
kind: StatusRangeType::Left,
},
StatusRange {
line: 0,
x: 4..=7,
kind: StatusRangeType::Right,
},
StatusRange {
line: 0,
x: 8..=11,
kind: StatusRangeType::Window(9),
},
StatusRange {
line: 0,
x: 12..=15,
kind: StatusRangeType::Session(4),
},
StatusRange {
line: 0,
x: 16..=19,
kind: StatusRangeType::User,
},
StatusRange {
line: 0,
x: 20..=23,
kind: StatusRangeType::Control(3),
},
],
}),
pane_border_status: PaneBorderStatus::Off,
focus_follows_mouse: true,
active_pane: Some(PaneId::new(0)),
panes: vec![
PaneMouseTarget {
pane_id: PaneId::new(0),
pane_target: Some(pane_target(0)),
window_id: 5,
geometry: PaneGeometry::new(0, 0, 40, 10),
scrollbar: Some(
PaneScrollbar::from_view(
10,
30,
false,
PaneScrollbarsMode::On,
ScrollbarPosition::Right,
1,
0,
None,
)
.expect("scrollbar"),
),
border_controls: vec![super::BorderControlRange {
x: 41..=41,
y: 4,
control: 2,
}],
},
PaneMouseTarget {
pane_id: PaneId::new(1),
pane_target: Some(pane_target(1)),
window_id: 5,
geometry: PaneGeometry::new(41, 0, 39, 10),
scrollbar: None,
border_controls: Vec::new(),
},
],
}
}
#[test]
fn status_line_count_accepts_numeric_status_values() {
assert_eq!(super::status_line_count(Some("on"), 10), 1);
assert_eq!(super::status_line_count(Some("2"), 10), 2);
assert_eq!(super::status_line_count(Some("5"), 3), 3);
}
#[test]
fn generated_mouse_scrollbar_uses_visible_pane_border_geometry() {
let session_name = SessionName::new("mouse-scrollbar").expect("valid session");
let mut state = HandlerState::default();
state
.sessions
.create_session(session_name.clone(), TerminalSize { cols: 20, rows: 8 })
.expect("session created");
let target = WindowTarget::with_window(session_name.clone(), 0);
state
.options
.set(
ScopeSelector::Session(session_name.clone()),
OptionName::Status,
"off".to_owned(),
SetOptionMode::Replace,
)
.expect("status option");
for (option, value) in [
(OptionName::PaneBorderStatus, "bottom"),
(OptionName::PaneScrollbars, "on"),
] {
state
.options
.set(
ScopeSelector::Window(target.clone()),
option,
value.to_owned(),
SetOptionMode::Replace,
)
.expect("mouse geometry option");
}
let layout = layout_for_session(&state, &session_name, 1).expect("mouse layout");
let pane = layout.panes.first().expect("pane target");
let scrollbar = pane.scrollbar.expect("scrollbar target");
assert_eq!(pane.geometry, PaneGeometry::new(0, 0, 19, 7));
assert_eq!(scrollbar.slider_h, 7);
}
fn raw(b: u16, x: u16, y: u16) -> MouseForwardEvent {
MouseForwardEvent {
b,
lb: b,
x,
y,
lx: x,
ly: y,
sgr_b: b,
sgr_type: ' ',
ignore: false,
}
}
#[test]
fn status_ranges_hit_left_right_window_session_user_and_control() {
let mut state = super::ClientMouseState::default();
let now = Instant::now();
let left = classify_mouse_event(&mut state, &layout(), raw(0, 1, 0), now).expect("left");
assert_eq!(left.event.location, MouseLocation::StatusLeft);
let right = classify_mouse_event(&mut state, &layout(), raw(0, 5, 0), now).expect("right");
assert_eq!(right.event.location, MouseLocation::StatusRight);
let window = classify_mouse_event(&mut state, &layout(), raw(0, 9, 0), now).expect("window");
assert_eq!(window.event.location, MouseLocation::Status);
assert_eq!(window.event.window_id, Some(9));
let session = classify_mouse_event(&mut state, &layout(), raw(0, 13, 0), now).expect("session");
assert_eq!(session.event.session_id, 4);
let user = classify_mouse_event(&mut state, &layout(), raw(0, 17, 0), now).expect("user");
assert_eq!(user.event.location, MouseLocation::Status);
let control = classify_mouse_event(&mut state, &layout(), raw(0, 22, 0), now).expect("control");
assert_eq!(control.event.location, MouseLocation::Control(3));
}
#[test]
fn status_ranges_are_matched_on_the_clicked_status_line() {
let mut state = super::ClientMouseState::default();
let now = Instant::now();
let mut layout = layout();
layout.status_lines = 2;
layout.status = Some(StatusLineLayout {
ranges: vec![StatusRange {
line: 1,
x: 0..=3,
kind: StatusRangeType::Window(42),
}],
});
let first_line =
classify_mouse_event(&mut state, &layout, raw(0, 1, 0), now).expect("first line");
assert_eq!(first_line.event.location, MouseLocation::StatusDefault);
assert_eq!(first_line.event.window_id, None);
let second_line =
classify_mouse_event(&mut state, &layout, raw(0, 1, 1), now).expect("second line");
assert_eq!(second_line.event.location, MouseLocation::Status);
assert_eq!(second_line.event.window_id, Some(42));
}
#[test]
fn click_sequence_resets_on_button_location_or_pane_change() {
let mut state = super::ClientMouseState::default();
let base = Instant::now();
let layout = layout();
let down = classify_mouse_event(&mut state, &layout, raw(0, 5, 5), base).expect("down");
assert_eq!(down.event.location, MouseLocation::Pane);
assert_eq!(
down.key,
key_string_lookup_string("MouseDown1Pane").unwrap()
);
let second = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
lx: 5,
ly: 5,
..raw(0, 5, 5)
},
base + std::time::Duration::from_millis(100),
)
.expect("second");
assert_eq!(
second.key,
key_string_lookup_string("SecondClick1Pane").unwrap()
);
let reset = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
lx: 5,
ly: 5,
..raw(0, 50, 5)
},
base + std::time::Duration::from_millis(150),
)
.expect("reset");
assert_eq!(
reset.key,
key_string_lookup_string("MouseDown1Pane").unwrap()
);
let expired = state.expire_click_timer(base + std::time::Duration::from_millis(700), &layout);
assert!(expired.is_none(), "first click timeout just resets state");
}
#[test]
fn second_click_timeout_yields_double_click_and_third_click_has_no_timer() {
let mut state = super::ClientMouseState::default();
let base = Instant::now();
let layout = layout();
let _ = classify_mouse_event(&mut state, &layout, raw(0, 5, 5), base);
let _ = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
lx: 5,
ly: 5,
..raw(0, 5, 5)
},
base + std::time::Duration::from_millis(100),
);
let double = state
.expire_click_timer(base + std::time::Duration::from_millis(500), &layout)
.expect("double click");
assert_eq!(
double.key,
key_string_lookup_string("DoubleClick1Pane").unwrap()
);
let _ = classify_mouse_event(&mut state, &layout, raw(0, 5, 5), base);
let _ = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
lx: 5,
ly: 5,
..raw(0, 5, 5)
},
base + std::time::Duration::from_millis(100),
);
let triple = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
lx: 5,
ly: 5,
..raw(0, 5, 5)
},
base + std::time::Duration::from_millis(150),
)
.expect("triple");
assert_eq!(
triple.key,
key_string_lookup_string("TripleClick1Pane").unwrap()
);
assert!(
state.click_deadline.is_none(),
"triple click skips the timer"
);
}
#[test]
fn late_third_click_does_not_drop_pending_double_click() {
let mut state = super::ClientMouseState::default();
let base = Instant::now();
let layout = layout();
let _ = classify_mouse_event(&mut state, &layout, raw(0, 5, 5), base);
let _ = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
lx: 5,
ly: 5,
..raw(0, 5, 5)
},
base + std::time::Duration::from_millis(100),
);
let events = classify_mouse_events(
&mut state,
&layout,
MouseForwardEvent {
lx: 5,
ly: 5,
..raw(0, 5, 5)
},
base + std::time::Duration::from_millis(500),
);
assert_eq!(
events.iter().map(|event| event.key).collect::<Vec<_>>(),
vec![
key_string_lookup_string("DoubleClick1Pane").unwrap(),
key_string_lookup_string("MouseDown1Pane").unwrap(),
],
"the first two clicks should still dispatch DoubleClick1Pane before the late click"
);
}
#[test]
fn drag_update_uses_dragging_sentinel_and_release_synthesizes_drag_end() {
let mut state = super::ClientMouseState {
drag_handler: Some(MouseDragHandler::CopyModeSelection {
target: pane_target(0),
}),
..Default::default()
};
let now = Instant::now();
let layout = layout();
let start = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
b: 32,
lb: 0,
x: 6,
y: 6,
lx: 5,
ly: 5,
sgr_b: 32,
sgr_type: ' ',
ignore: false,
},
now,
)
.expect("drag start");
assert_eq!(start.key, super::KEYC_DRAGGING);
assert_eq!(start.event.raw.x, 5);
assert_eq!(start.event.raw.y, 5);
let context = copy_mode_mouse_context(&start.event, layout.panes[0].geometry, -1)
.expect("copy-mode drag start context");
assert_eq!(context.content_x, 5);
assert_eq!(context.content_y, 4);
assert_eq!(state.drag_flag, 1);
let end = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
b: 3,
lb: 0,
x: 7,
y: 7,
lx: 6,
ly: 6,
sgr_b: 0,
sgr_type: ' ',
ignore: false,
},
now + std::time::Duration::from_millis(10),
)
.expect("drag end");
assert_eq!(
end.key,
key_string_lookup_string("MouseDragEnd1Pane").unwrap()
);
assert_eq!(state.drag_flag, 0);
assert_eq!(state.slider_mpos, -1);
}
#[test]
fn plain_mouse_drag_keeps_current_coordinates_for_bindings() {
let mut state = super::ClientMouseState::default();
let now = Instant::now();
let layout = layout();
let drag = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
b: 32,
lb: 0,
x: 6,
y: 6,
lx: 5,
ly: 5,
sgr_b: 32,
sgr_type: 'M',
ignore: false,
},
now,
)
.expect("drag event");
assert_eq!(
drag.key,
key_string_lookup_string("MouseDrag1Pane").unwrap()
);
assert_eq!(drag.event.raw.x, 6);
assert_eq!(drag.event.raw.y, 6);
}
#[test]
fn scrollbar_drag_forces_slider_location_and_tracks_relative_mpos() {
let mut state = super::ClientMouseState::default();
let mut layout = layout();
layout.panes[0].scrollbar = Some(PaneScrollbar {
position: ScrollbarPosition::Right,
width: 1,
pad: 0,
slider_y: 2,
slider_h: 3,
});
let now = Instant::now();
let drag = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
b: 32,
lb: 0,
x: 40,
y: 4,
lx: 40,
ly: 4,
sgr_b: 32,
sgr_type: ' ',
ignore: false,
},
now,
)
.expect("scrollbar drag");
assert_eq!(drag.event.location, MouseLocation::ScrollbarSlider);
assert!(state.scrolling_flag);
assert_eq!(state.slider_mpos, 1 + i32::from(layout.status_lines));
let move_outside = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
b: 32,
lb: 0,
x: 10,
y: 8,
lx: 40,
ly: 4,
sgr_b: 32,
sgr_type: ' ',
ignore: false,
},
now + std::time::Duration::from_millis(5),
)
.expect("forced slider location");
assert_eq!(move_outside.event.location, MouseLocation::ScrollbarSlider);
}
#[test]
fn focus_follows_mouse_returns_the_new_pane_target() {
let mut state = super::ClientMouseState::default();
let event = classify_mouse_event(
&mut state,
&layout(),
MouseForwardEvent {
b: 35,
lb: 35,
x: 50,
y: 6,
lx: 49,
ly: 6,
sgr_b: 35,
sgr_type: 'm',
ignore: false,
},
Instant::now(),
)
.expect("move");
assert_eq!(
event.key,
key_string_lookup_string("MouseMovePane").unwrap()
);
assert_eq!(event.focus_target, Some(PaneId::new(1)));
}
#[test]
fn border_controls_win_over_plain_border_hits() {
let mut state = super::ClientMouseState::default();
let event =
classify_mouse_event(&mut state, &layout(), raw(0, 41, 5), Instant::now()).expect("border");
assert_eq!(event.event.location, MouseLocation::Control(2));
}
#[test]
fn border_drag_stays_routed_to_border_after_cursor_moves_into_pane() {
let mut state = super::ClientMouseState::default();
let now = Instant::now();
let layout = layout();
let down = classify_mouse_event(&mut state, &layout, raw(0, 41, 6), now).expect("down");
assert_eq!(down.event.location, MouseLocation::Border);
assert_eq!(
down.key,
key_string_lookup_string("MouseDown1Border").unwrap()
);
let first_drag = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
b: 32,
lb: 0,
x: 39,
y: 6,
lx: 41,
ly: 6,
sgr_b: 32,
sgr_type: ' ',
ignore: false,
},
now + std::time::Duration::from_millis(5),
)
.expect("first drag");
assert_eq!(first_drag.event.location, MouseLocation::Border);
assert_eq!(first_drag.event.raw.lx, 41);
assert_eq!(first_drag.event.raw.ly, 6);
assert_eq!(
first_drag.key,
key_string_lookup_string("MouseDrag1Border").unwrap()
);
let later_drag = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
b: 32,
lb: 0,
x: 38,
y: 6,
lx: 39,
ly: 6,
sgr_b: 32,
sgr_type: ' ',
ignore: false,
},
now + std::time::Duration::from_millis(10),
)
.expect("later drag");
assert_eq!(later_drag.event.location, MouseLocation::Border);
assert_eq!(later_drag.event.raw.lx, 39);
assert_eq!(later_drag.event.raw.ly, 6);
assert_eq!(
later_drag.key,
key_string_lookup_string("MouseDrag1Border").unwrap()
);
let release = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
b: 3,
lb: 0,
x: 38,
y: 6,
lx: 38,
ly: 6,
sgr_b: 0,
sgr_type: 'm',
ignore: false,
},
now + std::time::Duration::from_millis(15),
)
.expect("release");
assert_eq!(release.event.location, MouseLocation::Border);
assert_eq!(
release.key,
key_string_lookup_string("MouseDragEnd1Border").unwrap()
);
assert_eq!(state.drag_flag, 0);
assert!(state.drag_start_event.is_none());
}
#[test]
fn pane_drag_crossing_border_stays_routed_to_press_pane() {
let mut state = super::ClientMouseState::default();
let now = Instant::now();
let mut layout = layout();
layout.panes[0].scrollbar = None;
let down = classify_mouse_event(&mut state, &layout, raw(0, 39, 6), now).expect("down");
assert_eq!(down.event.location, MouseLocation::Pane);
assert_eq!(
down.key,
key_string_lookup_string("MouseDown1Pane").unwrap()
);
let drag = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
b: 32,
lb: 0,
x: 42,
y: 6,
lx: 39,
ly: 6,
sgr_b: 32,
sgr_type: ' ',
ignore: false,
},
now + std::time::Duration::from_millis(5),
)
.expect("drag");
assert_eq!(drag.event.location, MouseLocation::Pane);
assert_eq!(drag.event.pane_id, Some(PaneId::new(0)));
assert_eq!(drag.event.pane_target, Some(pane_target(0)));
assert_eq!(drag.event.raw.x, 42);
assert_eq!(drag.event.raw.y, 6);
assert_eq!(
drag.key,
key_string_lookup_string("MouseDrag1Pane").unwrap()
);
let release = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
b: 3,
lb: 0,
x: 42,
y: 6,
lx: 42,
ly: 6,
sgr_b: 0,
sgr_type: 'm',
ignore: false,
},
now + std::time::Duration::from_millis(10),
)
.expect("release");
assert_eq!(release.event.location, MouseLocation::Pane);
assert_eq!(release.event.pane_id, Some(PaneId::new(0)));
assert_eq!(
release.key,
key_string_lookup_string("MouseDragEnd1Pane").unwrap()
);
}
#[test]
fn adjacent_pane_down_moving_away_from_border_stays_pane_drag() {
let mut state = super::ClientMouseState::default();
let now = Instant::now();
let mut layout = layout();
layout.panes[0].scrollbar = None;
let down = classify_mouse_event(&mut state, &layout, raw(0, 39, 6), now).expect("down");
assert_eq!(down.event.location, MouseLocation::Pane);
let drag = classify_mouse_event(
&mut state,
&layout,
MouseForwardEvent {
b: 32,
lb: 0,
x: 37,
y: 6,
lx: 39,
ly: 6,
sgr_b: 32,
sgr_type: ' ',
ignore: false,
},
now + std::time::Duration::from_millis(5),
)
.expect("drag");
assert_eq!(drag.event.location, MouseLocation::Pane);
assert_eq!(
drag.key,
key_string_lookup_string("MouseDrag1Pane").unwrap()
);
}
#[test]
fn copy_mode_mouse_context_converts_to_content_coordinates() {
let event = super::AttachedMouseEvent {
raw: raw(0, 10, 6),
session_id: 1,
window_id: Some(5),
pane_id: Some(PaneId::new(0)),
pane_target: Some(pane_target(0)),
location: MouseLocation::Pane,
status_at: Some(0),
status_lines: 1,
ignore: false,
};
let context =
copy_mode_mouse_context(&event, PaneGeometry::new(0, 0, 40, 10), 3).expect("context");
assert_eq!(context.content_x, 10);
assert_eq!(context.content_y, 5);
assert_eq!(context.scroll_y, 6);
assert_eq!(context.slider_mpos, 3);
}
#[test]
fn copy_mode_mouse_context_unoffsets_the_line_number_gutter() {
let mut event = super::AttachedMouseEvent {
raw: raw(0, 2, 1),
session_id: 1,
window_id: Some(5),
pane_id: Some(PaneId::new(0)),
pane_target: Some(pane_target(0)),
location: MouseLocation::Pane,
status_at: None,
status_lines: 0,
ignore: false,
};
let layout = CopyModeLineNumberLayout::resolve(Some("absolute"), true, 23, 8, 0, 0)
.expect("line-number layout");
let gutter = copy_mode_mouse_context_with_line_numbers(
&event,
PaneGeometry::new(0, 0, 20, 8),
-1,
Some(layout),
)
.expect("gutter context");
assert_eq!(gutter.content_x, 0);
event.raw.x = 10;
let content = copy_mode_mouse_context_with_line_numbers(
&event,
PaneGeometry::new(0, 0, 20, 8),
-1,
Some(layout),
)
.expect("content context");
assert_eq!(content.content_x, 6);
}
#[test]
fn expire_click_timer_is_noop_when_no_deadline_set() {
let mut state = super::ClientMouseState::default();
assert!(state.click_deadline.is_none());
state.triple_click_pending = true; let result = state.expire_click_timer(Instant::now(), &layout());
assert!(result.is_none());
assert!(state.triple_click_pending);
}
#[test]
fn expire_click_timer_preserves_state_when_deadline_not_yet_reached() {
let now = Instant::now();
let mut state = super::ClientMouseState {
click_deadline: Some(now + std::time::Duration::from_secs(10)),
double_click_pending: true,
..Default::default()
};
let result = state.expire_click_timer(now, &layout());
assert!(result.is_none());
assert!(state.double_click_pending, "state must be untouched");
assert!(state.click_deadline.is_some());
}
#[test]
fn drag_end_preserves_modifier_bits_from_release_event() {
let mut state = super::ClientMouseState {
drag_flag: 1, ..Default::default()
};
let now = Instant::now();
let layout = layout();
let release = super::MouseForwardEvent {
b: 3, lb: 16, x: 5,
y: 5,
lx: 5,
ly: 5,
sgr_b: 16,
sgr_type: 'm',
ignore: false,
};
let event = classify_mouse_event(&mut state, &layout, release, now).expect("drag end");
let base_key = key_string_lookup_string("MouseDragEnd1Pane").unwrap();
assert_eq!(event.key & !rmux_core::KEYC_CTRL, base_key);
assert_ne!(
event.key & rmux_core::KEYC_CTRL,
0,
"Ctrl modifier preserved"
);
}
#[test]
fn sgr_release_provides_correct_button_for_mouse_up() {
let mut state = super::ClientMouseState::default();
let now = Instant::now();
let layout = layout();
let release = super::MouseForwardEvent {
b: 3, lb: 1, x: 5,
y: 5,
lx: 5,
ly: 5,
sgr_b: 1, sgr_type: 'm',
ignore: false,
};
let event = classify_mouse_event(&mut state, &layout, release, now).expect("mouse up");
assert_eq!(
event.key,
key_string_lookup_string("MouseUp2Pane").unwrap(),
"SGR release should use sgr_b for button identity"
);
}
#[test]
fn wheel_events_are_not_affected_by_active_drag() {
let mut state = super::ClientMouseState {
drag_flag: 1,
..Default::default()
};
let now = Instant::now();
let layout = layout();
let wheel = raw(64, 5, 5);
let event = classify_mouse_event(&mut state, &layout, wheel, now).expect("wheel");
assert_eq!(
event.key,
key_string_lookup_string("WheelUpPane").unwrap(),
"wheel event is not converted to drag end"
);
assert_eq!(
state.drag_flag, 1,
"drag flag preserved across wheel events"
);
}
#[test]
fn scrollbar_from_view_with_zero_history_returns_full_slider() {
let sb = PaneScrollbar::from_view(
10,
0,
false,
PaneScrollbarsMode::On,
ScrollbarPosition::Right,
1,
0,
None,
)
.expect("scrollbar");
assert_eq!(sb.slider_y, 0);
assert_eq!(
sb.slider_h, 10,
"slider covers entire scrollbar when no history"
);
}
#[test]
fn scrollbar_from_view_alternate_on_returns_none() {
assert!(PaneScrollbar::from_view(
10,
100,
true,
PaneScrollbarsMode::On,
ScrollbarPosition::Right,
1,
0,
None,
)
.is_none());
}
#[test]
fn scrollbar_from_view_modal_without_copy_mode_returns_none() {
assert!(PaneScrollbar::from_view(
10,
100,
false,
PaneScrollbarsMode::Modal,
ScrollbarPosition::Right,
1,
0,
None,
)
.is_none());
}
#[test]
fn scrollbar_from_view_modal_with_copy_mode_returns_some() {
assert!(PaneScrollbar::from_view(
10,
100,
false,
PaneScrollbarsMode::Modal,
ScrollbarPosition::Right,
1,
0,
Some(0),
)
.is_some());
}
#[test]
fn scrollbar_from_view_zero_rows_returns_none() {
assert!(PaneScrollbar::from_view(
0,
100,
false,
PaneScrollbarsMode::On,
ScrollbarPosition::Right,
1,
0,
None,
)
.is_none());
}
#[test]
fn scrollbar_from_view_zero_width_returns_none() {
assert!(PaneScrollbar::from_view(
10,
100,
false,
PaneScrollbarsMode::On,
ScrollbarPosition::Right,
0,
0,
None,
)
.is_none());
}
#[test]
fn scrollbar_slider_never_exceeds_scrollbar_height() {
let sb = PaneScrollbar::from_view(
5,
10000,
false,
PaneScrollbarsMode::On,
ScrollbarPosition::Right,
1,
0,
Some(5000),
)
.expect("scrollbar");
assert!(sb.slider_y < 5, "slider_y within bounds");
assert!(sb.slider_h >= 1, "slider_h at least 1");
assert!(
sb.slider_y + sb.slider_h <= 5,
"slider bottom within scrollbar: {} + {} <= 5",
sb.slider_y,
sb.slider_h
);
}
#[test]
fn scrollbar_copy_mode_offset_at_max_produces_slider_at_top() {
let sb = PaneScrollbar::from_view(
10,
100,
false,
PaneScrollbarsMode::On,
ScrollbarPosition::Right,
1,
0,
Some(100),
)
.expect("scrollbar");
assert!(sb.slider_y <= 10);
assert!(sb.slider_h >= 1);
assert!(
sb.slider_y + sb.slider_h <= 10,
"slider bottom within scrollbar at max offset: {} + {} <= 10",
sb.slider_y,
sb.slider_h
);
}
#[test]
fn scrollbar_slider_clamped_at_extreme_offsets() {
for offset in [0, 1, 50, 99, 100, 200, 500, 1000] {
let sb = PaneScrollbar::from_view(
3,
1000,
false,
PaneScrollbarsMode::On,
ScrollbarPosition::Right,
1,
0,
Some(offset),
)
.expect("scrollbar");
assert!(
sb.slider_y + sb.slider_h <= 3,
"offset {offset}: slider bottom within scrollbar: {} + {} <= 3",
sb.slider_y,
sb.slider_h
);
}
}
#[test]
fn click_sequence_resets_on_different_button() {
let mut state = super::ClientMouseState::default();
let base = Instant::now();
let layout = layout();
let _ = classify_mouse_event(&mut state, &layout, raw(0, 5, 5), base);
let second = classify_mouse_event(
&mut state,
&layout,
super::MouseForwardEvent {
lx: 5,
ly: 5,
..raw(1, 5, 5)
},
base + std::time::Duration::from_millis(50),
)
.expect("reset to down");
assert_eq!(
second.key,
key_string_lookup_string("MouseDown2Pane").unwrap(),
"different button resets to MouseDown"
);
}
#[test]
fn mouse_move_outside_active_pane_does_not_trigger_focus_change() {
let mut state = super::ClientMouseState::default();
let mut layout = layout();
layout.focus_follows_mouse = true;
layout.active_pane = Some(PaneId::new(0));
let event = classify_mouse_event(
&mut state,
&layout,
super::MouseForwardEvent {
b: 35,
lb: 35,
x: 5,
y: 5,
lx: 4,
ly: 5,
sgr_b: 35,
sgr_type: 'm',
ignore: false,
},
Instant::now(),
)
.expect("move");
assert_eq!(
event.focus_target, None,
"no focus change within active pane"
);
}
#[test]
fn nowhere_hit_returns_none() {
let mut state = super::ClientMouseState::default();
let mut layout = layout();
layout.panes.clear(); let result = classify_mouse_event(&mut state, &layout, raw(0, 5, 5), Instant::now());
assert!(result.is_none(), "nowhere hit returns None");
}
#[test]
fn left_scrollbar_position_hit_detection() {
let mut state = super::ClientMouseState::default();
let mut layout = layout();
layout.panes[0].scrollbar = Some(PaneScrollbar {
position: ScrollbarPosition::Left,
width: 1,
pad: 0,
slider_y: 3,
slider_h: 4,
});
layout.panes[0].geometry = PaneGeometry::new(1, 0, 39, 10);
let event = classify_mouse_event(&mut state, &layout, raw(0, 0, 2), Instant::now())
.expect("scrollbar up");
assert_eq!(event.event.location, MouseLocation::ScrollbarUp);
}
#[test]
fn scrollbar_padding_is_a_pane_binding_location_on_both_sides() {
for (position, geometry, pad_x) in [
(
ScrollbarPosition::Right,
PaneGeometry::new(0, 0, 17, 10),
17,
),
(ScrollbarPosition::Left, PaneGeometry::new(3, 0, 17, 10), 2),
] {
let mut state = super::ClientMouseState::default();
let mut layout = layout();
layout.panes[0].geometry = geometry;
layout.panes[0].scrollbar = Some(PaneScrollbar {
position,
width: 2,
pad: 1,
slider_y: 3,
slider_h: 4,
});
let event = classify_mouse_event(&mut state, &layout, raw(0, pad_x, 2), Instant::now())
.expect("scrollbar padding belongs to the pane binding region");
assert_eq!(event.event.location, MouseLocation::Pane, "{position:?}");
assert_eq!(
event.key,
key_string_lookup_string("MouseDown1Pane").expect("known mouse key"),
"{position:?}"
);
}
}
#[test]
fn left_scrollbar_rightmost_content_remains_pane() {
let mut layout = layout();
layout.panes[0].geometry = PaneGeometry::new(3, 0, 17, 10);
layout.panes[0].scrollbar = Some(PaneScrollbar {
position: ScrollbarPosition::Left,
width: 2,
pad: 1,
slider_y: 3,
slider_h: 4,
});
for x in 17..=19 {
let mut state = super::ClientMouseState::default();
let event = classify_mouse_event(&mut state, &layout, raw(0, x, 2), Instant::now())
.expect("visible content remains mouse-addressable");
assert_eq!(event.event.location, MouseLocation::Pane, "x={x}");
}
}
#[test]
fn pane_border_status_precedes_scrollbar_and_content_hit_regions() {
for (position, pane_status, geometry, status_y) in [
(
ScrollbarPosition::Right,
PaneBorderStatus::Top,
PaneGeometry::new(0, 1, 17, 7),
0,
),
(
ScrollbarPosition::Left,
PaneBorderStatus::Top,
PaneGeometry::new(3, 1, 17, 7),
0,
),
(
ScrollbarPosition::Right,
PaneBorderStatus::Bottom,
PaneGeometry::new(0, 0, 17, 7),
7,
),
(
ScrollbarPosition::Left,
PaneBorderStatus::Bottom,
PaneGeometry::new(3, 0, 17, 7),
7,
),
] {
let mut layout = layout();
layout.status_at = None;
layout.status_lines = 0;
layout.pane_border_status = pane_status;
layout.panes.truncate(1);
layout.panes[0].geometry = geometry;
layout.panes[0].scrollbar = Some(PaneScrollbar {
position,
width: 2,
pad: 1,
slider_y: 2,
slider_h: 3,
});
layout.panes[0].border_controls = vec![super::BorderControlRange {
x: 3..=3,
y: status_y,
control: 9,
}];
for x in [0, 1, 2, 3, 16, 19] {
let mut state = super::ClientMouseState::default();
let event =
classify_mouse_event(&mut state, &layout, raw(0, x, status_y), Instant::now())
.expect("pane status row remains mouse-addressable");
let expected = if x == 3 {
MouseLocation::Control(9)
} else {
MouseLocation::Border
};
assert_eq!(
event.event.location, expected,
"{position:?} {pane_status:?} x={x}"
);
}
}
}
#[test]
fn copy_mode_mouse_context_returns_none_without_pane_id() {
let event = super::AttachedMouseEvent {
raw: raw(0, 5, 5),
session_id: 1,
window_id: Some(5),
pane_id: None,
pane_target: None,
location: MouseLocation::Pane,
status_at: None,
status_lines: 0,
ignore: false,
};
assert!(copy_mode_mouse_context(&event, PaneGeometry::new(0, 0, 40, 10), 0).is_none());
}