aboutsummaryrefslogtreecommitdiff
path: root/src/app/input
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/input')
-rw-r--r--src/app/input/mod.rs49
-rw-r--r--src/app/input/mouse.rs118
-rw-r--r--src/app/input/terminal.rs115
3 files changed, 127 insertions, 155 deletions
diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs
index c17f64cd..c9056109 100644
--- a/src/app/input/mod.rs
+++ b/src/app/input/mod.rs
@@ -286,31 +286,13 @@ impl App {
}
let handled_pane_double_click = self.handle_pane_double_click(mouse);
+ if !handled_pane_double_click {
+ self.focus_pane_before_mouse_press(mouse);
+ }
let previous_agent_panel_sort = self.state.agent_panel_sort;
let previous_settings_section = self.state.settings.section;
if !handled_pane_double_click {
- let right_button = matches!(
- mouse.kind,
- MouseEventKind::Down(MouseButton::Right)
- | MouseEventKind::Up(MouseButton::Right)
- | MouseEventKind::Drag(MouseButton::Right)
- );
- let intentional_pane_press = matches!(
- mouse.kind,
- MouseEventKind::Down(MouseButton::Left | MouseButton::Middle)
- );
- if !right_button
- && intentional_pane_press
- && matches!(self.state.mode, Mode::Terminal | Mode::Resize)
- {
- if let (Some(ws_idx), Some(info)) = (
- self.state.active,
- self.state.pane_at(mouse.column, mouse.row).cloned(),
- ) {
- self.focus_pane_internal_via_api(ws_idx, info.id);
- }
- }
if let Some(action) = self.state.handle_mouse(&mut self.terminal_runtimes, mouse) {
match action {
MouseAction::NewWorkspace => {
@@ -459,6 +441,31 @@ impl App {
}
}
+ fn focus_pane_before_mouse_press(&mut self, mouse: MouseEvent) {
+ if !matches!(self.state.mode, Mode::Terminal | Mode::Resize)
+ || !matches!(
+ mouse.kind,
+ MouseEventKind::Down(MouseButton::Left | MouseButton::Middle)
+ )
+ {
+ return;
+ }
+
+ let Some(pane_id) = self
+ .state
+ .pane_at(mouse.column, mouse.row)
+ .map(|info| info.id)
+ else {
+ return;
+ };
+ let Some(ws_idx) = self.state.active else {
+ return;
+ };
+
+ // Focus through the runtime API before an application can consume its press.
+ self.focus_pane_internal_via_api(ws_idx, pane_id);
+ }
+
fn handle_modified_url_click(&mut self, mouse: MouseEvent) -> bool {
if self.state.mode != Mode::Terminal
|| !matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left))
diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs
index 59dcf502..2283b754 100644
--- a/src/app/input/mouse.rs
+++ b/src/app/input/mouse.rs
@@ -627,13 +627,7 @@ impl AppState {
if self.forward_pane_mouse_button(terminal_runtimes, &info, mouse) {
self.selection = None;
self.selection_autoscroll = None;
- if let Some(ws_idx) = self.active {
- return Some(MouseAction::FocusPane {
- ws_idx,
- pane_id: info.id,
- });
- }
- return None;
+ return self.mouse_pane_focus_action(info.id);
}
let (row, col) = (
@@ -646,12 +640,7 @@ impl AppState {
col,
self.pane_scroll_metrics(terminal_runtimes, info.id),
));
- if let Some(ws_idx) = self.active {
- return Some(MouseAction::FocusPane {
- ws_idx,
- pane_id: info.id,
- });
- }
+ return self.mouse_pane_focus_action(info.id);
} else if let Some(info) = self.view.pane_infos.iter().find(|p| {
mouse.column >= p.rect.x
&& mouse.column < p.rect.x + p.rect.width
@@ -662,12 +651,7 @@ impl AppState {
if self.mode != Mode::Terminal {
self.mode = Mode::Terminal;
}
- if let Some(ws_idx) = self.active {
- return Some(MouseAction::FocusPane {
- ws_idx,
- pane_id: id,
- });
- }
+ return self.mouse_pane_focus_action(id);
}
}
@@ -1414,6 +1398,16 @@ impl AppState {
.or_else(|| self.pane_frame_at(col, row))
}
+ fn mouse_pane_focus_action(&self, pane_id: crate::layout::PaneId) -> Option<MouseAction> {
+ let ws_idx = self.active?;
+ (self
+ .workspaces
+ .get(ws_idx)
+ .and_then(|workspace| workspace.focused_pane_id())
+ != Some(pane_id))
+ .then_some(MouseAction::FocusPane { ws_idx, pane_id })
+ }
+
pub(crate) fn pane_info_by_id(&self, pane_id: crate::layout::PaneId) -> Option<&PaneInfo> {
self.view.pane_infos.iter().find(|info| info.id == pane_id)
}
@@ -2066,6 +2060,45 @@ mod tests {
}
#[tokio::test]
+ async fn captured_left_press_focuses_target_before_forwarding() {
+ let mut app = app_for_mouse_test();
+ let mut ws = Workspace::test_new("test");
+ let source = ws.tabs[0].root_pane;
+ let target = ws.test_split(Direction::Horizontal);
+ ws.tabs[0].layout.focus_pane(source);
+ app.state.workspaces = vec![ws];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
+ let info = app
+ .state
+ .pane_info_by_id(target)
+ .expect("target pane info")
+ .clone();
+ let (runtime, mut input_rx) =
+ crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes(
+ info.inner_rect.width,
+ info.inner_rect.height,
+ 0,
+ b"\x1b[?1002h\x1b[?1006h",
+ 4,
+ );
+ app.state.insert_test_runtime(target, runtime);
+
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ info.inner_rect.x + 1,
+ info.inner_rect.y + 1,
+ ));
+
+ assert_eq!(app.state.workspaces[0].focused_pane_id(), Some(target));
+ assert_eq!(
+ input_rx.try_recv().expect("forwarded captured left press"),
+ Bytes::from_static(b"\x1b[<0;2;2M")
+ );
+ }
+
+ #[tokio::test]
async fn pane_mouse_only_forwards_moved_events_for_any_motion_apps() {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
@@ -2336,6 +2369,52 @@ mod tests {
}
#[tokio::test]
+ async fn normal_right_click_keeps_focus_and_exposes_swap_for_reporting_pane() {
+ let mut app = app_for_mouse_test();
+ let mut ws = Workspace::test_new("test");
+ let source = ws.tabs[0].root_pane;
+ let target = ws.test_split(Direction::Horizontal);
+ ws.tabs[0].layout.focus_pane(source);
+ app.state.workspaces = vec![ws];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 100, 20));
+ let target_info = app
+ .state
+ .pane_info_by_id(target)
+ .expect("target pane info")
+ .clone();
+ let (runtime, mut input_rx) =
+ crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes(
+ target_info.inner_rect.width,
+ target_info.inner_rect.height,
+ 0,
+ b"\x1b[?1002h\x1b[?1006h",
+ 4,
+ );
+ app.state.insert_test_runtime(target, runtime);
+
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Right),
+ target_info.inner_rect.x,
+ target_info.inner_rect.y,
+ ));
+
+ assert!(input_rx.try_recv().is_err());
+ assert_eq!(app.state.workspaces[0].focused_pane_id(), Some(source));
+ let menu = app.state.context_menu.as_mut().expect("pane context menu");
+ assert!(matches!(
+ menu.kind,
+ ContextMenuKind::Pane {
+ pane_id,
+ source_pane_id: Some(source_pane_id),
+ ..
+ } if pane_id == target && source_pane_id == source
+ ));
+ assert!(menu.items().contains(&"Swap with focused pane"));
+ }
+
+ #[tokio::test]
async fn right_click_passthrough_requires_exact_modifier_match() {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
@@ -2357,6 +2436,7 @@ mod tests {
app.state.selected = 0;
app.state.mode = Mode::Terminal;
app.state.view.pane_infos = pane_infos;
+
app.state.right_click_passthrough_modifiers = Some(KeyModifiers::CONTROL);
let col = info.inner_rect.x + 2;
diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs
index c1bfac87..51a3899c 100644
--- a/src/app/input/terminal.rs
+++ b/src/app/input/terminal.rs
@@ -1052,121 +1052,6 @@ mod tests {
}
#[tokio::test]
- async fn clicking_unfocused_pane_with_mouse_reporting_focuses_it_via_left_button() {
- let mut app = app_for_mouse_test();
- let mut ws = Workspace::test_new("test");
- let first_pane = ws.tabs[0].root_pane;
- let second_pane = ws.test_split(ratatui::layout::Direction::Vertical);
-
- let terminal_area = Rect::new(26, 2, 80, 18);
- let pane_infos = ws.tabs[0].layout.panes(terminal_area);
- let first_info = pane_infos
- .iter()
- .find(|p| p.id == first_pane)
- .unwrap()
- .clone();
- let second_info = pane_infos
- .iter()
- .find(|p| p.id == second_pane)
- .unwrap()
- .clone();
-
- ws.insert_test_runtime(
- first_pane,
- crate::terminal::TerminalRuntime::test_with_screen_bytes(
- first_info.inner_rect.width.max(1),
- first_info.inner_rect.height.max(1),
- b"",
- ),
- );
- ws.insert_test_runtime(
- second_pane,
- crate::terminal::TerminalRuntime::test_with_screen_bytes(
- second_info.inner_rect.width.max(1),
- second_info.inner_rect.height.max(1),
- b"\x1b[?1002h",
- ),
- );
-
- ws.tabs[0].layout.focus_pane(first_pane);
-
- app.state.workspaces = vec![ws];
- app.state.active = Some(0);
- app.state.selected = 0;
- app.state.mode = Mode::Terminal;
- app.state.view.pane_infos = pane_infos;
-
- app.handle_mouse(mouse(
- MouseEventKind::Down(MouseButton::Left),
- second_info.inner_rect.x + 2,
- second_info.inner_rect.y + 2,
- ));
-
- assert_eq!(
- app.state.workspaces[0].tabs[0].layout.focused(),
- second_pane
- );
- assert_eq!(app.state.mode, Mode::Terminal);
- }
-
- #[tokio::test]
- async fn right_clicking_unfocused_mouse_reporting_pane_keeps_focus_for_context_menu() {
- let mut app = app_for_mouse_test();
- let mut ws = Workspace::test_new("test");
- let first_pane = ws.tabs[0].root_pane;
- let second_pane = ws.test_split(ratatui::layout::Direction::Vertical);
-
- let terminal_area = Rect::new(26, 2, 80, 18);
- let pane_infos = ws.tabs[0].layout.panes(terminal_area);
- let first_info = pane_infos
- .iter()
- .find(|p| p.id == first_pane)
- .unwrap()
- .clone();
- let second_info = pane_infos
- .iter()
- .find(|p| p.id == second_pane)
- .unwrap()
- .clone();
-
- ws.insert_test_runtime(
- first_pane,
- crate::terminal::TerminalRuntime::test_with_screen_bytes(
- first_info.inner_rect.width.max(1),
- first_info.inner_rect.height.max(1),
- b"",
- ),
- );
- ws.insert_test_runtime(
- second_pane,
- crate::terminal::TerminalRuntime::test_with_screen_bytes(
- second_info.inner_rect.width.max(1),
- second_info.inner_rect.height.max(1),
- b"\x1b[?1002h",
- ),
- );
-
- ws.tabs[0].layout.focus_pane(first_pane);
-
- app.state.workspaces = vec![ws];
- app.state.active = Some(0);
- app.state.selected = 0;
- app.state.mode = Mode::Terminal;
- app.state.view.pane_infos = pane_infos;
-
- app.handle_mouse(mouse(
- MouseEventKind::Down(MouseButton::Right),
- second_info.inner_rect.x + 2,
- second_info.inner_rect.y + 2,
- ));
-
- assert_eq!(app.state.workspaces[0].tabs[0].layout.focused(), first_pane);
- assert_eq!(app.state.mode, Mode::ContextMenu);
- let menu = app.state.context_menu.as_ref().expect("pane context menu");
- assert!(menu.items().contains(&"Swap with focused pane"));
- }
-
- #[tokio::test]
async fn terminal_direct_focus_pane_shortcut_switches_focus_without_leaving_terminal_mode() {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(