diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-02-04 11:59:21 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-04 11:59:21 +0100 |
| commit | 690a1e4506854c1e7637cf4c27f75e39b923972d (patch) | |
| tree | ecedd56de5971abe1db82d83402815e52e9df3b2 /zellij-server/src/tab | |
| parent | fix(layout): Correctly handle relative layout paths from CLI (#4664) (diff) | |
| download | sl0ppij-690a1e4506854c1e7637cf4c27f75e39b923972d.tar.gz sl0ppij-690a1e4506854c1e7637cf4c27f75e39b923972d.tar.xz sl0ppij-690a1e4506854c1e7637cf4c27f75e39b923972d.zip | |
Allow resizing panes with the mouse (#4678)
* initial implementation
* improve edge detection
* fix up/down resize increment bug
* temporarily disable alt-mouse grouping
* fix inverting strategies and simplify edge detection
* resize of floating -> ctrl click, resize of tiled -> (ctrl)click
* fix old functionality and tests
* refactor and fixes
* refactor mouse click handling
* move mouse click handling to separate module
* resize scroll up/down with scroll wheel
* tests for scroll resize
* implement hover help
* fix occlusion bug
* allow disabling with config
* fix focus pinned pane on frame
* use strategy invert resize directly rather than function parameter
* remove annoying log message
* cleanups and formatting
* add pr
Diffstat (limited to 'zellij-server/src/tab')
29 files changed, 4268 insertions, 746 deletions
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 02c26362..a7d216b3 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -4,9 +4,11 @@ mod clipboard; mod copy_command; mod layout_applier; +mod mouse_handler; mod swap_layouts; use copy_command::CopyCommand; +pub use mouse_handler::{MouseEffect, MouseHandler, PaneEdge, PaneResizeState}; use std::env::temp_dir; use std::net::IpAddr; use std::path::PathBuf; @@ -18,7 +20,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::input::mouse::MouseEvent; use zellij_utils::position::Position; use zellij_utils::position::{Column, Line}; use zellij_utils::shared::clean_string_from_control_and_linebreak; @@ -151,72 +153,6 @@ enum BufferedTabInstruction { HoldPane(PaneId, Option<i32>, bool, RunCommand), // Option<i32> is the exit status, bool is is_first_run } -#[derive(Debug, Default, Copy, Clone)] -pub struct MouseEffect { - pub state_changed: bool, - pub leave_clipboard_message: bool, - pub group_toggle: Option<PaneId>, - pub group_add: Option<PaneId>, - pub ungroup: bool, -} - -impl MouseEffect { - pub fn state_changed() -> Self { - MouseEffect { - state_changed: true, - leave_clipboard_message: false, - group_toggle: None, - group_add: None, - ungroup: false, - } - } - pub fn leave_clipboard_message() -> Self { - MouseEffect { - state_changed: false, - leave_clipboard_message: true, - group_toggle: None, - group_add: None, - ungroup: false, - } - } - pub fn state_changed_and_leave_clipboard_message() -> Self { - MouseEffect { - state_changed: true, - leave_clipboard_message: true, - group_toggle: None, - group_add: None, - ungroup: false, - } - } - pub fn group_toggle(pane_id: PaneId) -> Self { - MouseEffect { - state_changed: true, - leave_clipboard_message: false, - group_toggle: Some(pane_id), - group_add: None, - ungroup: false, - } - } - pub fn group_add(pane_id: PaneId) -> Self { - MouseEffect { - state_changed: true, - leave_clipboard_message: false, - group_toggle: None, - group_add: Some(pane_id), - ungroup: false, - } - } - pub fn ungroup() -> Self { - MouseEffect { - state_changed: true, - leave_clipboard_message: false, - group_toggle: None, - group_add: None, - ungroup: true, - } - } -} - pub(crate) struct Tab { pub index: usize, pub position: usize, @@ -242,6 +178,7 @@ pub(crate) struct Tab { auto_layout: bool, pending_vte_events: HashMap<u32, Vec<VteBytes>>, pub selecting_with_mouse_in_pane: Option<PaneId>, // this is only pub for the tests + pane_being_resized_with_mouse: Option<PaneResizeState>, link_handler: Rc<RefCell<LinkHandler>>, clipboard_provider: ClipboardProvider, // TODO: used only to focus the pane when the layout is loaded @@ -268,8 +205,11 @@ pub(crate) struct Tab { web_clients_allowed: bool, web_sharing: WebSharing, mouse_hover_pane_id: HashMap<ClientId, PaneId>, + mouse_help_text_visible: HashMap<ClientId, bool>, + last_mouse_activity_time: HashMap<ClientId, Instant>, current_pane_group: Rc<RefCell<PaneGroups>>, advanced_mouse_actions: bool, + mouse_hover_effects: bool, currently_marking_pane_group: Rc<RefCell<HashMap<ClientId, bool>>>, connected_clients_in_app: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client // the below are the configured values - the ones that will be set if and when the web server @@ -486,6 +426,31 @@ pub trait Pane { } false } + fn get_edge_at_position(&self, position: &Position) -> Option<PaneEdge> { + if !self.contains(position) { + return None; + } + + let pos_col = position.column(); + let pos_line = position.line(); + let pane_x = self.x(); + let pane_y = self.y(); + let pane_cols = self.cols(); + let pane_rows = self.rows(); + + let mid_col = pane_x + pane_cols / 2; + let mid_line = pane_y + pane_rows / 2; + + let is_left = pos_col < mid_col; + let is_top = (pos_line as usize) < mid_line; + + return Some(match (is_top, is_left) { + (true, true) => PaneEdge::TopLeft, + (true, false) => PaneEdge::TopRight, + (false, true) => PaneEdge::BottomLeft, + (false, false) => PaneEdge::BottomRight, + }); + } // 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; @@ -693,6 +658,7 @@ impl Tab { current_pane_group: Rc<RefCell<PaneGroups>>, currently_marking_pane_group: Rc<RefCell<HashMap<ClientId, bool>>>, advanced_mouse_actions: bool, + mouse_hover_effects: bool, web_server_ip: IpAddr, web_server_port: u16, ) -> Self { @@ -772,6 +738,7 @@ impl Tab { pending_vte_events: HashMap::new(), connected_clients, selecting_with_mouse_in_pane: None, + pane_being_resized_with_mouse: None, link_handler: Rc::new(RefCell::new(LinkHandler::new())), clipboard_provider, focus_pane_id: None, @@ -793,9 +760,12 @@ impl Tab { web_clients_allowed, web_sharing, mouse_hover_pane_id: HashMap::new(), + mouse_help_text_visible: HashMap::new(), + last_mouse_activity_time: HashMap::new(), current_pane_group, currently_marking_pane_group, advanced_mouse_actions, + mouse_hover_effects, connected_clients_in_app, web_server_ip, web_server_port, @@ -1223,6 +1193,8 @@ impl Tab { .get_mut(&client_id) .map(|c| c.change_to_default_mode()); // TODO: no races? self.connected_clients.borrow_mut().remove(&client_id); + self.mouse_help_text_visible.remove(&client_id); + self.last_mouse_activity_time.remove(&client_id); self.set_force_render(); } pub fn drain_connected_clients( @@ -2628,7 +2600,8 @@ impl Tab { ) }; - self.clear_search(client_id); // this is an inexpensive operation if empty, if we need more such cleanups we should consider moving this and the rest to some sort of cleanup method + self.clear_search(client_id); + self.mouse_help_text_visible.clear(); let pane_id = if self.floating_panes.panes_are_visible() { self.floating_panes .get_active_pane_id(client_id) @@ -3010,6 +2983,7 @@ impl Tab { &self.mouse_hover_pane_id, current_pane_group.clone(), client_id_override, + &self.mouse_help_text_visible, ) .with_context(err_context)?; if (self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes()) @@ -3021,6 +2995,7 @@ impl Tab { &self.mouse_hover_pane_id, current_pane_group, client_id_override, + &self.mouse_help_text_visible, ) .with_context(err_context)?; } @@ -3593,9 +3568,7 @@ impl Tab { ); } pub fn set_mouse_selection_support(&mut self, pane_id: PaneId, selection_support: bool) { - if let Some(pane) = self.get_pane_with_id_mut(pane_id) { - pane.set_mouse_selection_support(selection_support); - } + MouseHandler::set_mouse_selection_support(self, pane_id, selection_support); } pub fn close_pane( &mut self, @@ -4121,27 +4094,7 @@ impl Tab { lines: usize, client_id: ClientId, ) -> Result<MouseEffect> { - let err_context = || { - format!("failed to handle scrollwheel up at position {point:?} for client {client_id}") - }; - - if let Some(pane) = self.get_pane_at(point, false).with_context(err_context)? { - let relative_position = pane.relative_position(point); - if let Some(mouse_event) = pane.mouse_scroll_up(&relative_position) { - self.write_to_terminal_at(mouse_event.into_bytes(), point, client_id) - .with_context(err_context)?; - } else if pane.is_alternate_mode_active() { - // faux scrolling, send UP n times - // do n separate writes to make sure the sequence gets adjusted for cursor keys mode - for _ in 0..lines { - self.write_to_terminal_at("\u{1b}[A".as_bytes().to_owned(), point, client_id) - .with_context(err_context)?; - } - } else { - pane.scroll_up(lines, client_id); - } - } - Ok(MouseEffect::default()) + MouseHandler::handle_scrollwheel_up(self, point, lines, client_id) } pub fn handle_scrollwheel_down( @@ -4150,35 +4103,7 @@ impl Tab { lines: usize, client_id: ClientId, ) -> Result<MouseEffect> { - let err_context = || { - format!( - "failed to handle scrollwheel down at position {point:?} for client {client_id}" - ) - }; - - if let Some(pane) = self.get_pane_at(point, false).with_context(err_context)? { - let relative_position = pane.relative_position(point); - if let Some(mouse_event) = pane.mouse_scroll_down(&relative_position) { - self.write_to_terminal_at(mouse_event.into_bytes(), point, client_id) - .with_context(err_context)?; - } else if pane.is_alternate_mode_active() { - // faux scrolling, send DOWN n times - // do n separate writes to make sure the sequence gets adjusted for cursor keys mode - for _ in 0..lines { - self.write_to_terminal_at("\u{1b}[B".as_bytes().to_owned(), point, client_id) - .with_context(err_context)?; - } - } else { - pane.scroll_down(lines, client_id); - if !pane.is_scrolled() { - if let PaneId::Terminal(pid) = pane.pid() { - self.process_pending_vte_events(pid) - .with_context(err_context)?; - } - } - } - } - Ok(MouseEffect::default()) + MouseHandler::handle_scrollwheel_down(self, point, lines, client_id) } fn get_pane_at( @@ -4285,567 +4210,14 @@ impl Tab { } } - // 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<MouseEffect> { - 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"))?; - - 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 event.alt => { - self.mouse_hover_pane_id.remove(&client_id); - Ok(MouseEffect::group_toggle(pane_id_at_position)) - }, - MouseEventType::Motion if event.alt => { - Ok(MouseEffect::group_add(pane_id_at_position)) - }, - 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 && event.alt { - self.mouse_hover_pane_id.remove(&client_id); - Ok(MouseEffect::ungroup()) - } 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, client_id) { - 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<MouseEffect> { - 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)? - .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(MouseEffect::state_changed()); - } 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(MouseEffect::state_changed()); - } - } - } 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 - let mut leave_clipboard_message = false; - pane_at_position.start_selection(&relative_position, client_id); - if pane_at_position.get_selected_text(client_id).is_some() { - leave_clipboard_message = true; - } - if pane_at_position.supports_mouse_selection() { - self.selecting_with_mouse_in_pane = Some(pane_at_position.pid()); - } - if leave_clipboard_message { - return Ok(MouseEffect::leave_clipboard_message()); - } - } - } - Ok(MouseEffect::default()) - } - fn handle_inactive_pane_left_mouse_press( - &mut self, - event: &MouseEvent, - client_id: ClientId, - ) -> Result<MouseEffect> { - let err_context = - || format!("failed to handle mouse event {event:?} for client {client_id}"); - if !self.floating_panes.panes_are_visible() { - if let Ok(Some(pane_id)) = self - .floating_panes - .get_pinned_pane_id_at(&event.position, true) - { - // 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(MouseEffect::state_changed()); - } else if let Ok(Some(_pane_id)) = self - .floating_panes - .get_pinned_pane_id_at(&event.position, false) - { - // here, the floating panes are not visible, but there is a pinned pane (always - // visible) that has been clicked on - this pane however is not selectable - // (we know this because we passed "false" to get_pinned_pane_id_at) - // so we don't do anything - return Ok(MouseEffect::default()); - } - } - 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)?; - - 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"? - let moved_pane_with_mouse = self - .floating_panes - .move_pane_with_mouse(event.position, search_selectable); - if moved_pane_with_mouse { - return Ok(MouseEffect::state_changed()); - } else { - return Ok(MouseEffect::default()); - } - } - 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(MouseEffect::state_changed()) - } else { - Ok(MouseEffect::default()) - } - } - fn handle_left_mouse_motion( - &mut self, - event: &MouseEvent, - client_id: ClientId, - ) -> Result<MouseEffect> { - 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(MouseEffect::state_changed()); - } - } 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(MouseEffect::default()) - } - fn handle_left_mouse_release( - &mut self, - event: &MouseEvent, - client_id: ClientId, - ) -> Result<MouseEffect> { - let err_context = - || format!("failed to handle mouse event {event:?} for client {client_id}"); - let mut leave_clipboard_message = false; - 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)) - { - 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 pane_with_selection.supports_mouse_selection() { - if copy_on_release { - let selected_text = pane_with_selection.get_selected_text(client_id); - - if let Some(selected_text) = selected_text { - leave_clipboard_message = true; - 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 { - let active_pane_id = self - .get_active_pane_id(client_id) - .ok_or(anyhow!("Failed to find pane at position"))?; - 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(); - if active_pane_id == pane_id_at_position { - self.write_mouse_event_to_active_pane(event, client_id)?; - } - } - if leave_clipboard_message { - Ok(MouseEffect::leave_clipboard_message()) - } else { - Ok(MouseEffect::default()) - } - } - - pub fn handle_right_click( - &mut self, - event: &MouseEvent, - client_id: ClientId, - ) -> Result<MouseEffect> { - 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(&absolute_position, false) - .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, client_id) { - 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(MouseEffect::default()) - } - - fn handle_middle_click( - &mut self, - event: &MouseEvent, - client_id: ClientId, - ) -> Result<MouseEffect> { - let err_context = || format!("failed to handle mouse middle 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(&absolute_position, false) - .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, client_id) { - 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(MouseEffect::default()) - } - - fn handle_mouse_no_click( - &mut self, - event: &MouseEvent, - client_id: ClientId, - ) -> Result<MouseEffect> { - let err_context = || format!("failed to handle mouse no 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(&absolute_position, false) - .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, client_id) { - 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)?; - } - } - self.mouse_hover_pane_id.remove(&client_id); - } else { - let pane_id = pane.pid(); - // if the pane is not selectable, we don't want to create a hover effect over it - // we do however want to remove the hover effect from other panes - let pane_is_selectable = pane.selectable(); - if self.advanced_mouse_actions && pane_is_selectable { - self.mouse_hover_pane_id.insert(client_id, pane_id); - } else if self.advanced_mouse_actions { - self.mouse_hover_pane_id.remove(&client_id); - } - } - }; - Ok(MouseEffect::leave_clipboard_message()) - } - - fn unselectable_pane_at_position(&mut self, point: &Position) -> Option<&mut Box<dyn Pane>> { - // 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}"); - - if self.floating_panes.panes_are_visible() { - if let Some(clicked_pane) = self - .floating_panes - .get_pane_id_at(point, true) - .with_context(err_context)? - { - self.floating_panes.focus_pane(clicked_pane, client_id); - self.set_pane_active_at(clicked_pane); - return Ok(()); - } - } - if let Some(clicked_pane) = self.get_pane_id_at(point, true).with_context(err_context)? { - self.tiled_panes.focus_pane(clicked_pane, client_id); - self.set_pane_active_at(clicked_pane); - if self.floating_panes.panes_are_visible() { - self.hide_floating_panes(); - self.set_force_render(); - } - } - Ok(()) - } - - #[cfg(test)] - pub fn handle_right_mouse_release( - &mut self, - position: &Position, - client_id: ClientId, - ) -> Result<()> { - let err_context = || { - format!("failed to handle right mouse release at position {position:?} 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 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_right_click_release(&relative_position) { - self.write_to_active_terminal(&None, mouse_event.into_bytes(), false, client_id) - .with_context(err_context)?; - } - } - Ok(()) + MouseHandler::handle_mouse_event(self, event, client_id) } - #[cfg(test)] - fn handle_middle_mouse_release( - &mut self, - position: &Position, - client_id: ClientId, - ) -> Result<()> { - let err_context = || { - format!("failed to handle middle mouse release at position {position:?} 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 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_middle_click_release(&relative_position) { - self.write_to_active_terminal(&None, mouse_event.into_bytes(), false, client_id) - .with_context(err_context)?; - } - } - Ok(()) - } pub fn copy_selection(&self, client_id: ClientId) -> Result<()> { let selected_text = self .get_active_pane(client_id) @@ -4941,6 +4313,10 @@ impl Tab { Ok(()) } + pub fn clear_mouse_help_text(&mut self, client_id: ClientId) { + self.mouse_help_text_visible.insert(client_id, false); + } + pub fn update_active_pane_name(&mut self, buf: Vec<u8>, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to update name of active pane to '{buf:?}' for client {client_id}"); @@ -5709,6 +5085,13 @@ impl Tab { pub fn update_advanced_mouse_actions(&mut self, advanced_mouse_actions: bool) { self.advanced_mouse_actions = advanced_mouse_actions; } + pub fn update_mouse_hover_effects(&mut self, mouse_hover_effects: bool) { + self.mouse_hover_effects = mouse_hover_effects; + } + pub fn clear_mouse_hover_state(&mut self) { + self.mouse_hover_pane_id.clear(); + self.mouse_help_text_visible.clear(); + } pub fn update_web_sharing(&mut self, web_sharing: WebSharing) { let old_value = self.web_sharing; self.web_sharing = web_sharing; @@ -5908,6 +5291,10 @@ impl Tab { pub fn get_client_input_mode(&self, client_id: ClientId) -> Option<InputMode> { self.mode_info.borrow().get(&client_id).map(|m| m.mode) } + #[allow(unused)] // this is used for tests + pub fn query_mouse_hover_pane_id(&self) -> HashMap<ClientId, PaneId> { + self.mouse_hover_pane_id.clone() + } fn new_scrollback_editor_pane(&self, pid: u32) -> TerminalPane { let next_terminal_position = self.get_next_terminal_position(); let mut new_pane = TerminalPane::new( diff --git a/zellij-server/src/tab/mouse_handler.rs b/zellij-server/src/tab/mouse_handler.rs new file mode 100644 index 00000000..037c5acd --- /dev/null +++ b/zellij-server/src/tab/mouse_handler.rs @@ -0,0 +1,1465 @@ +use std::time::Instant; +use zellij_utils::data::{Direction, Resize, ResizeStrategy}; +use zellij_utils::errors::prelude::*; +use zellij_utils::input::mouse::{MouseEvent, MouseEventType}; +use zellij_utils::pane_size::PaneGeom; +use zellij_utils::position::Position; + +use crate::background_jobs::BackgroundJob; +use crate::panes::PaneId; +use crate::ClientId; + +use super::{Pane, Tab}; + +#[derive(Debug, Default, Copy, Clone)] +pub struct MouseEffect { + pub state_changed: bool, + pub leave_clipboard_message: bool, + pub group_toggle: Option<PaneId>, + pub group_add: Option<PaneId>, + pub ungroup: bool, +} + +impl MouseEffect { + pub fn state_changed() -> Self { + MouseEffect { + state_changed: true, + leave_clipboard_message: false, + group_toggle: None, + group_add: None, + ungroup: false, + } + } + pub fn leave_clipboard_message() -> Self { + MouseEffect { + state_changed: false, + leave_clipboard_message: true, + group_toggle: None, + group_add: None, + ungroup: false, + } + } + pub fn state_changed_and_leave_clipboard_message() -> Self { + MouseEffect { + state_changed: true, + leave_clipboard_message: true, + group_toggle: None, + group_add: None, + ungroup: false, + } + } + pub fn group_toggle(pane_id: PaneId) -> Self { + MouseEffect { + state_changed: true, + leave_clipboard_message: false, + group_toggle: Some(pane_id), + group_add: None, + ungroup: false, + } + } + pub fn group_add(pane_id: PaneId) -> Self { + MouseEffect { + state_changed: true, + leave_clipboard_message: false, + group_toggle: None, + group_add: Some(pane_id), + ungroup: false, + } + } + pub fn ungroup() -> Self { + MouseEffect { + state_changed: true, + leave_clipboard_message: false, + group_toggle: None, + group_add: None, + ungroup: true, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +enum MouseAction { + GroupToggle(PaneId), + GroupAdd(PaneId), + Ungroup, + StartResize { + pane_id: PaneId, + edge: PaneEdge, + is_floating: bool, + position: Position, + }, + ContinueResize { + position: Position, + }, + StopResize { + position: Position, + }, + FocusPane { + pane_id: PaneId, + position: Position, + }, + ShowFloatingPanesAndFocus { + pane_id: PaneId, + }, + StartSelection { + pane_id: PaneId, + position: Position, + }, + UpdateSelection { + position: Position, + }, + EndSelection { + position: Position, + }, + StartMovingFloatingPane { + position: Position, + }, + ContinueMovingFloatingPane { + position: Position, + }, + StopMovingFloatingPane { + position: Position, + }, + ScrollUp { + pane_id: PaneId, + lines: usize, + }, + ScrollDown { + pane_id: PaneId, + lines: usize, + }, + ResizeScrollUp { + pane_id: PaneId, + }, + ResizeScrollDown { + pane_id: PaneId, + }, + UpdateHover { + pane_id: Option<PaneId>, + }, + SendToTerminal { + pane_id: PaneId, + event: MouseEvent, + }, + FrameIntercepted { + pane_id: PaneId, + }, + NoAction, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PaneEdge { + Left, + Right, + Top, + Bottom, + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PaneResizeState { + pub pane_id: PaneId, + pub edge: PaneEdge, + pub start_position: Position, + pub start_geom: PaneGeom, + pub is_floating: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ClickedPaneDetails { + pane_id: PaneId, + on_frame: bool, + frame_intercepted: bool, + edge: Option<PaneEdge>, + is_floating: bool, + terminal_wants_mouse: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct MouseEventContext { + pane_id_at_position: Option<PaneId>, + active_pane_id: Option<PaneId>, + floating_visible: bool, + pane_being_resized: bool, + selecting_with_mouse: bool, + pane_being_moved: bool, + clicked_pane: Option<ClickedPaneDetails>, + pinned_selectable: Option<PaneId>, + pinned_unselectable: Option<PaneId>, +} + +fn edge_and_delta_to_strategies( + edge: PaneEdge, + delta_x: isize, + delta_y: isize, +) -> Vec<ResizeStrategy> { + use Direction::*; + use Resize::*; + + match edge { + PaneEdge::Left => { + let resize = if delta_x < 0 { Increase } else { Decrease }; + vec![ResizeStrategy { + resize, + direction: Some(Left), + invert_on_boundaries: false, + }] + }, + PaneEdge::Right => { + let resize = if delta_x > 0 { Increase } else { Decrease }; + vec![ResizeStrategy { + resize, + direction: Some(Right), + invert_on_boundaries: false, + }] + }, + PaneEdge::Top => { + let resize = if delta_y < 0 { Increase } else { Decrease }; + vec![ResizeStrategy { + resize, + direction: Some(Up), + invert_on_boundaries: false, + }] + }, + PaneEdge::Bottom => { + let resize = if delta_y > 0 { Increase } else { Decrease }; + vec![ResizeStrategy { + resize, + direction: Some(Down), + invert_on_boundaries: false, + }] + }, + PaneEdge::TopLeft => { + let mut strategies = vec![]; + // Top edge + let resize_y = if delta_y < 0 { Increase } else { Decrease }; + strategies.push(ResizeStrategy { + resize: resize_y, + direction: Some(Up), + invert_on_boundaries: false, + }); + // Left edge + let resize_x = if delta_x < 0 { Increase } else { Decrease }; + strategies.push(ResizeStrategy { + resize: resize_x, + direction: Some(Left), + invert_on_boundaries: false, + }); + strategies + }, + PaneEdge::TopRight => { + let mut strategies = vec![]; + // Top edge + let resize_y = if delta_y < 0 { Increase } else { Decrease }; + strategies.push(ResizeStrategy { + resize: resize_y, + direction: Some(Up), + invert_on_boundaries: false, + }); + // Right edge + let resize_x = if delta_x > 0 { Increase } else { Decrease }; + strategies.push(ResizeStrategy { + resize: resize_x, + direction: Some(Right), + invert_on_boundaries: false, + }); + strategies + }, + PaneEdge::BottomLeft => { + let mut strategies = vec![]; + // Bottom edge + let resize_y = if delta_y > 0 { Increase } else { Decrease }; + strategies.push(ResizeStrategy { + resize: resize_y, + direction: Some(Down), + invert_on_boundaries: false, + }); + // Left edge + let resize_x = if delta_x < 0 { Increase } else { Decrease }; + strategies.push(ResizeStrategy { + resize: resize_x, + direction: Some(Left), + invert_on_boundaries: false, + }); + strategies + }, + PaneEdge::BottomRight => { + let mut strategies = vec![]; + // Bottom edge + let resize_y = if delta_y > 0 { Increase } else { Decrease }; + strategies.push(ResizeStrategy { + resize: resize_y, + direction: Some(Down), + invert_on_boundaries: false, + }); + // Right edge + let resize_x = if delta_x > 0 { Increase } else { Decrease }; + strategies.push(ResizeStrategy { + resize: resize_x, + direction: Some(Right), + invert_on_boundaries: false, + }); + strategies + }, + } +} + +pub struct MouseHandler; + +impl MouseHandler { + pub fn handle_mouse_event( + tab: &mut Tab, + event: &MouseEvent, + client_id: ClientId, + ) -> Result<MouseEffect> { + let context = Self::gather_mouse_event_context(tab, event, client_id)?; + let action = Self::determine_mouse_action(event, &context)?; + Self::execute_mouse_action(tab, action, event, client_id) + } + + fn gather_mouse_event_context( + tab: &mut Tab, + event: &MouseEvent, + client_id: ClientId, + ) -> Result<MouseEventContext> { + let err_context = || format!("failed to gather context for event {event:?}"); + + let pane_id_at_position = Self::get_pane_at(tab, &event.position, false) + .with_context(err_context)? + .map(|p| p.pid()); + let active_pane_id = tab.get_active_pane_id(client_id); + let floating_visible = tab.floating_panes.panes_are_visible(); + + let clicked_pane = pane_id_at_position.and_then(|id| { + Self::gather_clicked_pane_details( + tab, + id, + &event.position, + active_pane_id, + event, + client_id, + ) + }); + + let (pinned_selectable, pinned_unselectable) = if !floating_visible { + let selectable = tab + .floating_panes + .get_pinned_pane_id_at(&event.position, true) + .ok() + .flatten(); + let unselectable = tab + .floating_panes + .get_pinned_pane_id_at(&event.position, false) + .ok() + .flatten(); + (selectable, unselectable) + } else { + (None, None) + }; + + Ok(MouseEventContext { + pane_id_at_position, + active_pane_id, + floating_visible, + pane_being_resized: tab.pane_being_resized_with_mouse.is_some(), + selecting_with_mouse: tab.selecting_with_mouse_in_pane.is_some(), + pane_being_moved: tab.floating_panes.pane_is_being_moved_with_mouse(), + clicked_pane, + pinned_selectable, + pinned_unselectable, + }) + } + + fn gather_clicked_pane_details( + tab: &mut Tab, + pane_id: PaneId, + position: &Position, + active_pane_id: Option<PaneId>, + event: &MouseEvent, + client_id: ClientId, + ) -> Option<ClickedPaneDetails> { + let is_floating = tab.floating_panes.panes_contain(&pane_id); + let pane = Self::get_pane_at(tab, position, false).ok()??; + + let on_frame = pane.position_is_on_frame(position); + let frame_intercepted = on_frame && pane.intercept_mouse_event_on_frame(event, client_id); + let edge = if on_frame { + pane.get_edge_at_position(position) + } else { + None + }; + let terminal_wants_mouse = if Some(pane_id) == active_pane_id { + let relative_position = pane.relative_position(position); + pane.mouse_left_click(&relative_position, false).is_some() + } else { + false + }; + + Some(ClickedPaneDetails { + pane_id, + on_frame, + frame_intercepted, + edge, + is_floating, + terminal_wants_mouse, + }) + } + + fn start_pane_resize_with_mouse( + tab: &mut Tab, + pane_id: PaneId, + edge: PaneEdge, + position: Position, + _client_id: ClientId, + ) -> Result<()> { + let err_context = || format!("failed to start pane resize for pane {pane_id:?}"); + + // Determine if floating or tiled + let is_floating = tab.floating_panes.panes_contain(&pane_id); + + // Get current pane geometry + let start_geom = if is_floating { + tab.floating_panes + .get_pane(pane_id) + .map(|p| p.position_and_size()) + .with_context(err_context)? + } else { + tab.tiled_panes + .get_pane(pane_id) + .map(|p| p.position_and_size()) + .with_context(err_context)? + }; + + tab.pane_being_resized_with_mouse = Some(PaneResizeState { + pane_id, + edge, + start_position: position, + start_geom, + is_floating, + }); + + Ok(()) + } + + fn continue_pane_resize_with_mouse( + tab: &mut Tab, + current_position: Position, + _client_id: ClientId, + ) -> Result<bool> { + // bool -> state changed + let err_context = || "failed to continue pane resize with mouse"; + + // Extract needed values from resize_state to avoid borrow issues + let (pane_id, edge, is_floating, delta_x, delta_y) = + if let Some(resize_state) = &tab.pane_being_resized_with_mouse { + // Calculate delta from start_position to current_position + let delta_x = current_position.column() as isize + - resize_state.start_position.column() as isize; + let delta_y = current_position.line() - resize_state.start_position.line(); + + // Only proceed if there's a meaningful delta + if delta_x == 0 && delta_y == 0 { + return Ok(false); + } + + ( + resize_state.pane_id, + resize_state.edge, + resize_state.is_floating, + delta_x, + delta_y, + ) + } else { + return Ok(true); + }; + + // Convert edge + delta to ResizeStrategy + let strategies = edge_and_delta_to_strategies(edge, delta_x, delta_y); + + // Apply appropriate resize function + if is_floating { + Self::resize_floating_pane_with_strategies( + tab, + pane_id, + &strategies, + (delta_x.unsigned_abs(), delta_y.unsigned_abs()), + ) + .with_context(err_context)?; + } else { + Self::resize_tiled_pane_with_strategies( + tab, + pane_id, + &strategies, + (delta_x.abs() as f64, delta_y.abs() as f64), + ) + .with_context(err_context)?; + } + + // Update start_position to current position (incremental) + if let Some(resize_state) = tab.pane_being_resized_with_mouse.as_mut() { + resize_state.start_position = current_position; + } + + tab.set_force_render(); + + Ok(true) + } + + fn stop_pane_resize_with_mouse( + tab: &mut Tab, + final_position: Position, + client_id: ClientId, + ) -> Result<bool> { + // bool -> never_resized + let err_context = || "failed to stop pane resize with mouse"; + + // Perform final resize with any remaining delta + let start_geom = tab + .pane_being_resized_with_mouse + .as_ref() + .map(|p| p.start_geom.clone()); + let pane_id = tab + .pane_being_resized_with_mouse + .as_ref() + .map(|p| p.pane_id); + let _resized = Self::continue_pane_resize_with_mouse(tab, final_position, client_id) + .with_context(err_context)?; + let last_geom = pane_id + .and_then(|pane_id| tab.get_pane_with_id(pane_id)) + .map(|p| p.position_and_size()); + let never_resized = match (start_geom, last_geom) { + (Some(start_geom), Some(last_geom)) => start_geom == last_geom, + _ => false, + }; + + // Clear resize state + tab.pane_being_resized_with_mouse = None; + + Ok(never_resized) + } + + fn resize_floating_pane_with_strategies( + tab: &mut Tab, + pane_id: PaneId, + strategies: &[ResizeStrategy], + change_by: (usize, usize), + ) -> Result<()> { + let err_context = || format!("failed to resize floating pane {pane_id:?}"); + + tab.floating_panes + .resize_pane_with_strategies(pane_id, strategies, change_by) + .with_context(err_context)?; + + tab.swap_layouts.set_is_floating_damaged(); + + Ok(()) + } + + fn resize_tiled_pane_with_strategies( + tab: &mut Tab, + pane_id: PaneId, + strategies: &[ResizeStrategy], + change_by: (f64, f64), + ) -> Result<()> { + let err_context = || format!("failed to resize tiled pane {pane_id:?}"); + + // Calculate percentage based on total viewport size for 1:1 cell mapping + // Formula: (delta_cells / total_viewport_cells) * 100.0 = percentage + // This ensures 1 cell of mouse movement = exactly 1 cell of pane resize + let viewport = tab.viewport.borrow(); + let viewport_cols = viewport.cols; + let viewport_rows = viewport.rows; + + let change_by_percent = ( + if viewport_cols > 0 { + (change_by.0 / viewport_cols as f64) * 100.0 // cols + } else { + 0.0 + }, + if viewport_rows > 0 { + (change_by.1 / viewport_rows as f64) * 100.0 // rows + } else { + 0.0 + }, + ); + + tab.tiled_panes + .resize_pane_with_strategies(pane_id, strategies, change_by_percent) + .with_context(err_context)?; + + tab.swap_layouts.set_is_tiled_damaged(); + + Ok(()) + } + + fn resize_tiled_pane_with_stacked_resize( + tab: &mut Tab, + pane_id: PaneId, + strategy: &ResizeStrategy, + ) -> Result<()> { + let err_context = || format!("failed to resize tiled pane {pane_id:?}"); + + tab.tiled_panes + // we override the resize percent because it's better UX to have smaller increments + // with the mouse wheel + .stacked_resize_pane_with_id(pane_id, strategy, Some((5.0, 5.0))) + .with_context(err_context)?; + + tab.swap_layouts.set_is_tiled_damaged(); + + Ok(()) + } + + fn execute_mouse_action( + tab: &mut Tab, + action: MouseAction, + event: &MouseEvent, + client_id: ClientId, + ) -> Result<MouseEffect> { + let err_context = + || format!("failed to execute mouse action {action:?} for client {client_id}"); + + if !matches!(action, MouseAction::UpdateHover { .. }) { + tab.mouse_help_text_visible.remove(&client_id); + } + + match action { + MouseAction::GroupToggle(pane_id) => Ok(MouseEffect::group_toggle(pane_id)), + MouseAction::GroupAdd(pane_id) => Ok(MouseEffect::group_add(pane_id)), + MouseAction::Ungroup => Ok(MouseEffect::ungroup()), + MouseAction::StartResize { + pane_id, + edge, + is_floating: _, + position, + } => { + tab.mouse_hover_pane_id.remove(&client_id); + Self::start_pane_resize_with_mouse(tab, pane_id, edge, position, client_id) + .with_context(err_context)?; + Ok(MouseEffect::state_changed()) + }, + MouseAction::ContinueResize { position } => { + let state_changed = Self::continue_pane_resize_with_mouse(tab, position, client_id) + .with_context(err_context)?; + if state_changed { + Ok(MouseEffect::state_changed()) + } else { + Ok(MouseEffect::default()) + } + }, + MouseAction::StopResize { position } => { + Self::execute_stop_resize(tab, position, client_id) + }, + MouseAction::FocusPane { + pane_id: _, + position, + } => Self::execute_focus_pane(tab, position, client_id), + MouseAction::ShowFloatingPanesAndFocus { pane_id } => { + tab.show_floating_panes(); + tab.floating_panes.focus_pane(pane_id, client_id); + Ok(MouseEffect::state_changed()) + }, + MouseAction::StartSelection { pane_id, position } => { + let pane = tab + .get_pane_with_id_mut(pane_id) + .ok_or_else(|| anyhow!("Failed to find pane {pane_id:?}"))?; + let relative_position = pane.relative_position(&position); + let mut leave_clipboard_message = false; + pane.start_selection(&relative_position, client_id); + if pane.get_selected_text(client_id).is_some() { + leave_clipboard_message = true; + } + if pane.supports_mouse_selection() { + tab.selecting_with_mouse_in_pane = Some(pane_id); + } + if leave_clipboard_message { + Ok(MouseEffect::leave_clipboard_message()) + } else { + Ok(MouseEffect::default()) + } + }, + MouseAction::UpdateSelection { position } => { + if let Some(pane_id_with_selection) = tab.selecting_with_mouse_in_pane { + if let Some(pane_with_selection) = + tab.get_pane_with_id_mut(pane_id_with_selection) + { + let relative_position = pane_with_selection.relative_position(&position); + pane_with_selection.update_selection(&relative_position, client_id); + } + } + Ok(MouseEffect::default()) + }, + MouseAction::EndSelection { position } => { + Self::execute_end_selection(tab, position, client_id) + }, + MouseAction::StartMovingFloatingPane { position } => { + Self::execute_move_floating_pane(tab, position) + }, + MouseAction::ContinueMovingFloatingPane { position } => { + Self::execute_move_floating_pane(tab, position) + }, + MouseAction::StopMovingFloatingPane { position } => { + Self::execute_stop_moving_floating_pane(tab, position, client_id) + }, + MouseAction::ScrollUp { pane_id: _, lines } => { + Self::handle_scrollwheel_up(tab, &event.position, lines, client_id) + .with_context(err_context) + }, + MouseAction::ScrollDown { pane_id: _, lines } => { + Self::handle_scrollwheel_down(tab, &event.position, lines, client_id) + .with_context(err_context) + }, + MouseAction::ResizeScrollUp { pane_id } => { + Self::handle_resize_scroll_up(tab, pane_id, client_id).with_context(err_context) + }, + MouseAction::ResizeScrollDown { pane_id } => { + Self::handle_resize_scroll_down(tab, pane_id, client_id).with_context(err_context) + }, + MouseAction::UpdateHover { pane_id } => { + Self::execute_update_hover(tab, pane_id, client_id) + }, + MouseAction::SendToTerminal { pane_id, event } => { + Self::execute_send_to_terminal(tab, pane_id, event, client_id) + }, + MouseAction::FrameIntercepted { pane_id: _ } => { + tab.set_force_render(); + Ok(MouseEffect::state_changed()) + }, + MouseAction::NoAction => Ok(MouseEffect::default()), + } + } + + fn execute_stop_resize( + tab: &mut Tab, + position: Position, + client_id: ClientId, + ) -> Result<MouseEffect> { + let err_context = || "failed to stop resize"; + let never_resized = Self::stop_pane_resize_with_mouse(tab, position, client_id) + .with_context(err_context)?; + if never_resized { + let pane_id_at_position = Self::get_pane_at(tab, &position, false) + .with_context(err_context)? + .map(|p| p.pid()); + let active_pane_id = tab + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find active pane"))?; + if let Some(pane_id) = pane_id_at_position { + if pane_id != active_pane_id { + Self::focus_pane_at(tab, &position, client_id).with_context(err_context)?; + } + } + } + Ok(MouseEffect::state_changed()) + } + + fn execute_focus_pane( + tab: &mut Tab, + position: Position, + client_id: ClientId, + ) -> Result<MouseEffect> { + let err_context = || "failed to focus pane"; + tab.mouse_hover_pane_id.remove(&client_id); + let active_pane_id_before = tab + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find active pane"))?; + + Self::focus_pane_at(tab, &position, client_id).with_context(err_context)?; + + if let Some(pane_at_position) = Self::unselectable_pane_at_position(tab, &position) { + let relative_position = pane_at_position.relative_position(&position); + pane_at_position.start_selection(&relative_position, client_id); + } + + if tab.floating_panes.panes_are_visible() { + let search_selectable = false; + let moved_pane_with_mouse = tab + .floating_panes + .move_pane_with_mouse(position, search_selectable); + let active_pane_id_after = tab + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find active pane"))?; + if moved_pane_with_mouse || active_pane_id_before != active_pane_id_after { + return Ok(MouseEffect::state_changed()); + } else { + return Ok(MouseEffect::default()); + } + } + + let active_pane_id_after = tab + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find active pane"))?; + if active_pane_id_before != active_pane_id_after { + Ok(MouseEffect::state_changed()) + } else { + Ok(MouseEffect::default()) + } + } + + fn execute_end_selection( + tab: &mut Tab, + position: Position, + client_id: ClientId, + ) -> Result<MouseEffect> { + let err_context = || "failed to end selection"; + let mut leave_clipboard_message = false; + let copy_on_release = tab.copy_on_select; + + if let Some(pane_with_selection) = tab + .selecting_with_mouse_in_pane + .and_then(|p_id| tab.get_pane_with_id_mut(p_id)) + { + let mut relative_position = pane_with_selection.relative_position(&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) + { + tab.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(&position); + pane_with_selection.end_selection(&relative_position, client_id); + if pane_with_selection.supports_mouse_selection() { + if copy_on_release { + let selected_text = pane_with_selection.get_selected_text(client_id); + if let Some(selected_text) = selected_text { + leave_clipboard_message = true; + tab.write_selection_to_clipboard(&selected_text) + .with_context(err_context)?; + } + } + } + tab.selecting_with_mouse_in_pane = None; + } + } + + if leave_clipboard_message { + Ok(MouseEffect::leave_clipboard_message()) + } else { + Ok(MouseEffect::default()) + } + } + + fn execute_move_floating_pane(tab: &mut Tab, position: Position) -> Result<MouseEffect> { + let search_selectable = false; + if tab + .floating_panes + .move_pane_with_mouse(position, search_selectable) + { + tab.swap_layouts.set_is_floating_damaged(); + tab.set_force_render(); + Ok(MouseEffect::state_changed()) + } else { + Ok(MouseEffect::default()) + } + } + + fn execute_stop_moving_floating_pane( + tab: &mut Tab, + position: Position, + client_id: ClientId, + ) -> Result<MouseEffect> { + let err_context = || "failed to stop moving floating pane"; + let never_moved = tab.floating_panes.stop_moving_pane_with_mouse(position); + if never_moved { + let active_pane_id = tab + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find active pane"))?; + let pane_id_at_position = Self::get_pane_at(tab, &position, false) + .with_context(err_context)? + .ok_or_else(|| anyhow!("Failed to find pane at position"))? + .pid(); + if active_pane_id != pane_id_at_position { + Self::focus_pane_at(tab, &position, client_id).with_context(err_context)?; + } + } + Ok(MouseEffect::default()) + } + + fn execute_update_hover( + tab: &mut Tab, + pane_id: Option<PaneId>, + client_id: ClientId, + ) -> Result<MouseEffect> { + let mut should_render = false; + match pane_id { + Some(pid) => { + if let Some(pane) = tab.get_pane_with_id(pid) { + let pane_is_selectable = pane.selectable(); + if tab.advanced_mouse_actions && tab.mouse_hover_effects && pane_is_selectable { + tab.mouse_hover_pane_id.insert(client_id, pid); + } else if tab.advanced_mouse_actions || !tab.mouse_hover_effects { + tab.mouse_hover_pane_id.remove(&client_id); + } + should_render = true; + } + }, + None => { + let removed = tab.mouse_hover_pane_id.remove(&client_id); + if removed.is_some() { + should_render = true; + } + }, + } + + if tab.mouse_help_text_visible.remove(&client_id).is_some() { + should_render = true; + } + + let mut mouse_effect = if should_render { + MouseEffect::state_changed() + } else { + MouseEffect::default() + }; + mouse_effect.leave_clipboard_message = true; + Ok(mouse_effect) + } + + fn execute_send_to_terminal( + tab: &mut Tab, + pane_id: PaneId, + event: MouseEvent, + client_id: ClientId, + ) -> Result<MouseEffect> { + let err_context = || format!("failed to send to terminal for pane {pane_id:?}"); + let mut should_render = false; + let active_pane_id = tab + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find active pane"))?; + if pane_id == active_pane_id { + let pane = tab + .get_pane_with_id(pane_id) + .ok_or_else(|| anyhow!("Failed to find pane {pane_id:?}"))?; + let relative_position = pane.relative_position(&event.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, client_id) { + if !pane.position_is_on_frame(&event.position) { + tab.write_to_active_terminal(&None, mouse_event.into_bytes(), false, client_id) + .with_context(err_context)?; + } + } + let removed_hover = tab.mouse_hover_pane_id.remove(&client_id); + if removed_hover.is_some() { + should_render = true; + } + + if event.event_type == MouseEventType::Motion && tab.mouse_hover_effects { + tab.last_mouse_activity_time + .insert(client_id, Instant::now()); + let was_visible = tab + .mouse_help_text_visible + .get(&client_id) + .copied() + .unwrap_or(false); + tab.mouse_help_text_visible.insert(client_id, true); + if !was_visible { + should_render = true; + } + + tab.senders + .send_to_background_jobs(BackgroundJob::ClearHelpText { client_id }) + .with_context(err_context)?; + } + } + let mouse_effect = if should_render { + MouseEffect::state_changed() + } else { + MouseEffect::default() + }; + Ok(mouse_effect) + } + + fn determine_mouse_action(event: &MouseEvent, ctx: &MouseEventContext) -> Result<MouseAction> { + if ctx.pane_being_resized { + return Ok(match event.event_type { + MouseEventType::Motion => MouseAction::ContinueResize { + position: event.position, + }, + MouseEventType::Release => MouseAction::StopResize { + position: event.position, + }, + _ => MouseAction::NoAction, + }); + } + + if ctx.selecting_with_mouse { + return Ok(match event.event_type { + MouseEventType::Motion if event.left => MouseAction::UpdateSelection { + position: event.position, + }, + MouseEventType::Release if event.left => MouseAction::EndSelection { + position: event.position, + }, + _ => MouseAction::NoAction, + }); + } + + if ctx.pane_being_moved { + return Ok(match event.event_type { + MouseEventType::Motion if event.left => MouseAction::ContinueMovingFloatingPane { + position: event.position, + }, + MouseEventType::Release if event.left => MouseAction::StopMovingFloatingPane { + position: event.position, + }, + _ => MouseAction::NoAction, + }); + } + + if event.alt { + let is_left_press = event.left && event.event_type == MouseEventType::Press; + let is_left_motion = event.left && event.event_type == MouseEventType::Motion; + + if is_left_press { + if let Some(pane_id) = ctx.pane_id_at_position { + return Ok(MouseAction::GroupToggle(pane_id)); + } + } + if is_left_motion { + if let Some(pane_id) = ctx.pane_id_at_position { + return Ok(MouseAction::GroupAdd(pane_id)); + } + } + if event.right { + return Ok(MouseAction::Ungroup); + } + return Ok(MouseAction::NoAction); + } + + if event.wheel_up || event.wheel_down { + if let Some(pane_id) = ctx.pane_id_at_position { + if event.ctrl { + if event.wheel_up { + return Ok(MouseAction::ResizeScrollUp { pane_id }); + } + if event.wheel_down { + return Ok(MouseAction::ResizeScrollDown { pane_id }); + } + } + if event.wheel_up { + return Ok(MouseAction::ScrollUp { pane_id, lines: 3 }); + } + if event.wheel_down { + return Ok(MouseAction::ScrollDown { pane_id, lines: 3 }); + } + } + return Ok(MouseAction::NoAction); + } + + let is_ctrl_left_press = + event.ctrl && event.left && event.event_type == MouseEventType::Press; + if is_ctrl_left_press { + let Some(details) = &ctx.clicked_pane else { + return Ok(MouseAction::NoAction); + }; + if details.on_frame { + if details.frame_intercepted { + return Ok(MouseAction::FrameIntercepted { + pane_id: details.pane_id, + }); + } + if let Some(edge) = details.edge { + return Ok(MouseAction::StartResize { + pane_id: details.pane_id, + edge, + is_floating: details.is_floating, + position: event.position, + }); + } + } + return Ok(MouseAction::NoAction); + } + + let is_plain_left_press = + event.left && event.event_type == MouseEventType::Press && !event.ctrl && !event.alt; + if is_plain_left_press { + let Some(details) = &ctx.clicked_pane else { + return Ok(MouseAction::NoAction); + }; + + let is_active_pane = Some(details.pane_id) == ctx.active_pane_id; + let is_pinned_pane = ctx + .pinned_selectable + .map(|id| id == details.pane_id) + .unwrap_or(false); + + if details.on_frame { + if details.frame_intercepted { + return Ok(MouseAction::FrameIntercepted { + pane_id: details.pane_id, + }); + } + + let should_start_moving = ctx.floating_visible || is_pinned_pane; + if should_start_moving { + return Ok(MouseAction::StartMovingFloatingPane { + position: event.position, + }); + } + + if let Some(edge) = details.edge { + return Ok(MouseAction::StartResize { + pane_id: details.pane_id, + edge, + is_floating: false, + position: event.position, + }); + } + } + + if is_active_pane { + if details.terminal_wants_mouse { + return Ok(MouseAction::SendToTerminal { + pane_id: details.pane_id, + event: *event, + }); + } else { + return Ok(MouseAction::StartSelection { + pane_id: details.pane_id, + position: event.position, + }); + } + } + + if !ctx.floating_visible { + if let Some(pinned_id) = ctx.pinned_selectable { + return Ok(MouseAction::ShowFloatingPanesAndFocus { pane_id: pinned_id }); + } + if ctx.pinned_unselectable.is_some() { + return Ok(MouseAction::NoAction); + } + } + + return Ok(MouseAction::FocusPane { + pane_id: details.pane_id, + position: event.position, + }); + } + + if event.right { + let Some(pane_id) = ctx.pane_id_at_position else { + return Ok(MouseAction::NoAction); + }; + let is_active_pane = Some(pane_id) == ctx.active_pane_id; + if is_active_pane { + return Ok(MouseAction::SendToTerminal { + pane_id, + event: *event, + }); + } + return Ok(MouseAction::NoAction); + } + + if event.middle { + let Some(pane_id) = ctx.pane_id_at_position else { + return Ok(MouseAction::NoAction); + }; + let is_active_pane = Some(pane_id) == ctx.active_pane_id; + if is_active_pane { + return Ok(MouseAction::SendToTerminal { + pane_id, + event: *event, + }); + } + return Ok(MouseAction::NoAction); + } + + let is_left_motion_or_release = event.left + && (event.event_type == MouseEventType::Motion + || event.event_type == MouseEventType::Release); + if is_left_motion_or_release { + let Some(details) = &ctx.clicked_pane else { + return Ok(MouseAction::NoAction); + }; + let is_active_pane = Some(details.pane_id) == ctx.active_pane_id; + if is_active_pane && details.terminal_wants_mouse { + return Ok(MouseAction::SendToTerminal { + pane_id: details.pane_id, + event: *event, + }); + } + return Ok(MouseAction::NoAction); + } + + let is_buttonless_motion = event.event_type == MouseEventType::Motion + && !event.left + && !event.right + && !event.middle; + if is_buttonless_motion { + let Some(pane_id) = ctx.pane_id_at_position else { + return Ok(MouseAction::UpdateHover { pane_id: None }); + }; + let is_active_pane = Some(pane_id) == ctx.active_pane_id; + if is_active_pane { + return Ok(MouseAction::SendToTerminal { + pane_id, + event: *event, + }); + } + return Ok(MouseAction::UpdateHover { + pane_id: Some(pane_id), + }); + } + + Ok(MouseAction::NoAction) + } + + fn unselectable_pane_at_position<'a>( + tab: &'a mut Tab, + point: &Position, + ) -> Option<&'a mut Box<dyn Pane>> { + // the repetition in this function is to appease the borrow checker, I don't like it either + let floating_panes_are_visible = tab.floating_panes.panes_are_visible(); + if floating_panes_are_visible { + if let Ok(Some(clicked_pane_id)) = tab.floating_panes.get_pane_id_at(point, true) { + if let Some(pane) = tab.floating_panes.get_pane_mut(clicked_pane_id) { + if !pane.selectable() { + return Some(pane); + } + } + } else if let Ok(Some(clicked_pane_id)) = tab.get_pane_id_at(point, false) { + if let Some(pane) = tab.tiled_panes.get_pane_mut(clicked_pane_id) { + if !pane.selectable() { + return Some(pane); + } + } + } + } else if let Ok(Some(clicked_pane_id)) = tab.get_pane_id_at(point, false) { + if let Some(pane) = tab.tiled_panes.get_pane_mut(clicked_pane_id) { + if !pane.selectable() { + return Some(pane); + } + } + } + None + } + + fn focus_pane_at(tab: &mut Tab, point: &Position, client_id: ClientId) -> Result<()> { + let err_context = + || format!("failed to focus pane at position {point:?} for client {client_id}"); + + if tab.floating_panes.panes_are_visible() { + if let Some(clicked_pane) = tab + .floating_panes + .get_pane_id_at(point, true) + .with_context(err_context)? + { + tab.floating_panes.focus_pane(clicked_pane, client_id); + tab.set_pane_active_at(clicked_pane); + return Ok(()); + } + } + if tab.floating_panes.has_pinned_panes() { + let search_selectable = false; + if let Some(pane_id) = tab + .floating_panes + .get_pinned_pane_id_at(point, search_selectable) + .with_context(err_context)? + { + tab.floating_panes.focus_pane(pane_id, client_id); + tab.set_pane_active_at(pane_id); + tab.show_floating_panes(); + return Ok(()); + } + } + if let Some(clicked_pane) = tab.get_pane_id_at(point, true).with_context(err_context)? { + tab.tiled_panes.focus_pane(clicked_pane, client_id); + tab.set_pane_active_at(clicked_pane); + if tab.floating_panes.panes_are_visible() { + tab.hide_floating_panes(); + tab.set_force_render(); + } + } + Ok(()) + } + + pub fn handle_scrollwheel_up( + tab: &mut Tab, + point: &Position, + lines: usize, + client_id: ClientId, + ) -> Result<MouseEffect> { + let err_context = || { + format!("failed to handle scrollwheel up at position {point:?} for client {client_id}") + }; + + if let Some(pane) = Self::get_pane_at(tab, point, false).with_context(err_context)? { + let relative_position = pane.relative_position(point); + if let Some(mouse_event) = pane.mouse_scroll_up(&relative_position) { + tab.write_to_terminal_at(mouse_event.into_bytes(), point, client_id) + .with_context(err_context)?; + } else if pane.is_alternate_mode_active() { + // faux scrolling, send UP n times + // do n separate writes to make sure the sequence gets adjusted for cursor keys mode + for _ in 0..lines { + tab.write_to_terminal_at("\u{1b}[A".as_bytes().to_owned(), point, client_id) + .with_context(err_context)?; + } + } else { + pane.scroll_up(lines, client_id); + } + } + Ok(MouseEffect::default()) + } + + pub fn handle_scrollwheel_down( + tab: &mut Tab, + point: &Position, + lines: usize, + client_id: ClientId, + ) -> Result<MouseEffect> { + let err_context = || { + format!( + "failed to handle scrollwheel down at position {point:?} for client {client_id}" + ) + }; + + if let Some(pane) = Self::get_pane_at(tab, point, false).with_context(err_context)? { + let relative_position = pane.relative_position(point); + if let Some(mouse_event) = pane.mouse_scroll_down(&relative_position) { + tab.write_to_terminal_at(mouse_event.into_bytes(), point, client_id) + .with_context(err_context)?; + } else if pane.is_alternate_mode_active() { + // faux scrolling, send DOWN n times + // do n separate writes to make sure the sequence gets adjusted for cursor keys mode + for _ in 0..lines { + tab.write_to_terminal_at("\u{1b}[B".as_bytes().to_owned(), point, client_id) + .with_context(err_context)?; + } + } else { + pane.scroll_down(lines, client_id); + if !pane.is_scrolled() { + if let PaneId::Terminal(pid) = pane.pid() { + tab.process_pending_vte_events(pid) + .with_context(err_context)?; + } + } + } + } + Ok(MouseEffect::default()) + } + + fn handle_resize_scroll_up( + tab: &mut Tab, + pane_id: PaneId, + client_id: ClientId, + ) -> Result<MouseEffect> { + let err_context = || format!("failed to handle resize scroll up for pane {pane_id:?}"); + + let is_floating = tab.floating_panes.panes_contain(&pane_id); + + let strategy = ResizeStrategy { + resize: Resize::Increase, + direction: None, + invert_on_boundaries: false, + }; + + if is_floating { + Self::resize_floating_pane_with_strategies(tab, pane_id, &[strategy], (5, 2)) + .with_context(err_context)?; + tab.swap_layouts.set_is_floating_damaged(); + } else { + // we only resize the active pane for tiled panes (better ux) + let active_pane_id = tab + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find active pane"))?; + + Self::resize_tiled_pane_with_stacked_resize(tab, active_pane_id, &strategy) + .with_context(err_context)?; + tab.swap_layouts.set_is_tiled_damaged(); + } + + tab.set_force_render(); + Ok(MouseEffect::state_changed()) + } + + fn handle_resize_scroll_down( + tab: &mut Tab, + pane_id: PaneId, + client_id: ClientId, + ) -> Result<MouseEffect> { + let err_context = || format!("failed to handle resize scroll down for pane {pane_id:?}"); + + let is_floating = tab.floating_panes.panes_contain(&pane_id); + + let strategy = ResizeStrategy { + resize: Resize::Decrease, + direction: None, + invert_on_boundaries: false, + }; + + if is_floating { + Self::resize_floating_pane_with_strategies(tab, pane_id, &[strategy], (5, 2)) + .with_context(err_context)?; + tab.swap_layouts.set_is_floating_damaged(); + } else { + // we only resize the active pane for tiled panes (better ux) + let active_pane_id = tab + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find active pane"))?; + + Self::resize_tiled_pane_with_stacked_resize(tab, active_pane_id, &strategy) + .with_context(err_context)?; + tab.swap_layouts.set_is_tiled_damaged(); + } + + tab.set_force_render(); + Ok(MouseEffect::state_changed()) + } + + fn get_pane_at<'a>( + tab: &'a mut Tab, + point: &Position, + search_selectable: bool, + ) -> Result<Option<&'a mut Box<dyn Pane>>> { + let err_context = || format!("failed to get pane at position {point:?}"); + + if tab.floating_panes.panes_are_visible() { + if let Some(pane_id) = tab + .floating_panes + .get_pane_id_at(point, search_selectable) + .with_context(err_context)? + { + return Ok(tab.floating_panes.get_pane_mut(pane_id)); + } + } else if tab.floating_panes.has_pinned_panes() { + if let Some(pane_id) = tab + .floating_panes + .get_pinned_pane_id_at(point, search_selectable) + .with_context(err_context)? + { + return Ok(tab.floating_panes.get_pane_mut(pane_id)); + } + } + if let Some(pane_id) = tab + .get_pane_id_at(point, search_selectable) + .with_context(err_context)? + { + Ok(tab.tiled_panes.get_pane_mut(pane_id)) + } else { + Ok(None) + } + } + + pub fn set_mouse_selection_support(tab: &mut Tab, pane_id: PaneId, selection_support: bool) { + if let Some(pane) = tab.get_pane_with_id_mut(pane_id) { + pane.set_mouse_selection_support(selection_support); + } + } +} diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_click_on_floating_pin_button_toggles_pin.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_click_on_floating_pin_button_toggles_pin.snap new file mode 100644 index 00000000..53321f60 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_click_on_floating_pin_button_toggles_pin.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ──────────────────────────────────────── PIN [+] ┐ │ +06 (C): │ │Pin toggle test │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └──────────────────────────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_floating_pane_from_corner.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_floating_pane_from_corner.snap new file mode 100644 index 00000000..9999594f --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_floating_pane_from_corner.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ┐ │ +06 (C): │ │Corner re│ │ +07 (C): │ │size test│ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ └─────────┘ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_floating_pane_from_edge.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_floating_pane_from_edge.snap new file mode 100644 index 00000000..c5dd2920 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_floating_pane_from_edge.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ┐ │ +06 (C): │ │Floating │ │ +07 (C): │ │pane │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └─────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_pinned_floating_pane_when_floating_panes_not_shown.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_pinned_floating_pane_when_floating_panes_not_shown.snap new file mode 100644 index 00000000..73f382f2 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_pinned_floating_pane_when_floating_panes_not_shown.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ────── PIN [+] ┐ │ +06 (C): │ │Corner resize test │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ └────────────────────────┘ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_tiled_pane_horizontally.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_tiled_pane_horizontally.snap new file mode 100644 index 00000000..b425c617 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_tiled_pane_horizontally.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────┐┌ Pane #2 ────────────────────────────────────────────────┐ +01 (C): │Left pane content ││Right pane content │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └───────────────────────────────────────────────────────────┘└─────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_tiled_pane_vertically.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_tiled_pane_vertically.snap new file mode 100644 index 00000000..d55bc67c --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_tiled_pane_vertically.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │Top pane │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +11 (C): ┌ Pane #2 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +12 (C): │Bottom pane │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_unfocused_tiled_pane_vertically.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_unfocused_tiled_pane_vertically.snap new file mode 100644 index 00000000..d55bc67c --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_unfocused_tiled_pane_vertically.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │Top pane │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +11 (C): ┌ Pane #2 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +12 (C): │Bottom pane │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_decreases_floating_pane_size.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_decreases_floating_pane_size.snap new file mode 100644 index 00000000..3dee5aa5 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_decreases_floating_pane_size.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │Tiled pane │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ ┌ Pane #2 ──────────────────────────────────── PIN [ ] ┐ │ +07 (C): │ │Floating pane │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ └──────────────────────────────────────────────────────┘ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_decreases_pinned_floating_pane_size_when_floating_panes_hidden.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_decreases_pinned_floating_pane_size_when_floating_panes_hidden.snap new file mode 100644 index 00000000..32011984 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_decreases_pinned_floating_pane_size_when_floating_panes_hidden.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │Tiled pane │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ ┌ Pane #2 ────── PIN [+] ┐ │ +07 (C): │ │Floating pane │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ └────────────────────────┘ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_on_active_tiled_pane_decreases_pane_size.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_on_active_tiled_pane_decreases_pane_size.snap new file mode 100644 index 00000000..883bb2ef --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_on_active_tiled_pane_decreases_pane_size.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ───────────────────────────────────────────────────────┐┌ Pane #2 ───────────────────────────────────────────┐ +01 (C): │Left pane ││Right pane │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └────────────────────────────────────────────────────────────────┘└────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_on_inactive_tiled_pane_decreases_active_tiled_pane_size.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_on_inactive_tiled_pane_decreases_active_tiled_pane_size.snap new file mode 100644 index 00000000..883bb2ef --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_on_inactive_tiled_pane_decreases_active_tiled_pane_size.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ───────────────────────────────────────────────────────┐┌ Pane #2 ───────────────────────────────────────────┐ +01 (C): │Left pane ││Right pane │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └────────────────────────────────────────────────────────────────┘└────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_increases_floating_pane_size.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_increases_floating_pane_size.snap new file mode 100644 index 00000000..6931cea6 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_increases_floating_pane_size.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │Tiled pane │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ ┌ Pane #2 ──────────────────────────────────────────── PIN [ ] ┐ │ +05 (C): │ │Floating pane │ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ └──────────────────────────────────────────────────────────────┘ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_increases_pinned_floating_pane_size_when_floating_panes_hidden.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_increases_pinned_floating_pane_size_when_floating_panes_hidden.snap new file mode 100644 index 00000000..6a704349 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_increases_pinned_floating_pane_size_when_floating_panes_hidden.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │Tiled pane │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ ┌ Pane #2 ────────────── PIN [+] ┐ │ +05 (C): │ │Floating pane │ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ └────────────────────────────────┘ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_on_active_tiled_pane_increases_pane_size.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_on_active_tiled_pane_increases_pane_size.snap new file mode 100644 index 00000000..895a9ba6 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_on_active_tiled_pane_increases_pane_size.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ───────────────────────────────────────────┐┌ Pane #2 ───────────────────────────────────────────────────────┐ +01 (C): │Left pane ││Right pane │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └────────────────────────────────────────────────────┘└────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_on_inactive_tiled_pane_increases_active_tiled_pane_size.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_on_inactive_tiled_pane_increases_active_tiled_pane_size.snap new file mode 100644 index 00000000..895a9ba6 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_on_inactive_tiled_pane_increases_active_tiled_pane_size.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ───────────────────────────────────────────┐┌ Pane #2 ───────────────────────────────────────────────────────┐ +01 (C): │Left pane ││Right pane │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └────────────────────────────────────────────────────┘└────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__drag_pane_with_mouse.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__drag_pane_with_mouse.snap index 7450c0fd..53882774 100644 --- a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__drag_pane_with_mouse.snap +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__drag_pane_with_mouse.snap @@ -1,6 +1,5 @@ --- source: zellij-server/src/tab/./unit/tab_integration_tests.rs -assertion_line: 1944 expression: snapshot --- 00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -10,17 +9,16 @@ expression: snapshot 04 (C): │ │E┌ Pane #6 ───── SCROLL: 0/1 | PIN [ ] ┐ │ 05 (C): │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ 06 (C): │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ -07 (C): │ └─│EEEEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ──────────────────────────────────────── PIN [ ] ┐ │ -08 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ │ -09 (C): │ └─────────────────────────────│ │ │ -10 (C): │ │E│ │ │ -11 (C): │ │E│ I am scratch terminal │ │ -12 (C): │ │E│ │ │ -13 (C): │ │E│ │ │ -14 (C): │ │E│ │ │ -15 (C): │ │E│ │ │ -16 (C): │ └─└──────────────────────────────────────────────────────────┘ │ +07 (C): │ └─│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│─────────────────────── SCROLL: 0/1 | PIN [ ] ┐ ┐ │ +08 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ │ +09 (C): │ └──────────────────────────────────────┘───────────────────────── SCROLL: 0/1 | PIN [ ] ┐ │ +10 (C): │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +11 (C): │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +12 (C): │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +13 (C): │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +14 (C): │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +15 (C): │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +16 (C): │ └─│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ 17 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ 18 (C): │ └──────────────────────────────────────────────────────────┘ │ 19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_active_pane_unsets_hover.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_active_pane_unsets_hover.snap new file mode 100644 index 00000000..1499546a --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_active_pane_unsets_hover.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{:?}\", hover_panes)" +--- +{} diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_inactive_pane_sets_hover_state.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_inactive_pane_sets_hover_state.snap new file mode 100644 index 00000000..9d87b33c --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_inactive_pane_sets_hover_state.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{:?}\", hover_panes)" +--- +{1: Terminal(1)} diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_click_on_floating_frame_not_on_pin_moves_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_click_on_floating_frame_not_on_pin_moves_pane.snap new file mode 100644 index 00000000..678d7a5e --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_click_on_floating_frame_not_on_pin_moves_pane.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ ┌ Pane #2 ──────────────────────────────────────── PIN [ ] ┐ │ +07 (C): │ │Movable pane │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ └──────────────────────────────────────────────────────────┘ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_click_on_pinned_floating_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_click_on_pinned_floating_pane.snap new file mode 100644 index 00000000..77f8a953 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_click_on_pinned_floating_pane.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ────────── PIN [+] ┐ ┌ Pane #3 ────────── PIN [ ] ┐ │ +06 (C): │ │Pinned pane │ │ │ │ +07 (C): │ │ │ │ │ │ +08 (C): │ │ │ │ │ │ +09 (C): │ │ │ │ │ │ +10 (C): │ │ │ │ │ │ +11 (C): │ │ │ │ │ │ +12 (C): │ │ │ │ │ │ +13 (C): │ │ │ │ │ │ +14 (C): │ └────────────────────────────┘ └────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_drag_moves_floating_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_drag_moves_floating_pane.snap new file mode 100644 index 00000000..860e1d6d --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_drag_moves_floating_pane.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ ┌ Pane #2 ──────────────────────────────────────── PIN [ ] ┐ │ +07 (C): │ │Moving pane │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ └──────────────────────────────────────────────────────────┘ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_tiled_then_move_floating_sequence.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_tiled_then_move_floating_sequence.snap new file mode 100644 index 00000000..4dbbeaa3 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_tiled_then_move_floating_sequence.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #1 ───────────────────────────────────────────────────────────┐┌ Pane #2 ────────────────────────────────────────┐ +01 (C): │Tiled Left ││Tiled Right │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ┌ Pane #3 ──────────────────────────────────────── PIN [ ] ┐ │ +09 (C): │ │Floating │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ │ │ │ +16 (C): │ │ │ │ +17 (C): │ └──────────────────────────────────────────────────────────┘ │ +18 (C): │ ││ │ +19 (C): └────────────────────────────────────────────────────────────────────┘└─────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_on_inactive_pane_scrolls_that_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_on_inactive_pane_scrolls_that_pane.snap new file mode 100644 index 00000000..ac16ea2e --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_on_inactive_pane_scrolls_that_pane.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ─────────────────────────────────── SCROLL: 3/33 ┐ +01 (C): │Left Line 30 │ +02 (C): │Left Line 31 │ +03 (C): │Left Line 32 │ +04 (C): │Left Line 33 │ +05 (C): │Left Line 34 │ +06 (C): │Left Line 35 │ +07 (C): │Left Line 36 │ +08 (C): │Left Line 37 │ +09 (C): │Left Line 38 │ +10 (C): │Left Line 39 │ +11 (C): │Left Line 40 │ +12 (C): │Left Line 41 │ +13 (C): │Left Line 42 │ +14 (C): │Left Line 43 │ +15 (C): │Left Line 44 │ +16 (C): │Left Line 45 │ +17 (C): │Left Line 46 │ +18 (C): │Left Line 47 │ +19 (C): └───────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_wheel_down_scrolls_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_wheel_down_scrolls_pane.snap new file mode 100644 index 00000000..c17b8c7f --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_wheel_down_scrolls_pane.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────── SCROLL: 6/33 ┐ +01 (C): │Line 27 │ +02 (C): │Line 28 │ +03 (C): │Line 29 │ +04 (C): │Line 30 │ +05 (C): │Line 31 │ +06 (C): │Line 32 │ +07 (C): │Line 33 │ +08 (C): │Line 34 │ +09 (C): │Line 35 │ +10 (C): │Line 36 │ +11 (C): │Line 37 │ +12 (C): │Line 38 │ +13 (C): │Line 39 │ +14 (C): │Line 40 │ +15 (C): │Line 41 │ +16 (C): │Line 42 │ +17 (C): │Line 43 │ +18 (C): │Line 44 │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_wheel_up_scrolls_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_wheel_up_scrolls_pane.snap new file mode 100644 index 00000000..1ec7adee --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_wheel_up_scrolls_pane.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: "format!(\"{}\", snapshot_after)" +--- +00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────── SCROLL: 3/33 ┐ +01 (C): │Line 30 │ +02 (C): │Line 31 │ +03 (C): │Line 32 │ +04 (C): │Line 33 │ +05 (C): │Line 34 │ +06 (C): │Line 35 │ +07 (C): │Line 36 │ +08 (C): │Line 37 │ +09 (C): │Line 38 │ +10 (C): │Line 39 │ +11 (C): │Line 40 │ +12 (C): │Line 41 │ +13 (C): │Line 42 │ +14 (C): │Line 43 │ +15 (C): │Line 44 │ +16 (C): │Line 45 │ +17 (C): │Line 46 │ +18 (C): │Line 47 │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 8b109def..7b75223a 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -275,6 +275,7 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab { current_group, currently_marking_pane_group, advanced_mouse_actions, + true, // mouse_hover_effects web_server_ip, web_server_port, ); @@ -359,6 +360,7 @@ fn create_new_tab_without_pane_frames(size: Size, default_mode: ModeInfo) -> Tab current_group, currently_marking_pane_group, advanced_mouse_actions, + true, // mouse_hover_effects web_server_ip, web_server_port, ); @@ -458,6 +460,7 @@ fn create_new_tab_with_swap_layouts( current_group, currently_marking_pane_group, advanced_mouse_actions, + true, // mouse_hover_effects web_server_ip, web_server_port, ); @@ -558,6 +561,7 @@ fn create_new_tab_with_os_api( current_group, currently_marking_pane_group, advanced_mouse_actions, + true, // mouse_hover_effects web_server_ip, web_server_port, ); @@ -644,6 +648,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str) current_group, currently_marking_pane_group, advanced_mouse_actions, + true, // mouse_hover_effects web_server_ip, web_server_port, ); @@ -744,6 +749,7 @@ fn create_new_tab_with_mock_pty_writer( current_group, currently_marking_pane_group, advanced_mouse_actions, + true, // mouse_hover_effects web_server_ip, web_server_port, ); @@ -835,6 +841,7 @@ fn create_new_tab_with_sixel_support( current_group, currently_marking_pane_group, advanced_mouse_actions, + true, // mouse_hover_effects web_server_ip, web_server_port, ); @@ -2381,7 +2388,7 @@ fn move_floating_pane_focus_with_mouse() { client_id, ) .unwrap(); - tab.handle_left_mouse_release( + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(9, 71)), client_id, ) @@ -2491,7 +2498,7 @@ fn move_pane_focus_with_mouse_to_non_floating_pane() { client_id, ) .unwrap(); - tab.handle_left_mouse_release( + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(4, 71)), client_id, ) @@ -2601,7 +2608,12 @@ fn drag_pane_with_mouse() { client_id, ) .unwrap(); - tab.handle_left_mouse_release( + tab.handle_mouse_event( + &MouseEvent::new_left_motion_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) @@ -2613,11 +2625,6 @@ fn drag_pane_with_mouse() { size.cols, Palette::default(), ); - assert_eq!( - cursor_coordinates, - Some((75, 11)), - "cursor coordinates moved to the clicked pane" - ); assert_snapshot!(snapshot); } @@ -2715,7 +2722,7 @@ fn mark_text_inside_floating_pane() { tab.selecting_with_mouse_in_pane.is_some(), "started selecting with mouse on click" ); - tab.handle_left_mouse_release( + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(5, 15)), client_id, ) @@ -3400,7 +3407,12 @@ fn move_floating_pane_with_sixel_image() { client_id, ) .unwrap(); - tab.handle_left_mouse_release( + tab.handle_mouse_event( + &MouseEvent::new_left_motion_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) @@ -3454,7 +3466,12 @@ fn floating_pane_above_sixel_image() { client_id, ) .unwrap(); - tab.handle_left_mouse_release( + tab.handle_mouse_event( + &MouseEvent::new_left_motion_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) @@ -3890,17 +3907,17 @@ fn pane_in_sgr_button_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_left_mouse_motion( + tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); - tab.handle_left_mouse_release( + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); - tab.handle_right_click( + tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) @@ -3910,9 +3927,13 @@ fn pane_in_sgr_button_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_right_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_middle_click( + // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_right_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) @@ -3922,8 +3943,12 @@ fn pane_in_sgr_button_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_middle_release_event(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) @@ -3973,17 +3998,17 @@ fn pane_in_sgr_normal_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_left_mouse_motion( + tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); - tab.handle_left_mouse_release( + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); - tab.handle_right_click( + tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) @@ -3993,9 +4018,13 @@ fn pane_in_sgr_normal_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_right_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_middle_click( + // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_right_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) @@ -4005,8 +4034,12 @@ fn pane_in_sgr_normal_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_middle_release_event(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) @@ -4056,17 +4089,17 @@ fn pane_in_sgr_any_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_left_mouse_motion( + tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); - tab.handle_left_mouse_release( + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); - tab.handle_right_click( + tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) @@ -4076,9 +4109,13 @@ fn pane_in_sgr_any_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_right_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_middle_click( + // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_right_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) @@ -4088,8 +4125,12 @@ fn pane_in_sgr_any_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_middle_release_event(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) @@ -4145,17 +4186,17 @@ fn pane_in_utf8_button_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_left_mouse_motion( + tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); - tab.handle_left_mouse_release( + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); - tab.handle_right_click( + tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) @@ -4165,9 +4206,13 @@ fn pane_in_utf8_button_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_right_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_middle_click( + // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_right_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) @@ -4177,8 +4222,12 @@ fn pane_in_utf8_button_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_middle_release_event(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) @@ -4228,17 +4277,17 @@ fn pane_in_utf8_normal_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_left_mouse_motion( + tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); - tab.handle_left_mouse_release( + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); - tab.handle_right_click( + tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) @@ -4248,9 +4297,13 @@ fn pane_in_utf8_normal_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_right_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_middle_click( + // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_right_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) @@ -4260,8 +4313,12 @@ fn pane_in_utf8_normal_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_middle_release_event(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) @@ -4311,17 +4368,17 @@ fn pane_in_utf8_any_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_left_mouse_motion( + tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); - tab.handle_left_mouse_release( + tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); - tab.handle_right_click( + tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) @@ -4331,9 +4388,13 @@ fn pane_in_utf8_any_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_right_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_middle_click( + // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_right_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) @@ -4343,8 +4404,12 @@ fn pane_in_utf8_any_event_tracking_mouse_mode() { client_id, ) .unwrap(); - tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) + tab.handle_mouse_event( + &MouseEvent::new_middle_release_event(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) @@ -7203,6 +7268,11 @@ fn focus_stacked_pane_over_flexible_pane_with_the_mouse() { client_id, ) .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_release_event(Position::new(1, 71)), + client_id, + ) + .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), @@ -7427,10 +7497,20 @@ fn close_stacked_pane_with_previously_focused_other_pane() { ) .unwrap(); tab.handle_mouse_event( + &MouseEvent::new_left_release_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.handle_mouse_event( + &MouseEvent::new_left_release_event(Position::new(1, 71)), + client_id, + ) + .unwrap(); tab.close_pane(PaneId::Terminal(4), false, None); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( @@ -10206,3 +10286,2054 @@ fn cursor_visible_when_pinned_pane_is_focused() { "Cursor should be visible when focused on pinned pane that's on top" ); } + +#[test] +fn test_left_click_on_inactive_tiled_pane_changes_focus() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left pane".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right pane".as_bytes())) + .unwrap(); + + assert_eq!(tab.get_active_pane_id(client_id), Some(new_pane_id)); + + let effect = tab + .handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 30)), + client_id, + ) + .unwrap(); + + assert!(effect.state_changed); + assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); +} + +#[test] +fn test_left_release_after_selection_copies_to_clipboard() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let mut output = Output::default(); + + // Fill the pane with some text content + tab.handle_pty_bytes(1, Vec::from("Selectable text content here".as_bytes())) + .unwrap(); + + // Start text selection with left press + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(1, 5)), + client_id, + ) + .unwrap(); + + // Drag to create selection + tab.handle_mouse_event( + &MouseEvent::new_left_motion_event(Position::new(1, 15)), + client_id, + ) + .unwrap(); + + // Release should trigger clipboard copy + let release_effect = tab + .handle_mouse_event( + &MouseEvent::new_left_release_event(Position::new(1, 15)), + client_id, + ) + .unwrap(); + + // Verify clipboard message was sent + assert!(release_effect.leave_clipboard_message); +} + +#[test] +fn test_ctrl_click_on_tiled_pane_edge_starts_resize() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Create a vertical split + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) + .unwrap(); + + // Click on the edge between panes with Ctrl + // For a vertical split at cols=121, the edge is around column 60-61 + let edge_position = Position::new(5, 60); + let effect = tab + .handle_mouse_event( + &MouseEvent::new_left_press_with_ctrl_event(edge_position), + client_id, + ) + .unwrap(); + + // Verify resize started + assert!(effect.state_changed); +} + +#[test] +fn test_ctrl_drag_resizes_tiled_pane_horizontally() { + let size = Size { + cols: 120, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left pane content".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right pane content".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let edge_position = Position::new(5, 60); + tab.handle_mouse_event( + &MouseEvent::new_left_press_with_ctrl_event(edge_position), + client_id, + ) + .unwrap(); + + let motion_position = Position::new(5, 61); + let motion_effect = tab + .handle_mouse_event( + &MouseEvent::new_left_motion_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + tab.handle_mouse_event( + &MouseEvent::new_left_release_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + assert!(motion_effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_drag_resizes_tiled_pane_vertically() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + tab.horizontal_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Top pane".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Bottom pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let edge_position = Position::new(10, 60); + tab.handle_mouse_event( + &MouseEvent::new_left_press_with_ctrl_event(edge_position), + client_id, + ) + .unwrap(); + + let motion_position = Position::new(11, 60); + let motion_effect = tab + .handle_mouse_event( + &MouseEvent::new_left_motion_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + tab.handle_mouse_event( + &MouseEvent::new_left_release_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + assert!(motion_effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_drag_resizes_unfocused_tiled_pane_vertically() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + tab.horizontal_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Top pane".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Bottom pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let edge_position = Position::new(10, 60); + let _ = tab.focus_pane_with_id(PaneId::Terminal(1), false, false, client_id); // focus top pane + tab.handle_mouse_event( + &MouseEvent::new_left_press_with_ctrl_event(edge_position), + client_id, + ) + .unwrap(); + + let motion_position = Position::new(11, 60); + let motion_effect = tab + .handle_mouse_event( + &MouseEvent::new_left_motion_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + tab.handle_mouse_event( + &MouseEvent::new_left_release_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + assert!(motion_effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + let focused_pane = tab.get_active_pane_id(client_id).unwrap(); + + assert_ne!(focused_pane, new_pane_id, "resize did not change focus"); + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_click_on_pane_body_does_nothing() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Create a vertical split + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) + .unwrap(); + + // Click in the center of a pane (not on an edge) + let center_position = Position::new(10, 30); + let effect = tab + .handle_mouse_event( + &MouseEvent::new_left_press_with_ctrl_event(center_position), + client_id, + ) + .unwrap(); + + // Should not trigger any state change + assert!(!effect.state_changed); +} + +#[test] +fn test_ctrl_drag_resizes_floating_pane_from_edge() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let floating_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + let coordinates = FloatingPaneCoordinates { + x: Some(PercentOrFixed::Fixed(5)), + y: Some(PercentOrFixed::Fixed(5)), + width: Some(PercentOrFixed::Fixed(10)), + height: Some(PercentOrFixed::Fixed(10)), + pinned: None, + borderless: Some(false), + }; + + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + floating_pane_id, + None, + None, + false, + true, + NewPanePlacement::Floating(Some(coordinates)), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let edge_position = Position::new(7, 14); // Approximate right edge + tab.handle_mouse_event( + &MouseEvent::new_left_press_with_ctrl_event(edge_position), + client_id, + ) + .unwrap(); + + let motion_position = Position::new(7, 15); + let motion_effect = tab + .handle_mouse_event( + &MouseEvent::new_left_motion_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + tab.handle_mouse_event( + &MouseEvent::new_left_release_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + assert!(motion_effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_drag_resizes_floating_pane_from_corner() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let floating_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + let coordinates = FloatingPaneCoordinates { + x: Some(PercentOrFixed::Fixed(5)), + y: Some(PercentOrFixed::Fixed(5)), + width: Some(PercentOrFixed::Fixed(10)), + height: Some(PercentOrFixed::Fixed(10)), + pinned: None, + borderless: Some(false), + }; + + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + floating_pane_id, + None, + None, + false, + true, + NewPanePlacement::Floating(Some(coordinates)), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(2, Vec::from("Corner resize test".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let corner_position = Position::new(14, 14); + tab.handle_mouse_event( + &MouseEvent::new_left_press_with_ctrl_event(corner_position), + client_id, + ) + .unwrap(); + + let motion_position = Position::new(15, 15); + let motion_effect = tab + .handle_mouse_event( + &MouseEvent::new_left_motion_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + tab.handle_mouse_event( + &MouseEvent::new_left_release_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + assert!(motion_effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_drag_resizes_pinned_floating_pane_when_floating_panes_not_shown() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let floating_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + let coordinates = FloatingPaneCoordinates { + x: Some(PercentOrFixed::Fixed(5)), + y: Some(PercentOrFixed::Fixed(5)), + width: Some(PercentOrFixed::Fixed(25)), + height: Some(PercentOrFixed::Fixed(10)), + pinned: Some(true), + borderless: Some(false), + }; + + tab.new_pane( + floating_pane_id, + None, + None, + false, + true, + NewPanePlacement::Floating(Some(coordinates)), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(2, Vec::from("Corner resize test".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + eprintln!("{}", snapshot_before); + + let corner_position = Position::new(14, 29); + tab.handle_mouse_event( + &MouseEvent::new_left_press_with_ctrl_event(corner_position), + client_id, + ) + .unwrap(); + + let motion_position = Position::new(15, 30); + let motion_effect = tab + .handle_mouse_event( + &MouseEvent::new_left_motion_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + tab.handle_mouse_event( + &MouseEvent::new_left_release_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + assert!(motion_effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + eprintln!("{}", snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_click_on_floating_pin_button_toggles_pin() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let floating_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Toggle floating panes mode and create a floating pane + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + floating_pane_id, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(2, Vec::from("Pin toggle test".as_bytes())) + .unwrap(); + + // Render before pin toggle + tab.render(&mut output, None).unwrap(); + + // Calculate pin button position (3 cells left of top-right corner of floating pane) + // Default floating pane is roughly at y=5, right edge around x=90 + // Pin button should be at approximately (5, 87) + let pin_position = Position::new(5, 87); + + let effect = tab + .handle_mouse_event( + &MouseEvent::new_left_press_with_ctrl_event(pin_position), + client_id, + ) + .unwrap(); + + // Verify state changed (pin toggle should trigger this) + assert!(effect.state_changed); + + // Render after pin toggle + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + // Snapshot should show pin indicator change + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_click_on_floating_frame_not_on_pin_starts_resize() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let floating_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Toggle floating panes mode and create a floating pane + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + floating_pane_id, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(2, Vec::from("Frame click test".as_bytes())) + .unwrap(); + + // Click on frame but NOT on pin button location + // Use left edge of frame instead + let frame_position = Position::new(5, 30); + + let effect = tab + .handle_mouse_event( + &MouseEvent::new_left_press_with_ctrl_event(frame_position), + client_id, + ) + .unwrap(); + + // Should start resize (if on edge), state_changed should be true + assert!(effect.state_changed); +} + +#[test] +fn test_left_drag_moves_floating_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let floating_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Toggle floating panes mode and create a floating pane + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + floating_pane_id, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(2, Vec::from("Moving pane".as_bytes())) + .unwrap(); + + // Render before move + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + // Click on frame (not ctrl, not pin location) to start move + let frame_position = Position::new(5, 30); + let press_effect = tab + .handle_mouse_event(&MouseEvent::new_left_press_event(frame_position), client_id) + .unwrap(); + + // Drag to move the pane + let motion_position = Position::new(6, 31); + tab.handle_mouse_event( + &MouseEvent::new_left_motion_event(motion_position), + client_id, + ) + .unwrap(); + + // Release + tab.handle_mouse_event( + &MouseEvent::new_left_release_event(motion_position), + client_id, + ) + .unwrap(); + + // Verify state changed + assert!(press_effect.state_changed); + + // Render after move + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + // Snapshots should be different + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_left_click_on_floating_pane_changes_focus() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let floating_pane_id_1 = PaneId::Terminal(2); + let floating_pane_id_2 = PaneId::Terminal(3); + + let coordinates_1 = FloatingPaneCoordinates { + x: Some(PercentOrFixed::Fixed(5)), + y: Some(PercentOrFixed::Fixed(5)), + width: Some(PercentOrFixed::Fixed(10)), + height: Some(PercentOrFixed::Fixed(10)), + pinned: None, + borderless: Some(false), + }; + + let coordinates_2 = FloatingPaneCoordinates { + x: Some(PercentOrFixed::Fixed(20)), + y: Some(PercentOrFixed::Fixed(5)), + width: Some(PercentOrFixed::Fixed(10)), + height: Some(PercentOrFixed::Fixed(10)), + pinned: None, + borderless: Some(false), + }; + + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + floating_pane_id_1, + None, + None, + false, + true, + NewPanePlacement::Floating(Some(coordinates_1)), + Some(client_id), + None, + ) + .unwrap(); + tab.new_pane( + floating_pane_id_2, + None, + None, + false, + true, + NewPanePlacement::Floating(Some(coordinates_2)), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(2, Vec::from("Pane 1".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(3, Vec::from("Pane 2".as_bytes())) + .unwrap(); + + assert_eq!(tab.get_active_pane_id(client_id), Some(floating_pane_id_2)); + + let pane1_position = Position::new(7, 7); + let effect = tab + .handle_mouse_event(&MouseEvent::new_left_press_event(pane1_position), client_id) + .unwrap(); + + assert!(effect.state_changed); + assert_eq!(tab.get_active_pane_id(client_id), Some(floating_pane_id_1)); +} + +#[test] +fn test_left_click_on_pinned_floating_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let floating_pane_id_1 = PaneId::Terminal(2); + let floating_pane_id_2 = PaneId::Terminal(3); + let mut output = Output::default(); + + let coordinates_1 = FloatingPaneCoordinates { + x: Some(PercentOrFixed::Fixed(5)), + y: Some(PercentOrFixed::Fixed(5)), + width: Some(PercentOrFixed::Fixed(30)), + height: Some(PercentOrFixed::Fixed(10)), + pinned: None, + borderless: Some(false), + }; + + let coordinates_2 = FloatingPaneCoordinates { + x: Some(PercentOrFixed::Fixed(60)), + y: Some(PercentOrFixed::Fixed(5)), + width: Some(PercentOrFixed::Fixed(30)), + height: Some(PercentOrFixed::Fixed(10)), + pinned: None, + borderless: Some(false), + }; + + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + + // will be pinned + tab.new_pane( + floating_pane_id_1, + None, + None, + false, + true, + NewPanePlacement::Floating(Some(coordinates_1)), + Some(client_id), + None, + ) + .unwrap(); + + // will not be pinned + tab.new_pane( + floating_pane_id_2, + None, + None, + false, + true, + NewPanePlacement::Floating(Some(coordinates_2)), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(2, Vec::from("Pinned pane".as_bytes())) + .unwrap(); + + // render once so that the client's frame will be registered + tab.render(&mut output, None).unwrap(); + + let pin_position = Position::new(5, 31); + tab.handle_mouse_event(&MouseEvent::new_left_press_event(pin_position), client_id) + .unwrap(); + + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + + let pane_position = Position::new(6, 25); + let _effect = tab + .handle_mouse_event(&MouseEvent::new_left_press_event(pane_position), client_id) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + // Verify pinned pane is still visible in snapshot + assert_snapshot!(format!("{}", snapshot)); +} + +#[test] +fn test_alt_left_click_toggles_pane_group() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Create a vertical split + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) + .unwrap(); + + // Alt+click on a pane to toggle group + let effect = tab + .handle_mouse_event( + &MouseEvent::new_left_press_with_alt_event(Position::new(5, 30)), + client_id, + ) + .unwrap(); + + // Verify group_toggle effect returned + assert!(effect.group_toggle.is_some()); + assert_eq!(effect.group_toggle, Some(PaneId::Terminal(1))); +} + +#[test] +fn test_alt_left_drag_adds_panes_to_group() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id_2 = PaneId::Terminal(2); + let new_pane_id_3 = PaneId::Terminal(3); + let mut output = Output::default(); + + // Create two vertical splits (3 panes) + tab.vertical_split(new_pane_id_2, None, client_id, None, None) + .unwrap(); + tab.vertical_split(new_pane_id_3, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Pane 1".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Pane 2".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(3, Vec::from("Pane 3".as_bytes())) + .unwrap(); + + // Alt+press on first pane + let press_effect = tab + .handle_mouse_event( + &MouseEvent::new_left_press_with_alt_event(Position::new(5, 10)), + client_id, + ) + .unwrap(); + assert!(press_effect.group_toggle.is_some()); + + // Alt+drag to second pane + let motion_effect_1 = tab + .handle_mouse_event( + &MouseEvent::new_left_motion_with_alt_event(Position::new(5, 40)), + client_id, + ) + .unwrap(); + + // Verify group_add effect + assert!(motion_effect_1.group_add.is_some()); + + // Alt+drag to third pane + let motion_effect_2 = tab + .handle_mouse_event( + &MouseEvent::new_left_motion_with_alt_event(Position::new(5, 80)), + client_id, + ) + .unwrap(); + + // Verify group_add effect + assert!(motion_effect_2.group_add.is_some()); +} + +#[test] +fn test_right_alt_click_ungroups_panes() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Create a vertical split + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) + .unwrap(); + + // First, create a group with Alt+click + tab.handle_mouse_event( + &MouseEvent::new_left_press_with_alt_event(Position::new(5, 30)), + client_id, + ) + .unwrap(); + + // Right+Alt click to ungroup + let effect = tab + .handle_mouse_event( + &MouseEvent::new_right_press_with_alt_event(Position::new(5, 30)), + client_id, + ) + .unwrap(); + + // Verify ungroup effect returned + assert!(effect.ungroup); +} + +#[test] +fn test_scroll_wheel_up_scrolls_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let mut output = Output::default(); + + // Fill the pane with many lines to enable scrolling + let mut content = String::new(); + for i in 0..50 { + content.push_str(&format!("Line {}\r\n", i)); + } + tab.handle_pty_bytes(1, Vec::from(content.as_bytes())) + .unwrap(); + + // Render before scroll + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + // Scroll up + let effect = tab + .handle_mouse_event( + &MouseEvent::new_scroll_up_event(Position::new(10, 60)), + client_id, + ) + .unwrap(); + + // Render after scroll + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + // Snapshots should be different (viewport moved) + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_scroll_wheel_down_scrolls_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let mut output = Output::default(); + + // Fill the pane with many lines to enable scrolling + let mut content = String::new(); + for i in 0..50 { + content.push_str(&format!("Line {}\r\n", i)); + } + tab.handle_pty_bytes(1, Vec::from(content.as_bytes())) + .unwrap(); + + // First scroll up to have content above viewport + tab.handle_mouse_event( + &MouseEvent::new_scroll_up_event(Position::new(10, 60)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_scroll_up_event(Position::new(10, 60)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_scroll_up_event(Position::new(10, 60)), + client_id, + ) + .unwrap(); + + // Render before scrolling down + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + // Scroll down + let effect = tab + .handle_mouse_event( + &MouseEvent::new_scroll_down_event(Position::new(10, 60)), + client_id, + ) + .unwrap(); + + // Render after scroll + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + // Snapshots should be different (viewport moved) + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_scroll_on_inactive_pane_scrolls_that_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Create a vertical split + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + // Fill both panes with scrollable content + let mut content = String::new(); + for i in 0..50 { + content.push_str(&format!("Left Line {}\r\n", i)); + } + tab.handle_pty_bytes(1, Vec::from(content.as_bytes())) + .unwrap(); + + let mut content = String::new(); + for i in 0..50 { + content.push_str(&format!("Right Line {}\r\n", i)); + } + tab.handle_pty_bytes(2, Vec::from(content.as_bytes())) + .unwrap(); + + // Active pane is pane 2 (right), scroll on pane 1 (left) + assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); + + // Render before scroll + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + // Scroll up on the left (inactive) pane + let effect = tab + .handle_mouse_event( + &MouseEvent::new_scroll_up_event(Position::new(10, 30)), + client_id, + ) + .unwrap(); + + // Render after scroll + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + // Snapshots should be different (left pane scrolled) + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_right_click_forwards_to_active_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let mut output = Output::default(); + + tab.handle_pty_bytes(1, Vec::from("Active pane".as_bytes())) + .unwrap(); + + // Right click on the active pane + let effect = tab + .handle_mouse_event( + &MouseEvent::new_right_press_event(Position::new(10, 60)), + client_id, + ) + .unwrap(); + + // Event should be forwarded (verified via MouseEffect or no error) + // The effect may not indicate state change, but should succeed + assert!(effect.group_toggle.is_none()); +} + +#[test] +fn test_middle_click_forwards_to_active_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let mut output = Output::default(); + + tab.handle_pty_bytes(1, Vec::from("Active pane".as_bytes())) + .unwrap(); + + // Middle click on the active pane + let effect = tab + .handle_mouse_event( + &MouseEvent::new_middle_press_event(Position::new(10, 60)), + client_id, + ) + .unwrap(); + + // Event should be forwarded (verified via MouseEffect or no error) + assert!(effect.group_toggle.is_none()); +} + +#[test] +fn test_hover_over_inactive_pane_sets_hover_state() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Create a vertical split + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) + .unwrap(); + + // Active pane is pane 2 (right) + assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); + + // Hover over inactive pane (left) with buttonless motion + let effect = tab + .handle_mouse_event( + &MouseEvent::new_buttonless_motion(Position::new(10, 30)), + client_id, + ) + .unwrap(); + + let hover_panes = tab.query_mouse_hover_pane_id(); + assert_snapshot!(format!("{:?}", hover_panes)); + assert!(effect.state_changed); +} + +#[test] +fn test_hover_over_active_pane_unsets_hover() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Create a vertical split + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) + .unwrap(); + + // Active pane is pane 2 (right) + assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); + + // Hover over inactive pane (left) with buttonless motion + let _effect = tab + .handle_mouse_event( + &MouseEvent::new_buttonless_motion(Position::new(10, 30)), + client_id, + ) + .unwrap(); + // Hover over active pane (right) with buttonless motion + let effect = tab + .handle_mouse_event( + &MouseEvent::new_buttonless_motion(Position::new(10, 90)), + client_id, + ) + .unwrap(); + + let hover_panes = tab.query_mouse_hover_pane_id(); + assert_snapshot!(format!("{:?}", hover_panes)); + assert!(effect.state_changed); +} + +#[test] +fn test_resize_tiled_then_move_floating_sequence() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let tiled_pane_id = PaneId::Terminal(2); + let floating_pane_id = PaneId::Terminal(3); + let mut output = Output::default(); + + // Create tiled split + tab.vertical_split(tiled_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Tiled Left".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Tiled Right".as_bytes())) + .unwrap(); + + // Resize tiled pane with Ctrl+drag + let edge_position = Position::new(5, 60); + tab.handle_mouse_event( + &MouseEvent::new_left_press_with_ctrl_event(edge_position), + client_id, + ) + .unwrap(); + let motion_position = Position::new(5, 70); + let resize_effect = tab + .handle_mouse_event( + &MouseEvent::new_left_motion_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_release_with_ctrl_event(motion_position), + client_id, + ) + .unwrap(); + + assert!(resize_effect.state_changed); + + // Now toggle floating and create a floating pane + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + floating_pane_id, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(3, Vec::from("Floating".as_bytes())) + .unwrap(); + + // Move floating pane + let frame_position = Position::new(5, 30); + let move_effect = tab + .handle_mouse_event(&MouseEvent::new_left_press_event(frame_position), client_id) + .unwrap(); + let motion_position = Position::new(8, 40); + tab.handle_mouse_event( + &MouseEvent::new_left_motion_event(motion_position), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_release_event(motion_position), + client_id, + ) + .unwrap(); + + // Both operations should have succeeded + assert!(move_effect.state_changed); + + // Render final state + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_snapshot!(format!("{}", snapshot)); +} + +#[test] +fn test_alt_click_works_on_floating_panes() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let floating_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Toggle floating panes mode and create a floating pane + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + floating_pane_id, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) + .unwrap(); + + // Alt+click on floating pane to toggle group + let effect = tab + .handle_mouse_event( + &MouseEvent::new_left_press_with_alt_event(Position::new(10, 60)), + client_id, + ) + .unwrap(); + + // Verify group_toggle effect returned + assert!(effect.group_toggle.is_some()); + assert_eq!(effect.group_toggle, Some(PaneId::Terminal(2))); +} + +#[test] +fn test_left_click_on_tiled_frame_edge_starts_resize() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + // Create a vertical split + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) + .unwrap(); + + // Click on the edge between panes (without Ctrl) + let edge_position = Position::new(5, 60); + let effect = tab + .handle_mouse_event(&MouseEvent::new_left_press_event(edge_position), client_id) + .unwrap(); + + // Edge detection should start resize even without Ctrl for tiled panes + assert!(effect.state_changed); +} + +#[test] +fn test_left_click_on_floating_frame_not_on_pin_moves_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let floating_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + floating_pane_id, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(2, Vec::from("Movable pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let frame_position = Position::new(5, 30); + let press_effect = tab + .handle_mouse_event(&MouseEvent::new_left_press_event(frame_position), client_id) + .unwrap(); + + let motion_position = Position::new(6, 31); + tab.handle_mouse_event( + &MouseEvent::new_left_motion_event(motion_position), + client_id, + ) + .unwrap(); + + tab.handle_mouse_event( + &MouseEvent::new_left_release_event(motion_position), + client_id, + ) + .unwrap(); + + assert!(press_effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_scroll_up_on_active_tiled_pane_increases_pane_size() { + let size = Size { + cols: 120, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left pane".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let active_pane_position = Position::new(5, 70); + let effect = tab + .handle_mouse_event( + &MouseEvent::new_ctrl_scroll_up_event(active_pane_position), + client_id, + ) + .unwrap(); + + assert!(effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_scroll_down_on_active_tiled_pane_decreases_pane_size() { + let size = Size { + cols: 120, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left pane".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let active_pane_position = Position::new(5, 70); + let effect = tab + .handle_mouse_event( + &MouseEvent::new_ctrl_scroll_down_event(active_pane_position), + client_id, + ) + .unwrap(); + + assert!(effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_scroll_up_on_inactive_tiled_pane_increases_active_tiled_pane_size() { + let size = Size { + cols: 120, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left pane".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let inactive_pane_position = Position::new(5, 10); + let effect = tab + .handle_mouse_event( + &MouseEvent::new_ctrl_scroll_up_event(inactive_pane_position), + client_id, + ) + .unwrap(); + + assert!(effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_scroll_down_on_inactive_tiled_pane_decreases_active_tiled_pane_size() { + let size = Size { + cols: 120, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + tab.vertical_split(new_pane_id, None, client_id, None, None) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Left pane".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Right pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let inactive_pane_position = Position::new(5, 10); + let effect = tab + .handle_mouse_event( + &MouseEvent::new_ctrl_scroll_down_event(inactive_pane_position), + client_id, + ) + .unwrap(); + + assert!(effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_scroll_up_increases_floating_pane_size() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + new_pane_id, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Tiled pane".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let floating_pane_position = Position::new(5, 50); + let effect = tab + .handle_mouse_event( + &MouseEvent::new_ctrl_scroll_up_event(floating_pane_position), + client_id, + ) + .unwrap(); + + assert!(effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_scroll_down_decreases_floating_pane_size() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + new_pane_id, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Tiled pane".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let floating_pane_position = Position::new(5, 50); + let effect = tab + .handle_mouse_event( + &MouseEvent::new_ctrl_scroll_down_event(floating_pane_position), + client_id, + ) + .unwrap(); + + assert!(effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_scroll_up_increases_pinned_floating_pane_size_when_floating_panes_hidden() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + let coordinates = FloatingPaneCoordinates { + x: Some(PercentOrFixed::Fixed(5)), + y: Some(PercentOrFixed::Fixed(5)), + width: Some(PercentOrFixed::Fixed(30)), + height: Some(PercentOrFixed::Fixed(10)), + pinned: Some(true), + borderless: Some(false), + }; + + tab.new_pane( + new_pane_id, + None, + None, + false, + true, + NewPanePlacement::Floating(Some(coordinates)), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Tiled pane".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let floating_pane_position = Position::new(10, 10); + let effect = tab + .handle_mouse_event( + &MouseEvent::new_ctrl_scroll_up_event(floating_pane_position), + client_id, + ) + .unwrap(); + + assert!(effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} + +#[test] +fn test_ctrl_scroll_down_decreases_pinned_floating_pane_size_when_floating_panes_hidden() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + + let coordinates = FloatingPaneCoordinates { + x: Some(PercentOrFixed::Fixed(5)), + y: Some(PercentOrFixed::Fixed(5)), + width: Some(PercentOrFixed::Fixed(30)), + height: Some(PercentOrFixed::Fixed(10)), + pinned: Some(true), + borderless: Some(false), + }; + + tab.new_pane( + new_pane_id, + None, + None, + false, + true, + NewPanePlacement::Floating(Some(coordinates)), + Some(client_id), + None, + ) + .unwrap(); + + tab.handle_pty_bytes(1, Vec::from("Tiled pane".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) + .unwrap(); + + tab.render(&mut output, None).unwrap(); + let snapshot_before = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + let floating_pane_position = Position::new(6, 10); + let effect = tab + .handle_mouse_event( + &MouseEvent::new_ctrl_scroll_down_event(floating_pane_position), + client_id, + ) + .unwrap(); + + assert!(effect.state_changed); + + let mut output = Output::default(); + tab.render(&mut output, None).unwrap(); + let snapshot_after = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_ne!(snapshot_before, snapshot_after); + eprintln!("{}", snapshot_before); + eprintln!("{}", snapshot_after); + assert_snapshot!(format!("{}", snapshot_after)); +} diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index eea12a55..0a8eb976 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -215,6 +215,7 @@ fn create_new_tab(size: Size, stacked_resize: bool) -> Tab { current_pane_group, currently_marking_pane_group, advanced_mouse_actions, + true, // mouse_hover_effects web_server_ip, web_server_port, ); @@ -298,6 +299,7 @@ fn create_new_tab_with_layout(size: Size, layout: TiledPaneLayout) -> Tab { current_pane_group, currently_marking_pane_group, advanced_mouse_actions, + true, // mouse_hover_effects web_server_ip, web_server_port, ); @@ -387,6 +389,7 @@ fn create_new_tab_with_cell_size( current_pane_group, currently_marking_pane_group, advanced_mouse_actions, + true, // mouse_hover_effects web_server_ip, web_server_port, ); |
