diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index a37590639a..9df9ec5f14 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -11,26 +11,14 @@ use zellij_utils::{ actions::Action, cast_termwiz_key, config::Config, - mouse::{MouseButton, MouseEvent}, + mouse::{MouseEvent, MouseEventType}, options::Options, }, ipc::{ClientToServerMsg, ExitReason}, - termwiz::input::InputEvent, + position::Position, + termwiz::input::{InputEvent, Modifiers, MouseButtons, MouseEvent as TermwizMouseEvent}, }; -#[derive(Debug, Clone, Copy)] -enum HeldMouseButton { - Left, - Right, - Middle, -} - -impl Default for HeldMouseButton { - fn default() -> Self { - HeldMouseButton::Left - } -} - /// Handles the dispatching of [`Action`]s according to the current /// [`InputMode`], and keep tracks of the current [`InputMode`]. struct InputHandler { @@ -43,10 +31,92 @@ struct InputHandler { send_client_instructions: SenderWithContext, should_exit: bool, receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, - holding_mouse: Option, + mouse_old_event: MouseEvent, mouse_mode_active: bool, } +fn termwiz_mouse_convert(original_event: &mut MouseEvent, event: &TermwizMouseEvent) { + let button_bits = &event.mouse_buttons; + original_event.left = button_bits.contains(MouseButtons::LEFT); + original_event.right = button_bits.contains(MouseButtons::RIGHT); + original_event.middle = button_bits.contains(MouseButtons::MIDDLE); + original_event.wheel_up = button_bits.contains(MouseButtons::VERT_WHEEL) + && button_bits.contains(MouseButtons::WHEEL_POSITIVE); + original_event.wheel_down = button_bits.contains(MouseButtons::VERT_WHEEL) + && !button_bits.contains(MouseButtons::WHEEL_POSITIVE); + + let mods = &event.modifiers; + original_event.shift = mods.contains(Modifiers::SHIFT); + original_event.alt = mods.contains(Modifiers::ALT); + original_event.ctrl = mods.contains(Modifiers::CTRL); +} + +fn from_termwiz(old_event: &mut MouseEvent, event: TermwizMouseEvent) -> MouseEvent { + // We use the state of old_event vs new_event to determine if this + // event is a Press, Release, or Motion. This is an unfortunate + // side effect of the pre-SGR-encoded X10 mouse protocol design in + // which release events don't carry information about WHICH + // button(s) were released, so we have to maintain a wee bit of + // state in between events. + // + // Note that only Left, Right, and Middle are saved in between + // calls. WheelUp/WheelDown typically do not generate Release + // events. + let mut new_event = MouseEvent::new(); + termwiz_mouse_convert(&mut new_event, &event); + new_event.position = Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1)); + + if (new_event.left && !old_event.left) + || (new_event.right && !old_event.right) + || (new_event.middle && !old_event.middle) + || new_event.wheel_up + || new_event.wheel_down + { + // This is a mouse Press event. + new_event.event_type = MouseEventType::Press; + + // Hang onto the button state. + *old_event = new_event; + } else if event.mouse_buttons.is_empty() + && !old_event.left + && !old_event.right + && !old_event.middle + { + // This is a mouse Motion event (no buttons are down). + new_event.event_type = MouseEventType::Motion; + + // Hang onto the button state. + *old_event = new_event; + } else if event.mouse_buttons.is_empty() + && (old_event.left || old_event.right || old_event.middle) + { + // This is a mouse Release event. Note that we set + // old_event.{button} to false (to release), but set ONLY the + // new_event that were released to true before sending the + // event up. + if old_event.left { + old_event.left = false; + new_event.left = true; + } + if old_event.right { + old_event.right = false; + new_event.right = true; + } + if old_event.middle { + old_event.middle = false; + new_event.middle = true; + } + new_event.event_type = MouseEventType::Release; + } else { + // Dragging with some button down. Return it as a Motion + // event, and hang on to the button state. + new_event.event_type = MouseEventType::Motion; + *old_event = new_event; + } + + new_event +} + impl InputHandler { /// Returns a new [`InputHandler`] with the attributes specified as arguments. fn new( @@ -67,7 +137,7 @@ impl InputHandler { send_client_instructions, should_exit: false, receive_input_instructions, - holding_mouse: None, + mouse_old_event: MouseEvent::new(), mouse_mode_active: false, } } @@ -99,8 +169,7 @@ impl InputHandler { self.handle_key(&key, raw_bytes, false); }, InputEvent::Mouse(mouse_event) => { - let mouse_event = - zellij_utils::input::mouse::MouseEvent::from(mouse_event); + let mouse_event = from_termwiz(&mut self.mouse_old_event, mouse_event); self.handle_mouse_event(&mouse_event); }, InputEvent::Paste(pasted_text) => { @@ -215,70 +284,9 @@ impl InputHandler { } } fn handle_mouse_event(&mut self, mouse_event: &MouseEvent) { - match *mouse_event { - MouseEvent::Press(button, point) => match button { - MouseButton::WheelUp => { - self.dispatch_action(Action::ScrollUpAt(point), None); - }, - MouseButton::WheelDown => { - self.dispatch_action(Action::ScrollDownAt(point), None); - }, - MouseButton::Left => { - if self.holding_mouse.is_some() { - self.dispatch_action(Action::MouseHoldLeft(point), None); - } else { - self.dispatch_action(Action::LeftClick(point), None); - } - self.holding_mouse = Some(HeldMouseButton::Left); - }, - MouseButton::Right => { - if self.holding_mouse.is_some() { - self.dispatch_action(Action::MouseHoldRight(point), None); - } else { - self.dispatch_action(Action::RightClick(point), None); - } - self.holding_mouse = Some(HeldMouseButton::Right); - }, - MouseButton::Middle => { - if self.holding_mouse.is_some() { - self.dispatch_action(Action::MouseHoldMiddle(point), None); - } else { - self.dispatch_action(Action::MiddleClick(point), None); - } - self.holding_mouse = Some(HeldMouseButton::Middle); - }, - }, - MouseEvent::Release(point) => { - let button_released = self.holding_mouse.unwrap_or_default(); - match button_released { - HeldMouseButton::Left => { - self.dispatch_action(Action::LeftMouseRelease(point), None) - }, - HeldMouseButton::Right => { - self.dispatch_action(Action::RightMouseRelease(point), None) - }, - HeldMouseButton::Middle => { - self.dispatch_action(Action::MiddleMouseRelease(point), None) - }, - }; - self.holding_mouse = None; - }, - MouseEvent::Hold(point) => { - let button_held = self.holding_mouse.unwrap_or_default(); - match button_held { - HeldMouseButton::Left => { - self.dispatch_action(Action::MouseHoldLeft(point), None) - }, - HeldMouseButton::Right => { - self.dispatch_action(Action::MouseHoldRight(point), None) - }, - HeldMouseButton::Middle => { - self.dispatch_action(Action::MouseHoldMiddle(point), None) - }, - }; - self.holding_mouse = Some(button_held); - }, - } + // This dispatch handles all of the output(s) to terminal + // pane(s). + self.dispatch_action(Action::MouseEvent(*mouse_event), None); } /// Dispatches an [`Action`]. /// diff --git a/zellij-client/src/os_input_output.rs b/zellij-client/src/os_input_output.rs index 47dcafb091..41d03b0f72 100644 --- a/zellij-client/src/os_input_output.rs +++ b/zellij-client/src/os_input_output.rs @@ -22,8 +22,10 @@ use zellij_utils::{ const SIGWINCH_CB_THROTTLE_DURATION: time::Duration = time::Duration::from_millis(50); -const ENABLE_MOUSE_SUPPORT: &str = "\u{1b}[?1000h\u{1b}[?1002h\u{1b}[?1015h\u{1b}[?1006h"; -const DISABLE_MOUSE_SUPPORT: &str = "\u{1b}[?1006l\u{1b}[?1015l\u{1b}[?1002l\u{1b}[?1000l"; +const ENABLE_MOUSE_SUPPORT: &str = + "\u{1b}[?1000h\u{1b}[?1002h\u{1b}[?1003h\u{1b}[?1015h\u{1b}[?1006h"; +const DISABLE_MOUSE_SUPPORT: &str = + "\u{1b}[?1006l\u{1b}[?1015l\u{1b}[?1003l\u{1b}[?1002l\u{1b}[?1000l"; fn into_raw_mode(pid: RawFd) { let mut tio = termios::tcgetattr(pid).expect("could not get terminal attribute"); diff --git a/zellij-server/src/output/mod.rs b/zellij-server/src/output/mod.rs index 32192a203a..38a1585bd9 100644 --- a/zellij-server/src/output/mod.rs +++ b/zellij-server/src/output/mod.rs @@ -578,8 +578,12 @@ impl FloatingPanesStack { .with_context(err_context)?; let left_chunk_x = c_chunk_left_side; let right_chunk_x = pane_right_edge + 1; - let left_chunk = + let mut left_chunk = CharacterChunk::new(left_chunk_characters, left_chunk_x, c_chunk.y); + if !c_chunk.selection_and_colors.is_empty() { + left_chunk.selection_and_colors = c_chunk.selection_and_colors.clone(); + } + c_chunk.x = right_chunk_x; c_chunk.terminal_characters = right_chunk_characters; return Ok(Some(left_chunk)); diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index 27eda0964d..ff4ca26c4d 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -898,9 +898,11 @@ impl FloatingPanes { // true => handled, false => not handled (eg. no pane at this position) let show_panes = self.show_panes; if self.pane_being_moved_with_mouse.is_some() { - self.move_pane_to_position(&position); - self.set_force_render(); - return true; + if self.move_pane_to_position(&position) { + // pane was moved to a new position + self.set_force_render(); + return true; + } } else if let Some(pane) = self.get_pane_at_mut(&position, search_selectable) { let clicked_on_frame = pane.position_is_on_frame(&position); if show_panes && clicked_on_frame { diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index 62d2e53373..95c3be59ba 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -17,6 +17,7 @@ use std::{ use zellij_utils::{ consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE}, data::{Palette, PaletteColor}, + input::mouse::{MouseEvent, MouseEventType}, pane_size::SizeInPixels, position::Position, vte, @@ -385,6 +386,7 @@ pub enum MouseTracking { Off, Normal, ButtonEventTracking, + AnyEventTracking, } impl Default for MouseTracking { @@ -1743,9 +1745,11 @@ impl Grid { } pub fn update_selection(&mut self, to: &Position) { let old_selection = self.selection; - self.selection.to(*to); - self.update_selected_lines(&old_selection, &self.selection.clone()); - self.mark_for_rerender(); + if &old_selection.end != to { + self.selection.to(*to); + self.update_selected_lines(&old_selection, &self.selection.clone()); + self.mark_for_rerender(); + } } pub fn end_selection(&mut self, end: &Position) { @@ -1930,6 +1934,96 @@ impl Grid { } } } + fn mouse_buttons_value_x10(&self, event: &MouseEvent) -> u8 { + let mut value = 35; // Default to no buttons down. + if event.event_type == MouseEventType::Release { + return value; + } + if event.left { + value = 32; + } else if event.middle { + value = 33; + } else if event.right { + value = 34; + } else if event.wheel_up { + value = 68; + } else if event.wheel_down { + value = 69; + } + if event.event_type == MouseEventType::Motion { + value += 32; + } + if event.shift { + value |= 0x04; + } + if event.alt { + value |= 0x08; + } + if event.ctrl { + value |= 0x10; + } + value + } + fn mouse_buttons_value_sgr(&self, event: &MouseEvent) -> u8 { + let mut value = 3; // Default to no buttons down. + if event.left { + value = 0; + } else if event.middle { + value = 1; + } else if event.right { + value = 2; + } else if event.wheel_up { + value = 64; + } else if event.wheel_down { + value = 65; + } + if event.event_type == MouseEventType::Motion { + value += 32; + } + if event.shift { + value |= 0x04; + } + if event.alt { + value |= 0x08; + } + if event.ctrl { + value |= 0x10; + } + value + } + pub fn mouse_event_signal(&self, event: &MouseEvent) -> Option { + let emit = match (&self.mouse_tracking, event.event_type) { + (MouseTracking::Off, _) => false, + (MouseTracking::AnyEventTracking, _) => true, + (_, MouseEventType::Press | MouseEventType::Release) => true, + (MouseTracking::ButtonEventTracking, MouseEventType::Motion) => { + event.left | event.right | event.middle | event.wheel_up | event.wheel_down + }, + (_, _) => false, + }; + + match (emit, &self.mouse_mode) { + (true, MouseMode::NoEncoding | MouseMode::Utf8) => { + let mut msg: Vec = vec![27, b'[', b'M', self.mouse_buttons_value_x10(event)]; + msg.append(&mut utf8_mouse_coordinates( + event.position.column() + 1, + event.position.line() + 1, + )); + Some(String::from_utf8_lossy(&msg).into()) + }, + (true, MouseMode::Sgr) => Some(format!( + "\u{1b}[<{:?};{:?};{:?}{}", + self.mouse_buttons_value_sgr(event), + event.position.column() + 1, + event.position.line() + 1, + match event.event_type { + MouseEventType::Press | MouseEventType::Motion => 'M', + _ => 'm', + } + )), + (_, _) => None, + } + } pub fn mouse_left_click_signal(&self, position: &Position, is_held: bool) -> Option { let utf8_event = || -> Option { let button_code = if is_held { b'@' } else { b' ' }; @@ -1954,10 +2048,14 @@ impl Grid { (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::Normal) if !is_held => { utf8_event() }, - (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::ButtonEventTracking) => { - utf8_event() - }, - (MouseMode::Sgr, MouseTracking::ButtonEventTracking) => sgr_event(), + ( + MouseMode::NoEncoding | MouseMode::Utf8, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => utf8_event(), + ( + MouseMode::Sgr, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => sgr_event(), (MouseMode::Sgr, MouseTracking::Normal) if !is_held => sgr_event(), _ => None, } @@ -2007,10 +2105,14 @@ impl Grid { (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::Normal) if !is_held => { utf8_event() }, - (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::ButtonEventTracking) => { - utf8_event() - }, - (MouseMode::Sgr, MouseTracking::ButtonEventTracking) => sgr_event(), + ( + MouseMode::NoEncoding | MouseMode::Utf8, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => utf8_event(), + ( + MouseMode::Sgr, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => sgr_event(), (MouseMode::Sgr, MouseTracking::Normal) if !is_held => sgr_event(), _ => None, } @@ -2060,10 +2162,14 @@ impl Grid { (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::Normal) if !is_held => { utf8_event() }, - (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::ButtonEventTracking) => { - utf8_event() - }, - (MouseMode::Sgr, MouseTracking::ButtonEventTracking) => sgr_event(), + ( + MouseMode::NoEncoding | MouseMode::Utf8, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => utf8_event(), + ( + MouseMode::Sgr, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => sgr_event(), (MouseMode::Sgr, MouseTracking::Normal) if !is_held => sgr_event(), _ => None, } @@ -2605,7 +2711,7 @@ impl Perform for Grid { self.mouse_tracking = MouseTracking::Off; }, 1003 => { - // TBD: any-even mouse tracking + self.mouse_tracking = MouseTracking::Off; }, 1004 => { self.focus_event_tracking = false; @@ -2708,7 +2814,7 @@ impl Perform for Grid { self.mouse_tracking = MouseTracking::ButtonEventTracking; }, 1003 => { - // TBD: any-even mouse tracking + self.mouse_tracking = MouseTracking::AnyEventTracking; }, 1004 => { self.focus_event_tracking = true; diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 2e825ac84c..61587d5eb8 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -14,6 +14,7 @@ use std::fmt::Debug; use std::rc::Rc; use std::time::{self, Instant}; use zellij_utils::input::command::RunCommand; +use zellij_utils::input::mouse::{MouseEvent, MouseEventType}; use zellij_utils::pane_size::Offset; use zellij_utils::{ data::{ @@ -575,14 +576,16 @@ impl Pane for TerminalPane { if cursor_at_the_bottom { self.grid.scroll_up_one_line(); self.selection_scrolled_at = time::Instant::now(); + self.set_should_render(true); } else if cursor_at_the_top { self.grid.scroll_down_one_line(); self.selection_scrolled_at = time::Instant::now(); + self.set_should_render(true); } else if cursor_in_the_middle { + // here we'll only render if the selection was updated, and that'll be handled by the + // grid self.grid.update_selection(to); } - - self.set_should_render(true); } fn end_selection(&mut self, end: &Position, _client_id: ClientId) { @@ -633,6 +636,10 @@ impl Pane for TerminalPane { self.exclude_from_sync } + fn mouse_event(&self, event: &MouseEvent) -> Option { + self.grid.mouse_event_signal(event) + } + fn mouse_left_click(&self, position: &Position, is_held: bool) -> Option { self.grid.mouse_left_click_signal(position, is_held) } @@ -842,6 +849,20 @@ impl Pane for TerminalPane { } false } + fn intercept_mouse_event_on_frame(&mut self, event: &MouseEvent, client_id: ClientId) -> bool { + if self.position_is_on_frame(&event.position) { + let relative_position = self.relative_position(&event.position); + if let MouseEventType::Press = event.event_type { + if let Some(client_frame) = self.frame.get_mut(&client_id) { + if client_frame.clicked_on_pinned(relative_position) { + self.toggle_pinned(); + return true; + } + } + } + } + false + } fn reset_logical_position(&mut self) { self.geom.logical_position = None; } diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index d611fdcdb8..7c65491b78 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -47,20 +47,14 @@ pub(crate) fn route_action( let mut should_break = false; let err_context = || format!("failed to route action for client {client_id}"); - // forward the action to plugins unless it is a mousehold - // this is a bit of a hack around the unfortunate architecture we use with plugins - // this will change as soon as we refactor - match action { - Action::MouseHoldLeft(..) | Action::MouseHoldRight(..) => {}, - _ => { - senders - .send_to_plugin(PluginInstruction::Update(vec![( - None, - Some(client_id), - Event::InputReceived, - )])) - .with_context(err_context)?; - }, + if !action.is_mouse_motion() { + senders + .send_to_plugin(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::InputReceived, + )])) + .with_context(err_context)?; } match action { @@ -592,49 +586,9 @@ pub(crate) fn route_action( .with_context(err_context)?; should_break = true; }, - Action::LeftClick(point) => { - senders - .send_to_screen(ScreenInstruction::LeftClick(point, client_id)) - .with_context(err_context)?; - }, - Action::RightClick(point) => { - senders - .send_to_screen(ScreenInstruction::RightClick(point, client_id)) - .with_context(err_context)?; - }, - Action::MiddleClick(point) => { - senders - .send_to_screen(ScreenInstruction::MiddleClick(point, client_id)) - .with_context(err_context)?; - }, - Action::LeftMouseRelease(point) => { - senders - .send_to_screen(ScreenInstruction::LeftMouseRelease(point, client_id)) - .with_context(err_context)?; - }, - Action::RightMouseRelease(point) => { - senders - .send_to_screen(ScreenInstruction::RightMouseRelease(point, client_id)) - .with_context(err_context)?; - }, - Action::MiddleMouseRelease(point) => { - senders - .send_to_screen(ScreenInstruction::MiddleMouseRelease(point, client_id)) - .with_context(err_context)?; - }, - Action::MouseHoldLeft(point) => { - senders - .send_to_screen(ScreenInstruction::MouseHoldLeft(point, client_id)) - .with_context(err_context)?; - }, - Action::MouseHoldRight(point) => { - senders - .send_to_screen(ScreenInstruction::MouseHoldRight(point, client_id)) - .with_context(err_context)?; - }, - Action::MouseHoldMiddle(point) => { + Action::MouseEvent(event) => { senders - .send_to_screen(ScreenInstruction::MouseHoldMiddle(point, client_id)) + .send_to_screen(ScreenInstruction::MouseEvent(event, client_id)) .with_context(err_context)?; }, Action::Copy => { diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 7f1e044a2d..27de5fa0d3 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -15,6 +15,7 @@ use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; use zellij_utils::input::config::Config; use zellij_utils::input::keybinds::Keybinds; +use zellij_utils::input::mouse::MouseEvent; use zellij_utils::input::options::Clipboard; use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::{ @@ -249,15 +250,7 @@ pub enum ScreenInstruction { TerminalColorRegisters(Vec<(usize, String)>), ChangeMode(ModeInfo, ClientId), ChangeModeForAllClients(ModeInfo), - LeftClick(Position, ClientId), - RightClick(Position, ClientId), - MiddleClick(Position, ClientId), - LeftMouseRelease(Position, ClientId), - RightMouseRelease(Position, ClientId), - MiddleMouseRelease(Position, ClientId), - MouseHoldLeft(Position, ClientId), - MouseHoldRight(Position, ClientId), - MouseHoldMiddle(Position, ClientId), + MouseEvent(MouseEvent, ClientId), Copy(ClientId), AddClient( ClientId, @@ -522,15 +515,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ToggleActiveSyncTab(..) => ScreenContext::ToggleActiveSyncTab, ScreenInstruction::ScrollUpAt(..) => ScreenContext::ScrollUpAt, ScreenInstruction::ScrollDownAt(..) => ScreenContext::ScrollDownAt, - ScreenInstruction::LeftClick(..) => ScreenContext::LeftClick, - ScreenInstruction::RightClick(..) => ScreenContext::RightClick, - ScreenInstruction::MiddleClick(..) => ScreenContext::MiddleClick, - ScreenInstruction::LeftMouseRelease(..) => ScreenContext::LeftMouseRelease, - ScreenInstruction::RightMouseRelease(..) => ScreenContext::RightMouseRelease, - ScreenInstruction::MiddleMouseRelease(..) => ScreenContext::MiddleMouseRelease, - ScreenInstruction::MouseHoldLeft(..) => ScreenContext::MouseHoldLeft, - ScreenInstruction::MouseHoldRight(..) => ScreenContext::MouseHoldRight, - ScreenInstruction::MouseHoldMiddle(..) => ScreenContext::MouseHoldMiddle, + ScreenInstruction::MouseEvent(..) => ScreenContext::MouseEvent, ScreenInstruction::Copy(..) => ScreenContext::Copy, ScreenInstruction::ToggleTab(..) => ScreenContext::ToggleTab, ScreenInstruction::AddClient(..) => ScreenContext::AddClient, @@ -3753,56 +3738,13 @@ pub(crate) fn screen_thread_main( screen.render(None)?; screen.unblock_input()?; }, - ScreenInstruction::LeftClick(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_left_click(&point, client_id), ?); - screen.log_and_report_session_state()?; - screen.render(None)?; - screen.unblock_input()?; - }, - ScreenInstruction::RightClick(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_right_click(&point, client_id), ?); - screen.log_and_report_session_state()?; - screen.render(None)?; - screen.unblock_input()?; - }, - ScreenInstruction::MiddleClick(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_middle_click(&point, client_id), ?); - screen.log_and_report_session_state()?; - screen.render(None)?; - screen.unblock_input()?; - }, - ScreenInstruction::LeftMouseRelease(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_left_mouse_release(&point, client_id), ?); - screen.render(None)?; - screen.unblock_input()?; - }, - ScreenInstruction::RightMouseRelease(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_right_mouse_release(&point, client_id), ?); - screen.render(None)?; - }, - ScreenInstruction::MiddleMouseRelease(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_middle_mouse_release(&point, client_id), ?); - screen.render(None)?; - }, - ScreenInstruction::MouseHoldLeft(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_mouse_hold_left(&point, client_id), ?); - screen.render(None)?; - }, - ScreenInstruction::MouseHoldRight(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_mouse_hold_right(&point, client_id), ?); - screen.render(None)?; - }, - ScreenInstruction::MouseHoldMiddle(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_mouse_hold_middle(&point, client_id), ?); + ScreenInstruction::MouseEvent(event, client_id) => { + let state_changed = screen + .get_active_tab_mut(client_id) + .and_then(|tab| tab.handle_mouse_event(&event, client_id))?; + if state_changed { + screen.log_and_report_session_state()?; + } screen.render(None)?; }, ScreenInstruction::Copy(client_id) => { diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index bb3e8a7f1c..2c0fed76b6 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -16,6 +16,7 @@ use zellij_utils::data::{ }; use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; +use zellij_utils::input::mouse::{MouseEvent, MouseEventType}; use zellij_utils::position::{Column, Line}; use zellij_utils::{position::Position, serde}; @@ -168,7 +169,7 @@ pub(crate) struct Tab { draw_pane_frames: bool, auto_layout: bool, pending_vte_events: HashMap>, - pub selecting_with_mouse: bool, // this is only pub for the tests TODO: remove this once we combine write_text_to_clipboard with render + pub selecting_with_mouse_in_pane: Option, // this is only pub for the tests link_handler: Rc>, clipboard_provider: ClipboardProvider, // TODO: used only to focus the pane when the layout is loaded @@ -397,10 +398,19 @@ pub trait Pane { } false } + // TODO: get rid of this in favor of intercept_mouse_event_on_frame fn intercept_left_mouse_click(&mut self, _position: &Position, _client_id: ClientId) -> bool { let intercepted = false; intercepted } + fn intercept_mouse_event_on_frame( + &mut self, + _event: &MouseEvent, + _client_id: ClientId, + ) -> bool { + let intercepted = false; + intercepted + } fn store_pane_name(&mut self); fn load_pane_name(&mut self); fn set_borderless(&mut self, borderless: bool); @@ -410,6 +420,9 @@ pub trait Pane { // TODO: this should probably be merged with the mouse_right_click fn handle_right_click(&mut self, _to: &Position, _client_id: ClientId) {} + fn mouse_event(&self, _event: &MouseEvent) -> Option { + None + } fn mouse_left_click(&self, _position: &Position, _is_held: bool) -> Option { None } @@ -646,7 +659,7 @@ impl Tab { auto_layout, pending_vte_events: HashMap::new(), connected_clients, - selecting_with_mouse: false, + selecting_with_mouse_in_pane: None, link_handler: Rc::new(RefCell::new(LinkHandler::new())), clipboard_provider, focus_pane_id: None, @@ -3171,7 +3184,7 @@ impl Tab { point: &Position, lines: usize, client_id: ClientId, - ) -> Result<()> { + ) -> Result { let err_context = || { format!("failed to handle scrollwheel up at position {point:?} for client {client_id}") }; @@ -3192,7 +3205,7 @@ impl Tab { pane.scroll_up(lines, client_id); } } - Ok(()) + Ok(false) } pub fn handle_scrollwheel_down( @@ -3200,7 +3213,7 @@ impl Tab { point: &Position, lines: usize, client_id: ClientId, - ) -> Result<()> { + ) -> Result { let err_context = || { format!( "failed to handle scrollwheel down at position {point:?} for client {client_id}" @@ -3229,7 +3242,7 @@ impl Tab { } } } - Ok(()) + Ok(false) } fn get_pane_at( @@ -3297,139 +3310,404 @@ impl Tab { } } - pub fn handle_left_click(&mut self, position: &Position, client_id: ClientId) -> Result<()> { - let err_context = || { - format!( - "failed to handle mouse left click at position {position:?} for client {client_id}" - ) - }; + // returns true if the mouse event caused some sort of tab/pane state change that needs to be + // reported to plugins + pub fn handle_mouse_event(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); + + let active_pane_id = self + .get_active_pane_id(client_id) + .ok_or(anyhow!("Failed to find pane at position"))?; - let intercepted = self - .get_pane_at(position, false) + if event.left { + // left mouse click + let pane_id_at_position = self + .get_pane_at(&event.position, false) + .with_context(err_context)? + .ok_or_else(|| anyhow!("Failed to find pane at position"))? + .pid(); + match event.event_type { + MouseEventType::Press => { + if pane_id_at_position == active_pane_id { + self.handle_active_pane_left_mouse_press(event, client_id) + } else { + self.handle_inactive_pane_left_mouse_press(event, client_id) + } + }, + MouseEventType::Motion => self.handle_left_mouse_motion(event, client_id), + MouseEventType::Release => self.handle_left_mouse_release(event, client_id), + } + } else if event.wheel_up { + self.handle_scrollwheel_up(&event.position, 3, client_id) + } else if event.wheel_down { + self.handle_scrollwheel_down(&event.position, 3, client_id) + } else if event.right { + self.handle_right_click(&event, client_id) + } else if event.middle { + self.handle_middle_click(&event, client_id) + } else { + self.handle_mouse_no_click(&event, client_id) + } + } + fn write_mouse_event_to_active_pane( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result<()> { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); + let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); + if let Some(active_pane) = active_pane { + let relative_position = active_pane.relative_position(&event.position); + let mut pass_event = *event; + pass_event.position = relative_position; + if let Some(mouse_event) = active_pane.mouse_event(&pass_event) { + if !active_pane.position_is_on_frame(&event.position) { + self.write_to_active_terminal( + &None, + mouse_event.into_bytes(), + false, + client_id, + ) + .with_context(err_context)?; + } + } + } + Ok(()) + } + // returns true if the mouse event caused some sort of tab/pane state change that needs to be + // reported to plugins + fn handle_active_pane_left_mouse_press( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); + let floating_panes_are_visible = self.floating_panes.panes_are_visible(); + let pane_at_position = self + .get_pane_at(&event.position, false) .with_context(err_context)? - .map(|pane| pane.intercept_left_mouse_click(&position, client_id)) - .unwrap_or(false); - if intercepted { - self.set_force_render(); - return Ok(()); + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; + if pane_at_position.position_is_on_frame(&event.position) { + // intercept frame click eg. for toggling pinned + let intercepted = pane_at_position.intercept_mouse_event_on_frame(&event, client_id); + if intercepted { + self.set_force_render(); + return Ok(true); + } else if floating_panes_are_visible { + // start moving if floating pane + let search_selectable = false; + if self + .floating_panes + .move_pane_with_mouse(event.position, search_selectable) + { + self.swap_layouts.set_is_floating_damaged(); + self.set_force_render(); + return Ok(true); + } + } + } else { + let relative_position = pane_at_position.relative_position(&event.position); + if let Some(mouse_event) = pane_at_position.mouse_left_click(&relative_position, false) + { + // send click to terminal if needed (eg. the program inside + // requested mouse mode) + if !pane_at_position.position_is_on_frame(&event.position) { + self.write_to_active_terminal( + &None, + mouse_event.into_bytes(), + false, + client_id, + ) + .with_context(err_context)?; + } + } else { + // start selection for copy/paste + pane_at_position.start_selection(&relative_position, client_id); + if let PaneId::Terminal(_) = pane_at_position.pid() { + self.selecting_with_mouse_in_pane = Some(pane_at_position.pid()); + } + } } - + Ok(false) + } + fn handle_inactive_pane_left_mouse_press( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); if !self.floating_panes.panes_are_visible() { let search_selectable = false; if let Ok(Some(pane_id)) = self .floating_panes - .get_pinned_pane_id_at(position, search_selectable) + .get_pinned_pane_id_at(&event.position, search_selectable) { // here, the floating panes are not visible, but there is a pinned pane (always // visible) that has been clicked on - so we make the entire surface visible and // focus it self.show_floating_panes(); self.floating_panes.focus_pane(pane_id, client_id); - return Ok(()); + return Ok(true); } } - - self.focus_pane_at(position, client_id) + let active_pane_id_before_click = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; + self.focus_pane_at(&event.position, client_id) .with_context(err_context)?; - let search_selectable = false; - if self.floating_panes.panes_are_visible() - && self + if let Some(pane_at_position) = self.unselectable_pane_at_position(&event.position) { + let relative_position = pane_at_position.relative_position(&event.position); + // we use start_selection here because it has a client_id, + // ideally we should add client_id to mouse_left_click and others, but this should be + // dealt with as part of the trait removal refactoring + pane_at_position.start_selection(&relative_position, client_id); + } + + if self.floating_panes.panes_are_visible() { + let search_selectable = false; + // we do this because this might be the beginning of the user dragging a pane + // that was not focused + // TODO: rename move_pane_with_mouse to "start_moving_pane_with_mouse"? + return Ok(self .floating_panes - .move_pane_with_mouse(*position, search_selectable) + .move_pane_with_mouse(event.position, search_selectable)); + } + let active_pane_id_after_click = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; + if active_pane_id_before_click != active_pane_id_after_click { + // focus changed, need to report it + Ok(true) + } else { + Ok(false) + } + } + fn handle_left_mouse_motion( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); + let pane_is_being_moved_with_mouse = self.floating_panes.pane_is_being_moved_with_mouse(); + let active_pane_id = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; + if pane_is_being_moved_with_mouse { + let search_selectable = false; + if self + .floating_panes + .move_pane_with_mouse(event.position, search_selectable) + { + self.swap_layouts.set_is_floating_damaged(); + self.set_force_render(); + return Ok(true); + } + } else if let Some(pane_id_with_selection) = self.selecting_with_mouse_in_pane { + if let Some(pane_with_selection) = self.get_pane_with_id_mut(pane_id_with_selection) { + let relative_position = pane_with_selection.relative_position(&event.position); + pane_with_selection.update_selection(&relative_position, client_id); + } + } else { + let pane_at_position = self + .get_pane_at(&event.position, false) + .with_context(err_context)? + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; + if pane_at_position.pid() == active_pane_id { + self.write_mouse_event_to_active_pane(event, client_id)?; + } + } + Ok(false) + } + fn handle_left_mouse_release( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); + let floating_panes_are_visible = self.floating_panes.panes_are_visible(); + let copy_on_release = self.copy_on_select; + + if let Some(pane_with_selection) = self + .selecting_with_mouse_in_pane + .and_then(|p_id| self.get_pane_with_id_mut(p_id)) { - self.swap_layouts.set_is_floating_damaged(); - self.set_force_render(); - return Ok(()); + let mut relative_position = pane_with_selection.relative_position(&event.position); + + relative_position.change_column( + (relative_position.column()) + .max(0) + .min(pane_with_selection.get_content_columns()), + ); + + relative_position.change_line( + (relative_position.line()) + .max(0) + .min(pane_with_selection.get_content_rows() as isize), + ); + + if let Some(mouse_event) = + pane_with_selection.mouse_left_click_release(&relative_position) + { + self.write_to_active_terminal(&None, mouse_event.into_bytes(), false, client_id) + .with_context(err_context)?; + } else { + let relative_position = pane_with_selection.relative_position(&event.position); + pane_with_selection.end_selection(&relative_position, client_id); + if let PaneId::Terminal(_) = pane_with_selection.pid() { + if copy_on_release { + let selected_text = pane_with_selection.get_selected_text(); + pane_with_selection.reset_selection(); + + if let Some(selected_text) = selected_text { + self.write_selection_to_clipboard(&selected_text) + .with_context(err_context)?; + } + } + } + + self.selecting_with_mouse_in_pane = None; + } + } else if floating_panes_are_visible && self.floating_panes.pane_is_being_moved_with_mouse() + { + self.floating_panes + .stop_moving_pane_with_mouse(event.position); + } else { + self.write_mouse_event_to_active_pane(event, client_id)?; } + Ok(false) + } + + pub fn handle_right_click(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + let err_context = || format!("failed to handle mouse right click for client {client_id}"); + + let absolute_position = event.position; + let active_pane_id = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; if let Some(pane) = self - .get_pane_at(position, false) + .get_pane_at(&absolute_position, false) .with_context(err_context)? { - let relative_position = pane.relative_position(position); - if let Some(mouse_event) = pane.mouse_left_click(&relative_position, false) { - if !pane.position_is_on_frame(position) { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; - } - } else { - pane.start_selection(&relative_position, client_id); - if let PaneId::Terminal(_) = pane.pid() { - self.selecting_with_mouse = true; + if pane.pid() == active_pane_id { + let relative_position = pane.relative_position(&absolute_position); + let mut event_for_pane = event.clone(); + event_for_pane.position = relative_position; + if let Some(mouse_event) = pane.mouse_event(&event_for_pane) { + if !pane.position_is_on_frame(&absolute_position) { + self.write_to_active_terminal( + &None, + mouse_event.into_bytes(), + false, + client_id, + ) + .with_context(err_context)?; + } + } else { + pane.handle_right_click(&relative_position, client_id); } } }; - Ok(()) + Ok(false) } - pub fn handle_right_click(&mut self, position: &Position, client_id: ClientId) -> Result<()> { - let err_context = || { - format!( - "failed to handle mouse right click at position {position:?} for client {client_id}" - ) - }; + fn handle_middle_click(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + let err_context = || format!("failed to handle mouse middle click for client {client_id}"); + let absolute_position = event.position; - self.focus_pane_at(position, client_id) - .with_context(err_context)?; + let active_pane_id = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; if let Some(pane) = self - .get_pane_at(position, false) + .get_pane_at(&absolute_position, false) .with_context(err_context)? { - let relative_position = pane.relative_position(position); - if let Some(mouse_event) = pane.mouse_right_click(&relative_position, false) { - if !pane.position_is_on_frame(position) { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; + if pane.pid() == active_pane_id { + let relative_position = pane.relative_position(&absolute_position); + let mut event_for_pane = event.clone(); + event_for_pane.position = relative_position; + if let Some(mouse_event) = pane.mouse_event(&event_for_pane) { + if !pane.position_is_on_frame(&absolute_position) { + self.write_to_active_terminal( + &None, + mouse_event.into_bytes(), + false, + client_id, + ) + .with_context(err_context)?; + } } - } else { - pane.handle_right_click(&relative_position, client_id); } }; - Ok(()) + Ok(false) } - pub fn handle_middle_click(&mut self, position: &Position, client_id: ClientId) -> Result<()> { - let err_context = || { - format!( - "failed to handle mouse middle click at position {position:?} for client {client_id}" - ) - }; + fn handle_mouse_no_click(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + let err_context = || format!("failed to handle mouse no click for client {client_id}"); + let absolute_position = event.position; - self.focus_pane_at(position, client_id) - .with_context(err_context)?; + let active_pane_id = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; if let Some(pane) = self - .get_pane_at(position, false) + .get_pane_at(&absolute_position, false) .with_context(err_context)? { - let relative_position = pane.relative_position(position); - if let Some(mouse_event) = pane.mouse_middle_click(&relative_position, false) { - if !pane.position_is_on_frame(position) { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; + if pane.pid() == active_pane_id { + let relative_position = pane.relative_position(&absolute_position); + let mut event_for_pane = event.clone(); + event_for_pane.position = relative_position; + if let Some(mouse_event) = pane.mouse_event(&event_for_pane) { + if !pane.position_is_on_frame(&absolute_position) { + self.write_to_active_terminal( + &None, + mouse_event.into_bytes(), + false, + client_id, + ) + .with_context(err_context)?; + } } } }; - Ok(()) + Ok(false) } + fn unselectable_pane_at_position(&mut self, point: &Position) -> Option<&mut Box> { + // the repetition in this function is to appease the borrow checker, I don't like it either + let floating_panes_are_visible = self.floating_panes.panes_are_visible(); + if floating_panes_are_visible { + if let Ok(Some(clicked_pane_id)) = self.floating_panes.get_pane_id_at(point, true) { + if let Some(pane) = self.floating_panes.get_pane_mut(clicked_pane_id) { + if !pane.selectable() { + return Some(pane); + } + } + } else if let Ok(Some(clicked_pane_id)) = self.get_pane_id_at(point, false) { + if let Some(pane) = self.tiled_panes.get_pane_mut(clicked_pane_id) { + if !pane.selectable() { + return Some(pane); + } + } + } + } else if let Ok(Some(clicked_pane_id)) = self.get_pane_id_at(point, false) { + if let Some(pane) = self.tiled_panes.get_pane_mut(clicked_pane_id) { + if !pane.selectable() { + return Some(pane); + } + } + } + None + } fn focus_pane_at(&mut self, point: &Position, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to focus pane at position {point:?} for client {client_id}"); @@ -3489,7 +3767,7 @@ impl Tab { Ok(()) } - pub fn handle_middle_mouse_release( + fn handle_middle_mouse_release( &mut self, position: &Position, client_id: ClientId, @@ -3521,239 +3799,6 @@ impl Tab { } Ok(()) } - - pub fn handle_left_mouse_release( - &mut self, - position: &Position, - client_id: ClientId, - ) -> Result<()> { - let err_context = || { - format!("failed to handle left mouse release at position {position:?} for client {client_id}") - }; - - self.last_mouse_hold_position = None; - - if self.floating_panes.panes_are_visible() - && self.floating_panes.pane_is_being_moved_with_mouse() - { - self.floating_panes.stop_moving_pane_with_mouse(*position); - return Ok(()); - } - - // read these here to avoid use of borrowed `*self`, since we are holding active_pane - let selecting = self.selecting_with_mouse; - let copy_on_release = self.copy_on_select; - let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); - - if let Some(active_pane) = active_pane { - let mut relative_position = active_pane.relative_position(position); - relative_position.change_column( - (relative_position.column()) - .max(0) - .min(active_pane.get_content_columns()), - ); - - relative_position.change_line( - (relative_position.line()) - .max(0) - .min(active_pane.get_content_rows() as isize), - ); - - if let Some(mouse_event) = active_pane.mouse_left_click_release(&relative_position) { - self.write_to_active_terminal(&None, mouse_event.into_bytes(), false, client_id) - .with_context(err_context)?; - } else { - let relative_position = active_pane.relative_position(position); - if let PaneId::Terminal(_) = active_pane.pid() { - if selecting { - active_pane.end_selection(&relative_position, client_id); - if copy_on_release { - let selected_text = active_pane.get_selected_text(); - active_pane.reset_selection(); - - if let Some(selected_text) = selected_text { - self.write_selection_to_clipboard(&selected_text) - .with_context(err_context)?; - } - } - } - } else { - // notify the release event to a plugin pane, should be renamed - active_pane.end_selection(&relative_position, client_id); - } - - self.selecting_with_mouse = false; - } - } - Ok(()) - } - - pub fn handle_mouse_hold_left( - &mut self, - position_on_screen: &Position, - client_id: ClientId, - ) -> Result { - let err_context = || { - format!("failed to handle left mouse hold at position {position_on_screen:?} for client {client_id}") - }; - - // return value indicates whether we should trigger a render - // determine if event is repeated to enable smooth scrolling - let is_repeated = if let Some(last_position) = self.last_mouse_hold_position { - position_on_screen == &last_position - } else { - false - }; - self.last_mouse_hold_position = Some(*position_on_screen); - - let search_selectable = true; - - if self.floating_panes.panes_are_visible() - && self.floating_panes.pane_is_being_moved_with_mouse() - && self - .floating_panes - .move_pane_with_mouse(*position_on_screen, search_selectable) - { - self.swap_layouts.set_is_floating_damaged(); - self.set_force_render(); - return Ok(!is_repeated); // we don't need to re-render in this case if the pane did not move - // return; - } - - let selecting = self.selecting_with_mouse; - let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); - - if let Some(active_pane) = active_pane { - let mut relative_position = active_pane.relative_position(position_on_screen); - if !is_repeated { - // ensure that coordinates are valid - relative_position.change_column( - (relative_position.column()) - .max(0) - .min(active_pane.get_content_columns()), - ); - - relative_position.change_line( - (relative_position.line()) - .max(0) - .min(active_pane.get_content_rows() as isize), - ); - if let Some(mouse_event) = active_pane.mouse_left_click(&relative_position, true) { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; - return Ok(true); // we need to re-render in this case so the selection disappears - } - } else if selecting { - active_pane.update_selection(&relative_position, client_id); - return Ok(true); // we need to re-render in this case so the selection is updated - } - } - Ok(false) // we shouldn't even get here, but might as well not needlessly render if we do - } - - pub fn handle_mouse_hold_right( - &mut self, - position_on_screen: &Position, - client_id: ClientId, - ) -> Result { - let err_context = || { - format!("failed to handle left mouse hold at position {position_on_screen:?} for client {client_id}") - }; - - // return value indicates whether we should trigger a render - // determine if event is repeated to enable smooth scrolling - let is_repeated = if let Some(last_position) = self.last_mouse_hold_position { - position_on_screen == &last_position - } else { - false - }; - self.last_mouse_hold_position = Some(*position_on_screen); - - let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); - - if let Some(active_pane) = active_pane { - let mut relative_position = active_pane.relative_position(position_on_screen); - if !is_repeated { - relative_position.change_column( - (relative_position.column()) - .max(0) - .min(active_pane.get_content_columns()), - ); - - relative_position.change_line( - (relative_position.line()) - .max(0) - .min(active_pane.get_content_rows() as isize), - ); - if let Some(mouse_event) = active_pane.mouse_right_click(&relative_position, true) { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; - return Ok(true); // we need to re-render in this case so the selection disappears - } - } - } - Ok(false) // we shouldn't even get here, but might as well not needlessly render if we do - } - - pub fn handle_mouse_hold_middle( - &mut self, - position_on_screen: &Position, - client_id: ClientId, - ) -> Result { - let err_context = || { - format!("failed to handle left mouse hold at position {position_on_screen:?} for client {client_id}") - }; - // return value indicates whether we should trigger a render - // determine if event is repeated to enable smooth scrolling - let is_repeated = if let Some(last_position) = self.last_mouse_hold_position { - position_on_screen == &last_position - } else { - false - }; - self.last_mouse_hold_position = Some(*position_on_screen); - - let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); - - if let Some(active_pane) = active_pane { - let mut relative_position = active_pane.relative_position(position_on_screen); - if !is_repeated { - relative_position.change_column( - (relative_position.column()) - .max(0) - .min(active_pane.get_content_columns()), - ); - - relative_position.change_line( - (relative_position.line()) - .max(0) - .min(active_pane.get_content_rows() as isize), - ); - if let Some(mouse_event) = active_pane.mouse_middle_click(&relative_position, true) - { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; - return Ok(true); // we need to re-render in this case so the selection disappears - } - } - } - Ok(false) // we shouldn't even get here, but might as well not needlessly render if we do - } - pub fn copy_selection(&self, client_id: ClientId) -> Result<()> { let selected_text = self .get_active_pane(client_id) diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 76b4474b29..74c2d4d99a 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -23,6 +23,7 @@ use zellij_utils::input::layout::{ FloatingPaneLayout, Layout, PluginUserConfiguration, RunPluginLocation, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }; +use zellij_utils::input::mouse::MouseEvent; use zellij_utils::input::plugins::PluginTag; use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::{Size, SizeInPixels}; @@ -1728,10 +1729,16 @@ fn move_floating_pane_focus_with_mouse() { .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); - tab.handle_left_click(&Position::new(9, 71), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(9, 71), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(9, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(9, 71)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), @@ -1826,10 +1833,16 @@ fn move_pane_focus_with_mouse_to_non_floating_pane() { .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); - tab.handle_left_click(&Position::new(4, 71), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(4, 71), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(4, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(4, 71)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), @@ -1924,10 +1937,16 @@ fn drag_pane_with_mouse() { .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), @@ -2022,16 +2041,22 @@ fn mark_text_inside_floating_pane() { .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); - tab.handle_left_click(&Position::new(6, 30), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(6, 30)), + client_id, + ) + .unwrap(); assert!( - tab.selecting_with_mouse, + tab.selecting_with_mouse_in_pane.is_some(), "started selecting with mouse on click" ); - tab.handle_left_mouse_release(&Position::new(5, 15), client_id) - .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(5, 15)), + client_id, + ) + .unwrap(); assert!( - !tab.selecting_with_mouse, + tab.selecting_with_mouse_in_pane.is_none(), "stopped selecting with mouse on release" ); tab.render(&mut output).unwrap(); @@ -2630,10 +2655,16 @@ fn move_floating_pane_with_sixel_image() { .unwrap(); let fixture = read_fixture("sixel-image-500px.six"); tab.handle_pty_bytes(2, fixture).unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let snapshot = take_snapshot_with_sixel( @@ -2668,10 +2699,16 @@ fn floating_pane_above_sixel_image() { .unwrap(); let fixture = read_fixture("sixel-image-500px.six"); tab.handle_pty_bytes(1, fixture).unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let snapshot = take_snapshot_with_sixel( @@ -3048,22 +3085,43 @@ fn pane_in_sgr_button_event_tracking_mouse_mode() { let sgr_mouse_mode_any_button = String::from("\u{1b}[?1002;1006h"); // button event tracking (1002) with SGR encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_left(&Position::new(9, 72), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_right_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_right(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_right_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_middle_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_middle(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) @@ -3110,22 +3168,43 @@ fn pane_in_sgr_normal_event_tracking_mouse_mode() { let sgr_mouse_mode_any_button = String::from("\u{1b}[?1000;1006h"); // normal event tracking (1000) with sgr encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_left(&Position::new(9, 72), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_right_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_right(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_right_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_middle_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_middle(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) @@ -3154,7 +3233,7 @@ fn pane_in_sgr_normal_event_tracking_mouse_mode() { } #[test] -fn pane_in_utf8_button_event_tracking_mouse_mode() { +fn pane_in_sgr_any_event_tracking_mouse_mode() { let size = Size { cols: 121, rows: 20, @@ -3169,25 +3248,137 @@ fn pane_in_utf8_button_event_tracking_mouse_mode() { ); pty_instruction_bus.start(); - let sgr_mouse_mode_any_button = String::from("\u{1b}[?1002;1005h"); // button event tracking (1002) with utf8 encoding (1005) + let sgr_mouse_mode_any_button = String::from("\u{1b}[?1003;1006h"); // any event tracking (1003) with SGR encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) + // TODO: CONTINUE HERE - make sure these pass, then add some button-less motions and see what + // we track them + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_right_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_mouse_hold_left(&Position::new(9, 72), client_id) + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) + tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) .unwrap(); - tab.handle_right_click(&Position::new(5, 71), client_id) + tab.handle_scrollwheel_down(&Position::new(5, 71), 1, client_id) .unwrap(); - tab.handle_mouse_hold_right(&Position::new(9, 72), client_id) + tab.handle_mouse_event( + &MouseEvent::new_buttonless_motion(Position::new(9, 72)), + client_id, + ) + .unwrap(); + + pty_instruction_bus.exit(); + + assert_eq!( + pty_instruction_bus.clone_output(), + vec![ + "\u{1b}[<0;71;5M".to_string(), // SGR left click + "\u{1b}[<32;72;9M".to_string(), // SGR left click (hold) + "\u{1b}[<0;75;7m".to_string(), // SGR left button release + "\u{1b}[<2;71;5M".to_string(), // SGR right click + "\u{1b}[<34;72;9M".to_string(), // SGR right click (hold) + "\u{1b}[<2;75;7m".to_string(), // SGR right button release + "\u{1b}[<1;71;5M".to_string(), // SGR middle click + "\u{1b}[<33;72;9M".to_string(), // SGR middle click (hold) + "\u{1b}[<1;75;7m".to_string(), // SGR middle button release + "\u{1b}[<64;71;5M".to_string(), // SGR scroll up + "\u{1b}[<65;71;5M".to_string(), // SGR scroll down + "\u{1b}[<35;72;9M".to_string(), // SGR buttonless motion + ] + ); +} + +#[test] +fn pane_in_utf8_button_event_tracking_mouse_mode() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + + let mut pty_instruction_bus = MockPtyInstructionBus::new(); + let mut tab = create_new_tab_with_mock_pty_writer( + size, + ModeInfo::default(), + pty_instruction_bus.pty_write_sender(), + ); + pty_instruction_bus.start(); + + let sgr_mouse_mode_any_button = String::from("\u{1b}[?1002;1005h"); // button event tracking (1002) with utf8 encoding (1005) + tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_right_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_middle_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_middle(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) @@ -3234,22 +3425,43 @@ fn pane_in_utf8_normal_event_tracking_mouse_mode() { let sgr_mouse_mode_any_button = String::from("\u{1b}[?1000;1005h"); // normal event tracking (1000) with sgr encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_left(&Position::new(9, 72), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_right_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_right(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_right_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_middle_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_middle(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) @@ -3277,6 +3489,95 @@ fn pane_in_utf8_normal_event_tracking_mouse_mode() { ); } +#[test] +fn pane_in_utf8_any_event_tracking_mouse_mode() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + + let mut pty_instruction_bus = MockPtyInstructionBus::new(); + let mut tab = create_new_tab_with_mock_pty_writer( + size, + ModeInfo::default(), + pty_instruction_bus.pty_write_sender(), + ); + pty_instruction_bus.start(); + + let sgr_mouse_mode_any_button = String::from("\u{1b}[?1003;1005h"); // any event tracking (1002) with utf8 encoding (1005) + tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_right_mouse_release(&Position::new(7, 75), client_id) + .unwrap(); + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) + .unwrap(); + tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) + .unwrap(); + tab.handle_scrollwheel_down(&Position::new(5, 71), 1, client_id) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_buttonless_motion(Position::new(9, 72)), + client_id, + ) + .unwrap(); + + pty_instruction_bus.exit(); + + assert_eq!( + pty_instruction_bus.clone_output(), + vec![ + "\u{1b}[M g%".to_string(), // utf8 left click + "\u{1b}[M@h)".to_string(), // utf8 left click (hold) + "\u{1b}[M#k'".to_string(), // utf8 left button release + "\u{1b}[M\"g%".to_string(), // utf8 right click + "\u{1b}[MBh)".to_string(), // utf8 right click (hold) + "\u{1b}[M#k'".to_string(), // utf8 right button release + "\u{1b}[M!g%".to_string(), // utf8 middle click + "\u{1b}[MAh)".to_string(), // utf8 middle click (hold) + "\u{1b}[M#k'".to_string(), // utf8 middle click release + "\u{1b}[M`g%".to_string(), // utf8 scroll up + "\u{1b}[Mag%".to_string(), // utf8 scroll down + "\u{1b}[MCh)".to_string(), // urf8 buttonless motion + ] + ); +} + #[test] fn tab_with_basic_layout() { let layout = r#" @@ -5941,8 +6242,11 @@ fn focus_stacked_pane_over_flexible_pane_with_the_mouse() { Some(client_id), ) .unwrap(); - tab.handle_left_click(&Position::new(1, 71), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(1, 71)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), @@ -6040,10 +6344,16 @@ fn focus_stacked_pane_under_flexible_pane_with_the_mouse() { Some(client_id), ) .unwrap(); - tab.handle_left_click(&Position::new(1, 71), client_id) - .unwrap(); - tab.handle_left_click(&Position::new(9, 71), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(1, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(9, 71)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), @@ -6141,10 +6451,16 @@ fn close_stacked_pane_with_previously_focused_other_pane() { Some(client_id), ) .unwrap(); - tab.handle_left_click(&Position::new(2, 71), client_id) - .unwrap(); - tab.handle_left_click(&Position::new(1, 71), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(2, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(1, 71)), + client_id, + ) + .unwrap(); tab.close_pane(PaneId::Terminal(4), false); tab.render(&mut output).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs index 87ca37dd4c..a93dde4637 100644 --- a/zellij-utils/assets/prost/api.action.rs +++ b/zellij-utils/assets/prost/api.action.rs @@ -5,7 +5,7 @@ pub struct Action { pub name: i32, #[prost( oneof = "action::OptionalPayload", - tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48" + tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49" )] pub optional_payload: ::core::option::Option, } @@ -68,12 +68,6 @@ pub mod action { RightMouseReleasePayload(super::Position), #[prost(message, tag = "28")] MiddleMouseReleasePayload(super::Position), - #[prost(message, tag = "29")] - MouseHoldLeftPayload(super::Position), - #[prost(message, tag = "30")] - MouseHoldRightPayload(super::Position), - #[prost(message, tag = "31")] - MouseHoldMiddlePayload(super::Position), #[prost(bytes, tag = "32")] SearchInputPayload(::prost::alloc::vec::Vec), #[prost(enumeration = "super::SearchDirection", tag = "33")] @@ -108,6 +102,8 @@ pub mod action { MessagePayload(super::CliPipePayload), #[prost(enumeration = "super::MoveTabDirection", tag = "48")] MoveTabPayload(i32), + #[prost(message, tag = "49")] + MouseEventPayload(super::MouseEventPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -256,6 +252,32 @@ pub struct Position { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct MouseEventPayload { + #[prost(uint32, tag = "1")] + pub event_type: u32, + #[prost(bool, tag = "2")] + pub left: bool, + #[prost(bool, tag = "3")] + pub right: bool, + #[prost(bool, tag = "4")] + pub middle: bool, + #[prost(bool, tag = "5")] + pub wheel_up: bool, + #[prost(bool, tag = "6")] + pub wheel_down: bool, + #[prost(bool, tag = "7")] + pub shift: bool, + #[prost(bool, tag = "8")] + pub alt: bool, + #[prost(bool, tag = "9")] + pub ctrl: bool, + #[prost(int64, tag = "10")] + pub line: i64, + #[prost(int64, tag = "11")] + pub column: i64, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct RunCommandAction { #[prost(string, tag = "1")] pub command: ::prost::alloc::string::String, @@ -427,9 +449,6 @@ pub enum ActionName { LeftMouseRelease = 54, RightMouseRelease = 55, MiddleMouseRelease = 56, - MouseHoldLeft = 57, - MouseHoldRight = 58, - MouseHoldMiddle = 59, SearchInput = 60, Search = 61, SearchToggleOption = 62, @@ -456,6 +475,7 @@ pub enum ActionName { MoveTab = 83, KeybindPipe = 84, TogglePanePinned = 85, + MouseEvent = 86, } impl ActionName { /// String value of the enum field names used in the ProtoBuf definition. @@ -521,9 +541,6 @@ impl ActionName { ActionName::LeftMouseRelease => "LeftMouseRelease", ActionName::RightMouseRelease => "RightMouseRelease", ActionName::MiddleMouseRelease => "MiddleMouseRelease", - ActionName::MouseHoldLeft => "MouseHoldLeft", - ActionName::MouseHoldRight => "MouseHoldRight", - ActionName::MouseHoldMiddle => "MouseHoldMiddle", ActionName::SearchInput => "SearchInput", ActionName::Search => "Search", ActionName::SearchToggleOption => "SearchToggleOption", @@ -550,6 +567,7 @@ impl ActionName { ActionName::MoveTab => "MoveTab", ActionName::KeybindPipe => "KeybindPipe", ActionName::TogglePanePinned => "TogglePanePinned", + ActionName::MouseEvent => "MouseEvent", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -612,9 +630,6 @@ impl ActionName { "LeftMouseRelease" => Some(Self::LeftMouseRelease), "RightMouseRelease" => Some(Self::RightMouseRelease), "MiddleMouseRelease" => Some(Self::MiddleMouseRelease), - "MouseHoldLeft" => Some(Self::MouseHoldLeft), - "MouseHoldRight" => Some(Self::MouseHoldRight), - "MouseHoldMiddle" => Some(Self::MouseHoldMiddle), "SearchInput" => Some(Self::SearchInput), "Search" => Some(Self::Search), "SearchToggleOption" => Some(Self::SearchToggleOption), @@ -641,6 +656,7 @@ impl ActionName { "MoveTab" => Some(Self::MoveTab), "KeybindPipe" => Some(Self::KeybindPipe), "TogglePanePinned" => Some(Self::TogglePanePinned), + "MouseEvent" => Some(Self::MouseEvent), _ => None, } } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index bd82b61c94..b159a21ef5 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -306,9 +306,7 @@ pub enum ScreenContext { LeftMouseRelease, RightMouseRelease, MiddleMouseRelease, - MouseHoldLeft, - MouseHoldRight, - MouseHoldMiddle, + MouseEvent, Copy, ToggleTab, AddClient, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 95ec6eb26b..615a7f5e60 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -10,6 +10,7 @@ use crate::data::{Direction, KeyWithModifier, PaneId, Resize}; use crate::data::{FloatingPaneCoordinates, InputMode}; use crate::home::{find_default_config_dir, get_layout_dir}; use crate::input::config::{Config, ConfigError, KdlError}; +use crate::input::mouse::{MouseEvent, MouseEventType}; use crate::input::options::OnForceClose; use miette::{NamedSource, Report}; use serde::{Deserialize, Serialize}; @@ -215,19 +216,11 @@ pub enum Action { Run(RunCommandAction), /// Detach session and exit Detach, - LeftClick(Position), - RightClick(Position), - MiddleClick(Position), LaunchOrFocusPlugin(RunPluginOrAlias, bool, bool, bool, bool), // bools => should float, // move_to_focused_tab, should_open_in_place, skip_cache LaunchPlugin(RunPluginOrAlias, bool, bool, bool, Option), // bools => should float, // should_open_in_place, skip_cache, Option is cwd - LeftMouseRelease(Position), - RightMouseRelease(Position), - MiddleMouseRelease(Position), - MouseHoldLeft(Position), - MouseHoldRight(Position), - MouseHoldMiddle(Position), + MouseEvent(MouseEvent), Copy, /// Confirm a prompt Confirm, @@ -803,6 +796,14 @@ impl Action { _ => false, } } + pub fn is_mouse_motion(&self) -> bool { + if let Action::MouseEvent(mouse_event) = self { + if let MouseEventType::Motion = mouse_event.event_type { + return true; + } + } + false + } } impl From for Action { diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 296b531380..aaf466d6ac 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -3,15 +3,12 @@ pub mod command; pub mod config; pub mod keybinds; pub mod layout; +pub mod mouse; pub mod options; pub mod permission; pub mod plugins; pub mod theme; -// Can't use this in wasm due to dependency on the `termwiz` crate. -#[cfg(not(target_family = "wasm"))] -pub mod mouse; - #[cfg(not(target_family = "wasm"))] pub use not_wasm::*; diff --git a/zellij-utils/src/input/mouse.rs b/zellij-utils/src/input/mouse.rs index 0a63ac2df0..4f89cce873 100644 --- a/zellij-utils/src/input/mouse.rs +++ b/zellij-utils/src/input/mouse.rs @@ -2,102 +2,210 @@ use serde::{Deserialize, Serialize}; use crate::position::Position; +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +/// A mouse event can have any number of buttons (including no +/// buttons) pressed or released. +pub struct MouseEvent { + /// A mouse event can current be a Press, Release, or Motion. + /// Future events could consider double-click and triple-click. + pub event_type: MouseEventType, + + // Mouse buttons associated with this event. + pub left: bool, + pub right: bool, + pub middle: bool, + pub wheel_up: bool, + pub wheel_down: bool, + + // Keyboard modifier flags can be encoded with events too. They + // are not often passed on the wire (instead used for + // selection/copy-paste and changing terminal properties + // on-the-fly at the user-facing terminal), but alt-mouseclick + // usually passes through and is testable on vttest. termwiz + // already exposes them too. + pub shift: bool, + pub alt: bool, + pub ctrl: bool, + + /// The coordinates are zero-based. + pub position: Position, +} + /// A mouse related event -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub enum MouseEvent { +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)] +pub enum MouseEventType { /// A mouse button was pressed. - /// - /// The coordinates are zero-based. - Press(MouseButton, Position), + Press, /// A mouse button was released. - /// - /// The coordinates are zero-based. - Release(Position), + Release, /// A mouse button is held over the given coordinates. - /// - /// The coordinates are zero-based. - Hold(Position), + Motion, } -impl From for MouseEvent { - fn from(event: termwiz::input::MouseEvent) -> Self { - #[allow(clippy::if_same_then_else)] - if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::LEFT) - { - MouseEvent::Press( - MouseButton::Left, - Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1)), - ) - } else if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::RIGHT) - { - MouseEvent::Press( - MouseButton::Right, - Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1)), - ) - } else if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::MIDDLE) - { - MouseEvent::Press( - MouseButton::Middle, - Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1)), - ) - } else if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::VERT_WHEEL) - { - if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::WHEEL_POSITIVE) - { - MouseEvent::Press( - MouseButton::WheelUp, - Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1)), - ) - } else { - MouseEvent::Press( - MouseButton::WheelDown, - Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1)), - ) - } - } else if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::NONE) - { - // release - MouseEvent::Release(Position::new( - event.y.saturating_sub(1) as i32, - event.x.saturating_sub(1), - )) - } else { - // this is an unsupported event, we just do this in order to send "something", but if - // something happens here, we might want to add more specific support - MouseEvent::Release(Position::new( - event.y.saturating_sub(1) as i32, - event.x.saturating_sub(1), - )) - } +impl MouseEvent { + pub fn new() -> Self { + let event = MouseEvent { + event_type: MouseEventType::Motion, + left: false, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position: Position::new(0, 0), + }; + event + } + pub fn new_buttonless_motion(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Motion, + left: false, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_left_press_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Press, + left: true, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_right_press_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Press, + left: false, + right: true, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_middle_press_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Press, + left: false, + right: false, + middle: true, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_middle_release_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Release, + left: false, + right: false, + middle: true, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_left_release_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Release, + left: true, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_left_motion_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Motion, + left: true, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_right_release_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Release, + left: false, + right: true, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_right_motion_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Motion, + left: false, + right: true, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_middle_motion_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Motion, + left: false, + right: false, + middle: true, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] -pub enum MouseButton { - /// The left mouse button. - Left, - /// The right mouse button. - Right, - /// The middle mouse button. - Middle, - /// Mouse wheel is going up. - /// - /// This event is typically only used with Mouse::Press. - WheelUp, - /// Mouse wheel is going down. - /// - /// This event is typically only used with Mouse::Press. - WheelDown, } diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto index df7f3b05b7..3c4948cf2c 100644 --- a/zellij-utils/src/plugin_api/action.proto +++ b/zellij-utils/src/plugin_api/action.proto @@ -35,9 +35,6 @@ message Action { Position left_mouse_release_payload = 26; Position right_mouse_release_payload = 27; Position middle_mouse_release_payload = 28; - Position mouse_hold_left_payload = 29; - Position mouse_hold_right_payload = 30; - Position mouse_hold_middle_payload = 31; bytes search_input_payload = 32; SearchDirection search_payload = 33; SearchOption search_toggle_option_payload = 34; @@ -55,6 +52,7 @@ message Action { LaunchOrFocusPluginPayload launch_plugin_payload = 46; CliPipePayload message_payload = 47; MoveTabDirection move_tab_payload = 48; + MouseEventPayload mouse_event_payload = 49; } } @@ -216,9 +214,6 @@ enum ActionName { LeftMouseRelease = 54; RightMouseRelease = 55; MiddleMouseRelease = 56; - MouseHoldLeft = 57; - MouseHoldRight = 58; - MouseHoldMiddle = 59; SearchInput = 60; Search = 61; SearchToggleOption = 62; @@ -245,6 +240,7 @@ enum ActionName { MoveTab = 83; KeybindPipe = 84; TogglePanePinned = 85; + MouseEvent = 86; } message Position { @@ -252,6 +248,20 @@ message Position { int64 column = 2; } +message MouseEventPayload { + uint32 event_type = 1; + bool left = 2; + bool right = 3; + bool middle = 4; + bool wheel_up = 5; + bool wheel_down = 6; + bool shift = 7; + bool alt = 8; + bool ctrl = 9; + int64 line = 10; + int64 column = 11; +} + message RunCommandAction { string command = 1; repeated string args = 2; diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index e3c0513007..7330c656be 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -2,7 +2,8 @@ pub use super::generated_api::api::{ action::{ action::OptionalPayload, Action as ProtobufAction, ActionName as ProtobufActionName, DumpScreenPayload, EditFilePayload, GoToTabNamePayload, IdAndName, - LaunchOrFocusPluginPayload, MovePanePayload, MoveTabDirection as ProtobufMoveTabDirection, + LaunchOrFocusPluginPayload, MouseEventPayload as ProtobufMouseEventPayload, + MovePanePayload, MoveTabDirection as ProtobufMoveTabDirection, NameAndValue as ProtobufNameAndValue, NewFloatingPanePayload, NewPanePayload, NewPluginPanePayload, NewTiledPanePayload, PaneIdAndShouldFloat, PluginConfiguration as ProtobufPluginConfiguration, Position as ProtobufPosition, @@ -21,6 +22,7 @@ use crate::input::command::{OpenFilePayload, RunCommandAction}; use crate::input::layout::{ PluginUserConfiguration, RunPlugin, RunPluginLocation, RunPluginOrAlias, }; +use crate::input::mouse::{MouseEvent, MouseEventType}; use crate::position::Position; use std::collections::BTreeMap; @@ -382,21 +384,27 @@ impl TryFrom for Action { Some(ProtobufActionName::LeftClick) => match protobuf_action.optional_payload { Some(OptionalPayload::LeftClickPayload(payload)) => { let position = payload.try_into()?; - Ok(Action::LeftClick(position)) + Ok(Action::MouseEvent(MouseEvent::new_left_press_event( + position, + ))) }, _ => Err("Wrong payload for Action::LeftClick"), }, Some(ProtobufActionName::RightClick) => match protobuf_action.optional_payload { Some(OptionalPayload::RightClickPayload(payload)) => { let position = payload.try_into()?; - Ok(Action::RightClick(position)) + Ok(Action::MouseEvent(MouseEvent::new_right_press_event( + position, + ))) }, _ => Err("Wrong payload for Action::RightClick"), }, Some(ProtobufActionName::MiddleClick) => match protobuf_action.optional_payload { Some(OptionalPayload::MiddleClickPayload(payload)) => { let position = payload.try_into()?; - Ok(Action::MiddleClick(position)) + Ok(Action::MouseEvent(MouseEvent::new_middle_press_event( + position, + ))) }, _ => Err("Wrong payload for Action::MiddleClick"), }, @@ -460,14 +468,18 @@ impl TryFrom for Action { Some(ProtobufActionName::LeftMouseRelease) => match protobuf_action.optional_payload { Some(OptionalPayload::LeftMouseReleasePayload(payload)) => { let position = payload.try_into()?; - Ok(Action::LeftMouseRelease(position)) + Ok(Action::MouseEvent(MouseEvent::new_left_release_event( + position, + ))) }, _ => Err("Wrong payload for Action::LeftMouseRelease"), }, Some(ProtobufActionName::RightMouseRelease) => match protobuf_action.optional_payload { Some(OptionalPayload::RightMouseReleasePayload(payload)) => { let position = payload.try_into()?; - Ok(Action::RightMouseRelease(position)) + Ok(Action::MouseEvent(MouseEvent::new_right_release_event( + position, + ))) }, _ => Err("Wrong payload for Action::RightMouseRelease"), }, @@ -475,31 +487,19 @@ impl TryFrom for Action { match protobuf_action.optional_payload { Some(OptionalPayload::MiddleMouseReleasePayload(payload)) => { let position = payload.try_into()?; - Ok(Action::MiddleMouseRelease(position)) + Ok(Action::MouseEvent(MouseEvent::new_middle_release_event( + position, + ))) }, _ => Err("Wrong payload for Action::MiddleMouseRelease"), } }, - Some(ProtobufActionName::MouseHoldLeft) => match protobuf_action.optional_payload { - Some(OptionalPayload::MouseHoldLeftPayload(payload)) => { - let position = payload.try_into()?; - Ok(Action::MouseHoldLeft(position)) - }, - _ => Err("Wrong payload for Action::MouseHoldLeft"), - }, - Some(ProtobufActionName::MouseHoldRight) => match protobuf_action.optional_payload { - Some(OptionalPayload::MouseHoldRightPayload(payload)) => { - let position = payload.try_into()?; - Ok(Action::MouseHoldRight(position)) - }, - _ => Err("Wrong payload for Action::MouseHoldRight"), - }, - Some(ProtobufActionName::MouseHoldMiddle) => match protobuf_action.optional_payload { - Some(OptionalPayload::MouseHoldMiddlePayload(payload)) => { - let position = payload.try_into()?; - Ok(Action::MouseHoldMiddle(position)) + Some(ProtobufActionName::MouseEvent) => match protobuf_action.optional_payload { + Some(OptionalPayload::MouseEventPayload(payload)) => { + let event = payload.try_into()?; + Ok(Action::MouseEvent(event)) }, - _ => Err("Wrong payload for Action::MouseHoldMiddle"), + _ => Err("Wrong payload for Action::MouseEvent"), }, Some(ProtobufActionName::SearchInput) => match protobuf_action.optional_payload { Some(OptionalPayload::SearchInputPayload(payload)) => { @@ -1040,27 +1040,6 @@ impl TryFrom for ProtobufAction { name: ProtobufActionName::Detach as i32, optional_payload: None, }), - Action::LeftClick(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::LeftClick as i32, - optional_payload: Some(OptionalPayload::LeftClickPayload(position)), - }) - }, - Action::RightClick(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::RightClick as i32, - optional_payload: Some(OptionalPayload::RightClickPayload(position)), - }) - }, - Action::MiddleClick(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::MiddleClick as i32, - optional_payload: Some(OptionalPayload::MiddleClickPayload(position)), - }) - }, Action::LaunchOrFocusPlugin( run_plugin_or_alias, should_float, @@ -1105,46 +1084,11 @@ impl TryFrom for ProtobufAction { )), }) }, - Action::LeftMouseRelease(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::LeftMouseRelease as i32, - optional_payload: Some(OptionalPayload::LeftMouseReleasePayload(position)), - }) - }, - Action::RightMouseRelease(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::RightMouseRelease as i32, - optional_payload: Some(OptionalPayload::RightMouseReleasePayload(position)), - }) - }, - Action::MiddleMouseRelease(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::MiddleMouseRelease as i32, - optional_payload: Some(OptionalPayload::MiddleMouseReleasePayload(position)), - }) - }, - Action::MouseHoldLeft(position) => { - let position: ProtobufPosition = position.try_into()?; + Action::MouseEvent(event) => { + let payload: ProtobufMouseEventPayload = event.try_into()?; Ok(ProtobufAction { - name: ProtobufActionName::MouseHoldLeft as i32, - optional_payload: Some(OptionalPayload::MouseHoldLeftPayload(position)), - }) - }, - Action::MouseHoldRight(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::MouseHoldRight as i32, - optional_payload: Some(OptionalPayload::MouseHoldRightPayload(position)), - }) - }, - Action::MouseHoldMiddle(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::MouseHoldMiddle as i32, - optional_payload: Some(OptionalPayload::MouseHoldMiddlePayload(position)), + name: ProtobufActionName::MouseEvent as i32, + optional_payload: Some(OptionalPayload::MouseEventPayload(payload)), }) }, Action::SearchInput(bytes) => Ok(ProtobufAction { @@ -1442,6 +1386,51 @@ impl TryFrom for ProtobufPosition { } } +impl TryFrom for MouseEvent { + type Error = &'static str; + fn try_from(protobuf_event: ProtobufMouseEventPayload) -> Result { + Ok(MouseEvent { + event_type: match protobuf_event.event_type as u32 { + 0 => MouseEventType::Press, + 1 => MouseEventType::Release, + _ => MouseEventType::Motion, + }, + left: protobuf_event.left as bool, + right: protobuf_event.right as bool, + middle: protobuf_event.middle as bool, + wheel_up: protobuf_event.wheel_up as bool, + wheel_down: protobuf_event.wheel_down as bool, + shift: protobuf_event.shift as bool, + alt: protobuf_event.alt as bool, + ctrl: protobuf_event.ctrl as bool, + position: Position::new(protobuf_event.line as i32, protobuf_event.column as u16), + }) + } +} + +impl TryFrom for ProtobufMouseEventPayload { + type Error = &'static str; + fn try_from(event: MouseEvent) -> Result { + Ok(ProtobufMouseEventPayload { + event_type: match event.event_type { + MouseEventType::Press => 0, + MouseEventType::Release => 1, + MouseEventType::Motion => 2, + } as u32, + left: event.left as bool, + right: event.right as bool, + middle: event.middle as bool, + wheel_up: event.wheel_up as bool, + wheel_down: event.wheel_down as bool, + shift: event.shift as bool, + alt: event.alt as bool, + ctrl: event.ctrl as bool, + line: event.position.line.0 as i64, + column: event.position.column.0 as i64, + }) + } +} + impl TryFrom for PluginUserConfiguration { type Error = &'static str; fn try_from(plugin_configuration: ProtobufPluginConfiguration) -> Result {