aboutsummaryrefslogtreecommitdiff
path: root/src/app/input
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/input')
-rw-r--r--src/app/input/modal.rs9
-rw-r--r--src/app/input/mouse.rs271
2 files changed, 277 insertions, 3 deletions
diff --git a/src/app/input/modal.rs b/src/app/input/modal.rs
index e85e9bb1..aedc3adb 100644
--- a/src/app/input/modal.rs
+++ b/src/app/input/modal.rs
@@ -1290,11 +1290,16 @@ mod tests {
},
x: 0,
y: 0,
- list: MenuListState::new(4),
+ list: MenuListState::new(0),
};
+ let idx = menu
+ .items()
+ .iter()
+ .position(|item| *item == "Close pane")
+ .expect("close pane item");
let mut terminal_runtimes = crate::terminal::TerminalRuntimeRegistry::new();
- apply_context_menu_action(&mut state, &mut terminal_runtimes, menu, 4);
+ apply_context_menu_action(&mut state, &mut terminal_runtimes, menu, idx);
assert_eq!(state.selected, 0);
assert_eq!(state.mode, Mode::ConfirmClose);
diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs
index b991828a..7bd68330 100644
--- a/src/app/input/mouse.rs
+++ b/src/app/input/mouse.rs
@@ -6,7 +6,8 @@ use tracing::warn;
use crate::{
app::state::{
AgentPanelScope, AppState, ContextMenuKind, ContextMenuState, DragState, DragTarget,
- MenuListState, Mode, TabPressState, ViewLayout, WorkspacePressState,
+ MenuListState, Mode, RightClickPassthroughGesture, TabPressState, ViewLayout,
+ WorkspacePressState,
},
layout::{PaneInfo, SplitBorder},
selection::Selection,
@@ -141,6 +142,10 @@ impl AppState {
&& mouse.row >= sidebar.y
&& mouse.row < sidebar.y + sidebar.height;
+ if self.handle_right_click_passthrough(terminal_runtimes, mouse, in_sidebar) {
+ return None;
+ }
+
if self.mode == Mode::OpenExistingWorktree {
match mouse.kind {
MouseEventKind::ScrollUp => {
@@ -1335,6 +1340,82 @@ impl AppState {
.and_then(crate::terminal::TerminalRuntime::scroll_metrics)
}
+ fn handle_right_click_passthrough(
+ &mut self,
+ terminal_runtimes: &TerminalRuntimeRegistry,
+ mouse: MouseEvent,
+ in_sidebar: bool,
+ ) -> bool {
+ if let Some(gesture) = self.right_click_passthrough.clone() {
+ match mouse.kind {
+ MouseEventKind::Drag(MouseButton::Right)
+ | MouseEventKind::Up(MouseButton::Right) => {
+ let forwarded_mouse =
+ self.strip_right_click_passthrough_modifiers(mouse, gesture.modifiers);
+ let _ = self.forward_pane_mouse_button(
+ terminal_runtimes,
+ &gesture.pane_info,
+ forwarded_mouse,
+ );
+ if matches!(mouse.kind, MouseEventKind::Up(MouseButton::Right)) {
+ self.right_click_passthrough = None;
+ }
+ return true;
+ }
+ _ => {
+ self.right_click_passthrough = None;
+ }
+ }
+ }
+
+ if self.mode != Mode::Terminal
+ || in_sidebar
+ || !matches!(mouse.kind, MouseEventKind::Down(MouseButton::Right))
+ {
+ return false;
+ }
+
+ let Some(modifiers) = self.right_click_passthrough_modifiers else {
+ return false;
+ };
+ if mouse.modifiers != modifiers {
+ return false;
+ }
+
+ let Some(info) = self.pane_at(mouse.column, mouse.row).cloned() else {
+ return false;
+ };
+
+ self.focus_pane(info.id);
+ let forwarded_mouse = self.strip_right_click_passthrough_modifiers(mouse, modifiers);
+ if !self.forward_pane_mouse_button(terminal_runtimes, &info, forwarded_mouse) {
+ return false;
+ }
+
+ self.selection = None;
+ self.selection_autoscroll = None;
+ self.workspace_press = None;
+ self.tab_press = None;
+ self.drag = None;
+ self.context_menu = None;
+ self.right_click_passthrough = Some(RightClickPassthroughGesture {
+ pane_info: info,
+ modifiers,
+ });
+ true
+ }
+
+ fn strip_right_click_passthrough_modifiers(
+ &self,
+ mouse: MouseEvent,
+ modifiers: crossterm::event::KeyModifiers,
+ ) -> MouseEvent {
+ MouseEvent {
+ modifiers: mouse.modifiers.difference(modifiers),
+ ..mouse
+ }
+ }
+
pub(super) fn handle_terminal_wheel(
&mut self,
terminal_runtimes: &TerminalRuntimeRegistry,
@@ -1640,6 +1721,194 @@ mod tests {
assert_eq!(metrics.offset_from_bottom, 7);
}
+ #[tokio::test]
+ async fn configured_right_click_passthrough_forwards_full_gesture_to_pane() {
+ let mut app = app_for_mouse_test();
+ let mut ws = Workspace::test_new("test");
+ let pane_id = ws.tabs[0].root_pane;
+ let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
+ let info = pane_infos[0].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,
+ );
+ ws.insert_test_runtime(pane_id, runtime);
+
+ 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.state.right_click_passthrough_modifiers = Some(KeyModifiers::CONTROL);
+
+ let col = info.inner_rect.x + 2;
+ let row = info.inner_rect.y + 3;
+ app.handle_mouse(MouseEvent {
+ modifiers: KeyModifiers::CONTROL,
+ ..mouse(MouseEventKind::Down(MouseButton::Right), col, row)
+ });
+ app.handle_mouse(MouseEvent {
+ modifiers: KeyModifiers::CONTROL,
+ ..mouse(MouseEventKind::Drag(MouseButton::Right), col + 1, row + 1)
+ });
+ app.handle_mouse(MouseEvent {
+ modifiers: KeyModifiers::CONTROL,
+ ..mouse(MouseEventKind::Up(MouseButton::Right), col + 1, row + 1)
+ });
+
+ assert_eq!(app.state.mode, Mode::Terminal);
+ assert!(app.state.context_menu.is_none());
+ assert!(app.state.right_click_passthrough.is_none());
+ assert_eq!(
+ input_rx.try_recv().expect("forwarded right mouse down"),
+ Bytes::from_static(b"\x1b[<2;3;4M")
+ );
+ assert_eq!(
+ input_rx.try_recv().expect("forwarded right mouse drag"),
+ Bytes::from_static(b"\x1b[<34;4;5M")
+ );
+ assert_eq!(
+ input_rx.try_recv().expect("forwarded right mouse up"),
+ Bytes::from_static(b"\x1b[<2;4;5m")
+ );
+ assert!(input_rx.try_recv().is_err());
+ }
+
+ #[tokio::test]
+ async fn unset_right_click_passthrough_keeps_modified_right_click_as_herdr_menu() {
+ let mut app = app_for_mouse_test();
+ let mut ws = Workspace::test_new("test");
+ let pane_id = ws.tabs[0].root_pane;
+ let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
+ let info = pane_infos[0].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,
+ );
+ ws.insert_test_runtime(pane_id, runtime);
+
+ 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.state.right_click_passthrough_modifiers = None;
+
+ app.handle_mouse(MouseEvent {
+ modifiers: KeyModifiers::CONTROL,
+ ..mouse(
+ MouseEventKind::Down(MouseButton::Right),
+ info.inner_rect.x + 2,
+ info.inner_rect.y + 3,
+ )
+ });
+
+ assert_eq!(app.state.mode, Mode::ContextMenu);
+ assert!(app.state.context_menu.is_some());
+ assert!(app.state.right_click_passthrough.is_none());
+ assert!(input_rx.try_recv().is_err());
+ }
+
+ #[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");
+ let pane_id = ws.tabs[0].root_pane;
+ let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
+ let info = pane_infos[0].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,
+ );
+ ws.insert_test_runtime(pane_id, runtime);
+
+ 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.state.right_click_passthrough_modifiers = Some(KeyModifiers::CONTROL);
+
+ let col = info.inner_rect.x + 2;
+ let row = info.inner_rect.y + 3;
+ app.handle_mouse(MouseEvent {
+ modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
+ ..mouse(MouseEventKind::Down(MouseButton::Right), col, row)
+ });
+
+ assert_eq!(app.state.mode, Mode::ContextMenu);
+ assert!(app.state.context_menu.is_some());
+ assert!(app.state.right_click_passthrough.is_none());
+ assert!(input_rx.try_recv().is_err());
+ }
+
+ #[tokio::test]
+ async fn right_click_passthrough_does_not_forward_pane_frame_clicks() {
+ let mut app = app_for_mouse_test();
+ let mut ws = Workspace::test_new("test");
+ let pane_id = ws.tabs[0].root_pane;
+ let other_pane = ws.test_split(Direction::Vertical);
+ app.state.workspaces = vec![ws];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.mode = Mode::Terminal;
+ app.state.right_click_passthrough_modifiers = Some(KeyModifiers::CONTROL);
+ crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
+
+ let info = app
+ .state
+ .view
+ .pane_infos
+ .iter()
+ .find(|info| info.id == pane_id)
+ .expect("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(pane_id, runtime);
+ app.state.insert_test_runtime(
+ other_pane,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(10, 5, b""),
+ );
+
+ assert!(app.state.pane_at(info.rect.x, info.rect.y).is_none());
+ assert!(app
+ .state
+ .pane_mouse_target(info.rect.x, info.rect.y)
+ .is_some());
+ app.handle_mouse(MouseEvent {
+ modifiers: KeyModifiers::CONTROL,
+ ..mouse(
+ MouseEventKind::Down(MouseButton::Right),
+ info.rect.x,
+ info.rect.y,
+ )
+ });
+
+ assert_eq!(app.state.mode, Mode::ContextMenu);
+ assert!(app.state.context_menu.is_some());
+ assert!(app.state.right_click_passthrough.is_none());
+ assert!(input_rx.try_recv().is_err());
+ }
+
fn sample_worktree_open_state() -> crate::app::state::WorktreeOpenState {
crate::app::state::WorktreeOpenState {
source_workspace_id: "source".into(),