aboutsummaryrefslogtreecommitdiff
path: root/src/app/input/selection.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/input/selection.rs')
-rw-r--r--src/app/input/selection.rs230
1 files changed, 217 insertions, 13 deletions
diff --git a/src/app/input/selection.rs b/src/app/input/selection.rs
index 953e9b26..ad1dd33f 100644
--- a/src/app/input/selection.rs
+++ b/src/app/input/selection.rs
@@ -1,9 +1,9 @@
use crossterm::event::{MouseEvent, MouseEventKind};
-use crate::app::state::AppState;
+use crate::app::state::{AppState, SelectionAutoscroll, SelectionAutoscrollDirection};
impl AppState {
- fn update_selection_cursor(
+ pub(crate) fn update_selection_cursor(
&mut self,
pane_id: crate::layout::PaneId,
screen_col: u16,
@@ -30,20 +30,97 @@ impl AppState {
return;
};
+ let top = info.inner_rect.y;
let bottom = info.inner_rect.y + info.inner_rect.height.saturating_sub(1);
- if screen_row < info.inner_rect.y {
- self.scroll_pane_up(
- pane_id,
- Self::selection_edge_scroll_lines(info.inner_rect.y - screen_row),
- );
- } else if screen_row > bottom {
- self.scroll_pane_down(
- pane_id,
- Self::selection_edge_scroll_lines(screen_row - bottom),
- );
- }
+ // Only activate autoscroll when the user is actively dragging.
+ // An anchored click in the hot zone should not start the timer.
+ // Check before advancing the cursor: if already Dragging from a prior
+ // event, it stays true. If Anchored, the mouse must have moved away
+ // from the anchor cell for this to count as a real drag.
+ let was_dragging = self.selection.as_ref().is_some_and(|s| s.is_dragging());
+ let anchor_differs_from_mouse = self.selection.as_ref().is_some_and(|s| {
+ // Convert anchor to screen coords for comparison.
+ // Anchor is stored in absolute row; for a simple screen
+ // comparison, check whether the mouse is on a different
+ // cell than the anchor's screen position.
+ let (ar, ac) =
+ s.anchor_screen_pos(info.inner_rect, self.pane_scroll_metrics(s.pane_id));
+ ar != screen_row || ac != screen_col
+ });
+ let is_dragging = was_dragging || anchor_differs_from_mouse;
+
+ // Advance the selection cursor.
self.update_selection_cursor(pane_id, screen_col, screen_row);
+
+ // If the mouse is on a different cell than the anchor but drag()
+ // didn't transition (cursor clamped to edge == anchor), force
+ // Dragging so the selection becomes visible and autoscroll can run.
+ if is_dragging {
+ if let Some(sel) = self.selection.as_mut() {
+ if sel.is_just_click() {
+ sel.force_dragging();
+ }
+ }
+ }
+
+ if screen_row < top {
+ // Cursor above pane — immediate scroll + set autoscroll state
+ if is_dragging {
+ self.scroll_pane_up(pane_id, Self::selection_edge_scroll_lines(top - screen_row));
+ // Re-advance cursor after scroll so it reflects the new viewport position
+ self.update_selection_cursor(pane_id, screen_col, screen_row);
+ self.selection_autoscroll = Some(SelectionAutoscroll {
+ direction: SelectionAutoscrollDirection::Up,
+ last_mouse_screen_col: screen_col,
+ last_mouse_screen_row: screen_row,
+ inner_rect: info.inner_rect,
+ });
+ }
+ } else if screen_row > bottom {
+ // Cursor below pane — immediate scroll + set autoscroll state
+ if is_dragging {
+ self.scroll_pane_down(
+ pane_id,
+ Self::selection_edge_scroll_lines(screen_row - bottom),
+ );
+ // Re-advance cursor after scroll so it reflects the new viewport position
+ self.update_selection_cursor(pane_id, screen_col, screen_row);
+ self.selection_autoscroll = Some(SelectionAutoscroll {
+ direction: SelectionAutoscrollDirection::Down,
+ last_mouse_screen_col: screen_col,
+ last_mouse_screen_row: screen_row,
+ inner_rect: info.inner_rect,
+ });
+ }
+ } else if screen_row == top {
+ // Hot zone: top edge row — no immediate scroll, set autoscroll state
+ if is_dragging {
+ self.selection_autoscroll = Some(SelectionAutoscroll {
+ direction: SelectionAutoscrollDirection::Up,
+ last_mouse_screen_col: screen_col,
+ last_mouse_screen_row: screen_row,
+ inner_rect: info.inner_rect,
+ });
+ } else {
+ self.selection_autoscroll = None;
+ }
+ } else if screen_row == bottom {
+ // Hot zone: bottom edge row — no immediate scroll, set autoscroll state
+ if is_dragging {
+ self.selection_autoscroll = Some(SelectionAutoscroll {
+ direction: SelectionAutoscrollDirection::Down,
+ last_mouse_screen_col: screen_col,
+ last_mouse_screen_row: screen_row,
+ inner_rect: info.inner_rect,
+ });
+ } else {
+ self.selection_autoscroll = None;
+ }
+ } else {
+ // Safe zone: inside pane, not on edge rows — clear autoscroll
+ self.selection_autoscroll = None;
+ }
}
pub(super) fn scroll_selection_with_wheel(&mut self, mouse: MouseEvent) -> bool {
@@ -66,3 +143,130 @@ impl AppState {
true
}
}
+
+#[cfg(test)]
+mod autoscroll_tests {
+ use super::*;
+ use crate::layout::PaneInfo;
+ use crate::workspace::Workspace;
+ use ratatui::layout::Rect;
+
+ /// Build an AppState with one workspace/pane and pane_infos populated
+ /// so pane_info_by_id works. Returns (state, pane_id).
+ fn make_state_with_pane() -> (AppState, crate::layout::PaneId) {
+ let mut state = AppState::test_new();
+ let ws = Workspace::test_new("test");
+ let pane_id = ws.tabs[0].root_pane;
+ state.workspaces.push(ws);
+ state.active = Some(0);
+ state.view.pane_infos.push(PaneInfo {
+ id: pane_id,
+ rect: Rect::new(0, 0, 80, 24),
+ inner_rect: Rect::new(0, 0, 80, 24),
+ scrollbar_rect: None,
+ is_focused: true,
+ });
+ (state, pane_id)
+ }
+
+ #[test]
+ fn above_pane_sets_autoscroll_up() {
+ // Build state with pane starting at row 5 so we can drag above it
+ let mut state = AppState::test_new();
+ let ws = Workspace::test_new("test");
+ let pane_id = ws.tabs[0].root_pane;
+ state.workspaces.push(ws);
+ state.active = Some(0);
+ state.view.pane_infos.push(PaneInfo {
+ id: pane_id,
+ rect: Rect::new(0, 5, 80, 24),
+ inner_rect: Rect::new(0, 5, 80, 24),
+ scrollbar_rect: None,
+ is_focused: true,
+ });
+ // Anchor at (5, 10), drag to different cell above pane
+ let mut sel = crate::selection::Selection::anchor(pane_id, 5, 10, None);
+ sel.drag(4, 5, Rect::new(0, 5, 80, 24), None);
+ state.selection = Some(sel);
+ state.update_selection_drag(5, 4);
+ let autoscroll = state.selection_autoscroll.as_ref().unwrap();
+ assert_eq!(autoscroll.direction, SelectionAutoscrollDirection::Up);
+ }
+
+ #[test]
+ fn top_hot_zone_sets_autoscroll_up_on_drag() {
+ let (mut state, pane_id) = make_state_with_pane();
+ // Anchor at (5, 10), drag to top edge row (row 0) — different cell
+ let mut sel = crate::selection::Selection::anchor(pane_id, 5, 10, None);
+ sel.drag(0, 0, Rect::new(0, 0, 80, 24), None);
+ state.selection = Some(sel);
+ state.update_selection_drag(0, 0);
+ let autoscroll = state.selection_autoscroll.as_ref().unwrap();
+ assert_eq!(autoscroll.direction, SelectionAutoscrollDirection::Up);
+ }
+
+ #[test]
+ fn top_hot_zone_clears_autoscroll_on_click() {
+ // An anchored click on the top edge row should NOT start autoscroll.
+ let (mut state, pane_id) = make_state_with_pane();
+ state.selection = Some(crate::selection::Selection::anchor(pane_id, 0, 0, None));
+ // Same-cell drag on top edge row — still anchored
+ state.update_selection_drag(0, 0);
+ assert!(state.selection_autoscroll.is_none());
+ }
+
+ #[test]
+ fn bottom_hot_zone_sets_autoscroll_down_on_drag() {
+ let (mut state, pane_id) = make_state_with_pane();
+ // Anchor at (0, 0), drag to bottom edge row (row 23) — different cell
+ let mut sel = crate::selection::Selection::anchor(pane_id, 0, 0, None);
+ sel.drag(23, 0, Rect::new(0, 0, 80, 24), None);
+ state.selection = Some(sel);
+ state.update_selection_drag(0, 23);
+ let autoscroll = state.selection_autoscroll.as_ref().unwrap();
+ assert_eq!(autoscroll.direction, SelectionAutoscrollDirection::Down);
+ }
+
+ #[test]
+ fn bottom_hot_zone_clears_autoscroll_on_click() {
+ // An anchored click on the bottom edge row should NOT start autoscroll.
+ let (mut state, pane_id) = make_state_with_pane();
+ // Anchor at bottom edge row
+ state.selection = Some(crate::selection::Selection::anchor(pane_id, 23, 0, None));
+ // Same-cell drag — still anchored
+ state.update_selection_drag(0, 23);
+ assert!(state.selection_autoscroll.is_none());
+ }
+
+ #[test]
+ fn below_pane_sets_autoscroll_down_on_drag() {
+ let (mut state, pane_id) = make_state_with_pane();
+ // Anchor at (0, 0), drag to different cell below pane
+ let mut sel = crate::selection::Selection::anchor(pane_id, 0, 0, None);
+ sel.drag(5, 5, Rect::new(0, 0, 80, 24), None);
+ state.selection = Some(sel);
+ // Drag cursor one row below the pane bottom
+ state.update_selection_drag(0, 24);
+ let autoscroll = state.selection_autoscroll.as_ref().unwrap();
+ assert_eq!(autoscroll.direction, SelectionAutoscrollDirection::Down);
+ }
+
+ #[test]
+ fn safe_zone_clears_autoscroll() {
+ let (mut state, pane_id) = make_state_with_pane();
+ // Anchor at (0, 0), drag to (5, 5) so it's truly dragging
+ let mut sel = crate::selection::Selection::anchor(pane_id, 0, 0, None);
+ sel.drag(5, 5, Rect::new(0, 0, 80, 24), None);
+ state.selection = Some(sel);
+ // Set autoscroll first
+ state.selection_autoscroll = Some(SelectionAutoscroll {
+ direction: SelectionAutoscrollDirection::Down,
+ last_mouse_screen_col: 5,
+ last_mouse_screen_row: 23,
+ inner_rect: Rect::new(0, 0, 80, 24),
+ });
+ // Move cursor into safe zone (middle of pane, not on edge rows)
+ state.update_selection_drag(5, 12);
+ assert!(state.selection_autoscroll.is_none());
+ }
+}