aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-server/src/tab
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-02-04 11:59:21 +0100
committerGitHub <noreply@github.com>2026-02-04 11:59:21 +0100
commit690a1e4506854c1e7637cf4c27f75e39b923972d (patch)
treeecedd56de5971abe1db82d83402815e52e9df3b2 /zellij-server/src/tab
parentfix(layout): Correctly handle relative layout paths from CLI (#4664) (diff)
downloadsl0ppij-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')
-rw-r--r--zellij-server/src/tab/mod.rs737
-rw-r--r--zellij-server/src/tab/mouse_handler.rs1465
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_click_on_floating_pin_button_toggles_pin.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_floating_pane_from_corner.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_floating_pane_from_edge.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_pinned_floating_pane_when_floating_panes_not_shown.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_tiled_pane_horizontally.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_tiled_pane_vertically.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_drag_resizes_unfocused_tiled_pane_vertically.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_decreases_floating_pane_size.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_decreases_pinned_floating_pane_size_when_floating_panes_hidden.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_on_active_tiled_pane_decreases_pane_size.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_down_on_inactive_tiled_pane_decreases_active_tiled_pane_size.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_increases_floating_pane_size.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_increases_pinned_floating_pane_size_when_floating_panes_hidden.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_on_active_tiled_pane_increases_pane_size.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__ctrl_scroll_up_on_inactive_tiled_pane_increases_active_tiled_pane_size.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__drag_pane_with_mouse.snap22
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_active_pane_unsets_hover.snap5
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_inactive_pane_sets_hover_state.snap5
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_click_on_floating_frame_not_on_pin_moves_pane.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_click_on_pinned_floating_pane.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__left_drag_moves_floating_pane.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_tiled_then_move_floating_sequence.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_on_inactive_pane_scrolls_that_pane.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_wheel_down_scrolls_pane.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__scroll_wheel_up_scrolls_pane.snap24
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs2249
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs3
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,
);