diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-01-14 17:13:41 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-14 17:13:41 +0100 |
| commit | 28cdd5bcf6dc9b1cd940e6e8c5c045da3e65de53 (patch) | |
| tree | 83624ac6388b399c10b50685bf6b442ce29ea146 | |
| parent | docs(changelog): add PRs (diff) | |
| download | sl0ppij-28cdd5bcf6dc9b1cd940e6e8c5c045da3e65de53.tar.gz sl0ppij-28cdd5bcf6dc9b1cd940e6e8c5c045da3e65de53.tar.xz sl0ppij-28cdd5bcf6dc9b1cd940e6e8c5c045da3e65de53.zip | |
fix: center floating panes when width/height but no x/y are provided (#4607)
* fix: center floating panes when width/height but no x/y are provided
* rustfmt
* add pr
9 files changed, 334 insertions, 72 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f820019..92dc3a15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * feat: new `layout-manager` interface and plugin API commands (https://github.com/zellij-org/zellij/pull/4601) * fix: keep serializing sessions resurrected from the welcome screen (https://github.com/zellij-org/zellij/pull/4604) * fix: sanitize session names when deleting them from the CLI (https://github.com/zellij-org/zellij/pull/4583) +* fix: properly center floating panes started with `--floating` when explicit width/height is provided (https://github.com/zellij-org/zellij/pull/4607) ## [0.43.1] - 2025-08-08 * fix: pane rename backspace regression (https://github.com/zellij-org/zellij/pull/4346) diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index a52c78e7..18710c01 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -292,18 +292,14 @@ impl FloatingPanes { let mut position = floating_pane_grid .find_room_for_new_pane() .with_context(err_context)?; - if let Some(x) = &floating_pane_layout.x { - position.x = x.to_position(viewport.cols); - } - if let Some(y) = &floating_pane_layout.y { - position.y = y.to_position(viewport.rows); - } - if let Some(width) = &floating_pane_layout.width { - position.cols = Dimension::fixed(width.to_position(viewport.cols)); - } - if let Some(height) = &floating_pane_layout.height { - position.rows = Dimension::fixed(height.to_position(viewport.rows)); - } + position.apply_floating_pane_position( + floating_pane_layout.x.clone(), + floating_pane_layout.y.clone(), + floating_pane_layout.width.clone(), + floating_pane_layout.height.clone(), + viewport.cols, + viewport.rows, + ); if let Some(is_pinned) = &floating_pane_layout.pinned { position.is_pinned = *is_pinned; } diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane_in_auto_layout-2.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane_in_auto_layout-2.snap index 1394348a..0b88ec67 100644 --- a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane_in_auto_layout-2.snap +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane_in_auto_layout-2.snap @@ -1,6 +1,5 @@ --- source: zellij-server/src/tab/./unit/tab_integration_tests.rs -assertion_line: 8040 expression: snapshot --- 00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -8,19 +7,18 @@ expression: snapshot 02 (C): │ │ 03 (C): │ │ 04 (C): │ │ -05 (C): │ ┌ Pane #2 ─────────────────────────────────── PIN [ ] ┐ ┌ Pane #3 ─────────────────────────────────── PIN [ ] ┐ │ -06 (C): │ │ │ │ │ │ -07 (C): │ │ │ │ │ │ -08 (C): │ │ │ │ │ │ -09 (C): │ │ │ │ │ │ -10 (C): │ │ │ │ │ │ -11 (C): │ │ │ │ │ │ -12 (C): │ │ │ │ │ │ -13 (C): │ │ │ │ │ │ -14 (C): │ └─────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────┘ │ +05 (C): │┌ Pane #2 ────────────────────────────────── PIN [ ] ┐ ┌ Pane #3 ────────────────────────────────── PIN [ ] ┐ │ +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__new_floating_pane_in_auto_layout-3.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane_in_auto_layout-3.snap index 13848a2b..07992d63 100644 --- a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane_in_auto_layout-3.snap +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane_in_auto_layout-3.snap @@ -1,26 +1,24 @@ --- source: zellij-server/src/tab/./unit/tab_integration_tests.rs -assertion_line: 8043 expression: snapshot --- -00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -01 (C): │ ┌ Pane #2 ─────────────────────────────────── PIN [ ] ┐ ┌ Pane #3 ─────────────────────────────────── PIN [ ] ┐ │ -02 (C): │ │ │ │ │ │ -03 (C): │ │ │ │ │ │ -04 (C): │ │ │ │ │ │ -05 (C): │ │ │ │ │ │ -06 (C): │ │ │ │ │ │ -07 (C): │ │ │ │ │ │ -08 (C): │ │ │ │ │ │ -09 (C): │ │ │ │ │ │ -10 (C): │ └─────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────┘ │ -11 (C): │ ┌ Pane #4 ─────────────────────────────────── PIN [ ] ┐ │ -12 (C): │ │ │ │ -13 (C): │ │ │ │ -14 (C): │ │ │ │ -15 (C): │ │ │ │ -16 (C): │ │ │ │ -17 (C): │ │ │ │ -18 (C): │ │ │ │ -19 (C): └─────────────────────────────└─────────────────────────────────────────────────────┘───────────────────────────────────┘ - +00 (C): ┌┌ Pane #2 ────────────────────────────────── PIN [ ] ┐─────┌ Pane #3 ────────────────────────────────── PIN [ ] ┐──────┐ +01 (C): ││ │ │ │ │ +02 (C): ││ │ │ │ │ +03 (C): ││ │ │ │ │ +04 (C): ││ │ │ │ │ +05 (C): ││ │ │ │ │ +06 (C): ││ │ │ │ │ +07 (C): ││ │ │ │ │ +08 (C): ││ │ │ │ │ +09 (C): │└────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────┘ │ +10 (C): │ │ +11 (C): │ ┌ Pane #4 ────────────────────────────────── PIN [ ] ┐ │ +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__new_floating_pane_in_auto_layout.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane_in_auto_layout.snap index 675409f8..2f15c3ca 100644 --- a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane_in_auto_layout.snap +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane_in_auto_layout.snap @@ -1,6 +1,5 @@ --- source: zellij-server/src/tab/./unit/tab_integration_tests.rs -assertion_line: 8040 expression: snapshot --- 00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -13,14 +12,13 @@ expression: snapshot 07 (C): │ │ 08 (C): │ │ 09 (C): │ │ -10 (C): │ ┌ Pane #2 ──────────────────────────────────────── PIN [ ] ┐ -11 (C): │ │ │ -12 (C): │ │ │ -13 (C): │ │ │ -14 (C): │ │ │ -15 (C): │ │ │ -16 (C): │ │ │ -17 (C): │ │ │ -18 (C): │ │ │ -19 (C): └────────────────────────────────────────────────────────────└──────────────────────────────────────────────────────────┘ - +10 (C): │ ┌ Pane #2 ──────────────────────────────────────── PIN [ ] ┐│ +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/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 6eb39eae..d80fb8b7 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -9405,7 +9405,7 @@ fn new_floating_pane_in_auto_layout() { stacked_resize, ); - let mut expected_cursor_coordinates = vec![(62, 11), (62, 6), (31, 12)]; + let mut expected_cursor_coordinates = vec![(61, 11), (61, 6), (34, 12)]; for i in 0..3 { let new_pane_id = i + 2; tab.new_pane( diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index b305a28f..75b427c3 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -1497,6 +1497,229 @@ fn open_new_floating_pane_with_custom_coordinates_exceeding_viewport() { } #[test] +fn floating_pane_auto_centers_horizontally_with_only_width() { + let size = Size { + cols: 120, + rows: 20, + }; + let mut screen = create_new_screen(size, true); + + new_tab(&mut screen, 1, 0); + let active_tab = screen.get_active_tab_mut(1).unwrap(); + active_tab + .new_pane( + PaneId::Terminal(2), + None, + None, + false, + true, + NewPanePlacement::Floating(Some(FloatingPaneCoordinates { + x: None, + y: Some(SplitSize::Fixed(5)), + width: Some(SplitSize::Fixed(60)), + height: Some(SplitSize::Fixed(10)), + pinned: None, + })), + Some(1), + None, + ) + .unwrap(); + let active_pane = active_tab.get_active_pane(1).unwrap(); + assert_eq!(active_pane.x(), 30, "x centered: (120-60)/2 = 30"); + assert_eq!(active_pane.y(), 5, "y explicitly set"); + assert_eq!(active_pane.cols(), 60, "width set"); + assert_eq!(active_pane.rows(), 10, "height set"); +} + +#[test] +fn floating_pane_auto_centers_vertically_with_only_height() { + let size = Size { + cols: 120, + rows: 40, + }; + let mut screen = create_new_screen(size, true); + + new_tab(&mut screen, 1, 0); + let active_tab = screen.get_active_tab_mut(1).unwrap(); + active_tab + .new_pane( + PaneId::Terminal(2), + None, + None, + false, + true, + NewPanePlacement::Floating(Some(FloatingPaneCoordinates { + x: Some(SplitSize::Fixed(10)), + y: None, + width: Some(SplitSize::Fixed(50)), + height: Some(SplitSize::Fixed(20)), + pinned: None, + })), + Some(1), + None, + ) + .unwrap(); + let active_pane = active_tab.get_active_pane(1).unwrap(); + assert_eq!(active_pane.x(), 10, "x explicitly set"); + assert_eq!(active_pane.y(), 10, "y centered: (40-20)/2 = 10"); + assert_eq!(active_pane.cols(), 50, "width set"); + assert_eq!(active_pane.rows(), 20, "height set"); +} + +#[test] +fn floating_pane_auto_centers_both_axes_with_only_size() { + let size = Size { + cols: 120, + rows: 40, + }; + let mut screen = create_new_screen(size, true); + + new_tab(&mut screen, 1, 0); + let active_tab = screen.get_active_tab_mut(1).unwrap(); + active_tab + .new_pane( + PaneId::Terminal(2), + None, + None, + false, + true, + NewPanePlacement::Floating(Some(FloatingPaneCoordinates { + x: None, + y: None, + width: Some(SplitSize::Fixed(80)), + height: Some(SplitSize::Fixed(30)), + pinned: None, + })), + Some(1), + None, + ) + .unwrap(); + let active_pane = active_tab.get_active_pane(1).unwrap(); + assert_eq!(active_pane.x(), 20, "x centered: (120-80)/2 = 20"); + assert_eq!(active_pane.y(), 5, "y centered: (40-30)/2 = 5"); + assert_eq!(active_pane.cols(), 80, "width set"); + assert_eq!(active_pane.rows(), 30, "height set"); +} + +#[test] +fn floating_pane_respects_explicit_coordinates_with_size() { + let size = Size { + cols: 120, + rows: 40, + }; + let mut screen = create_new_screen(size, true); + + new_tab(&mut screen, 1, 0); + let active_tab = screen.get_active_tab_mut(1).unwrap(); + active_tab + .new_pane( + PaneId::Terminal(2), + None, + None, + false, + true, + NewPanePlacement::Floating(Some(FloatingPaneCoordinates { + x: Some(SplitSize::Fixed(15)), + y: Some(SplitSize::Fixed(8)), + width: Some(SplitSize::Fixed(80)), + height: Some(SplitSize::Fixed(30)), + pinned: None, + })), + Some(1), + None, + ) + .unwrap(); + let active_pane = active_tab.get_active_pane(1).unwrap(); + assert_eq!(active_pane.x(), 15, "x explicitly set, not centered"); + assert_eq!(active_pane.y(), 8, "y explicitly set, not centered"); + assert_eq!(active_pane.cols(), 80, "width set"); + assert_eq!(active_pane.rows(), 30, "height set"); +} + +#[test] +fn floating_pane_centers_with_percentage_width() { + let size = Size { + cols: 120, + rows: 40, + }; + let mut screen = create_new_screen(size, true); + + new_tab(&mut screen, 1, 0); + let active_tab = screen.get_active_tab_mut(1).unwrap(); + active_tab + .new_pane( + PaneId::Terminal(2), + None, + None, + false, + true, + NewPanePlacement::Floating(Some(FloatingPaneCoordinates { + x: None, + y: Some(SplitSize::Fixed(5)), + width: Some(SplitSize::Percent(50)), + height: Some(SplitSize::Fixed(20)), + pinned: None, + })), + Some(1), + None, + ) + .unwrap(); + let active_pane = active_tab.get_active_pane(1).unwrap(); + let expected_width = ((50.0_f64 / 100.0) * 120.0).floor() as usize; + let expected_x = (120 - expected_width) / 2; + assert_eq!(active_pane.cols(), expected_width, "width is 50% of 120"); + assert_eq!( + active_pane.x(), + expected_x, + "x centered based on calculated width" + ); + assert_eq!(active_pane.y(), 5, "y explicitly set"); +} + +#[test] +fn floating_pane_centers_large_pane_safely() { + let size = Size { + cols: 100, + rows: 30, + }; + let mut screen = create_new_screen(size, true); + + new_tab(&mut screen, 1, 0); + let active_tab = screen.get_active_tab_mut(1).unwrap(); + active_tab + .new_pane( + PaneId::Terminal(2), + None, + None, + false, + true, + NewPanePlacement::Floating(Some(FloatingPaneCoordinates { + x: None, + y: None, + width: Some(SplitSize::Fixed(150)), + height: Some(SplitSize::Fixed(50)), + pinned: None, + })), + Some(1), + None, + ) + .unwrap(); + let active_pane = active_tab.get_active_pane(1).unwrap(); + assert_eq!( + active_pane.x(), + 0, + "x is 0 when pane larger than viewport (saturating_sub)" + ); + assert_eq!( + active_pane.y(), + 0, + "y is 0 when pane larger than viewport (saturating_sub)" + ); + assert!(active_pane.cols() <= 100, "width clamped to viewport"); + assert!(active_pane.rows() <= 30, "height clamped to viewport"); +} + +#[test] pub fn mouse_hover_effect() { let size = Size { cols: 130, diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index 8ba81159..ae7d85a1 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -73,6 +73,24 @@ pub enum SplitSize { Fixed(usize), // An absolute number of columns or rows } +impl From<PercentOrFixed> for SplitSize { + fn from(pof: PercentOrFixed) -> Self { + match pof { + PercentOrFixed::Percent(p) => SplitSize::Percent(p), + PercentOrFixed::Fixed(f) => SplitSize::Fixed(f), + } + } +} + +impl From<SplitSize> for PercentOrFixed { + fn from(ss: SplitSize) -> Self { + match ss { + SplitSize::Percent(p) => PercentOrFixed::Percent(p), + SplitSize::Fixed(f) => PercentOrFixed::Fixed(f), + } + } +} + impl SplitSize { pub fn to_fixed(&self, full_size: usize) -> usize { match self { @@ -727,6 +745,14 @@ impl PercentOrFixed { }, } } + pub fn to_fixed(&self, full_size: usize) -> usize { + match self { + PercentOrFixed::Percent(percent) => { + ((*percent as f64 / 100.0) * full_size as f64).floor() as usize + }, + PercentOrFixed::Fixed(fixed) => *fixed, + } + } } impl PercentOrFixed { diff --git a/zellij-utils/src/pane_size.rs b/zellij-utils/src/pane_size.rs index 50c4229a..453e57c1 100644 --- a/zellij-utils/src/pane_size.rs +++ b/zellij-utils/src/pane_size.rs @@ -5,7 +5,7 @@ use std::{ }; use crate::data::FloatingPaneCoordinates; -use crate::input::layout::{SplitDirection, SplitSize}; +use crate::input::layout::{PercentOrFixed, SplitDirection, SplitSize}; use crate::position::Position; /// Contains the position and size of a [`Pane`], or more generally of any terminal, measured @@ -250,23 +250,45 @@ impl PaneGeom { SplitDirection::Horizontal => self.rows.is_percent(), } } + pub fn apply_floating_pane_position( + &mut self, + x: Option<PercentOrFixed>, + y: Option<PercentOrFixed>, + width: Option<PercentOrFixed>, + height: Option<PercentOrFixed>, + viewport_cols: usize, + viewport_rows: usize, + ) { + if let Some(width) = &width { + self.cols = Dimension::fixed(width.to_fixed(viewport_cols)); + } + if let Some(height) = &height { + self.rows = Dimension::fixed(height.to_fixed(viewport_rows)); + } + self.x = match &x { + Some(x) => x.to_fixed(viewport_cols), + None if width.is_some() => viewport_cols.saturating_sub(self.cols.as_usize()) / 2, + None => self.x, + }; + self.y = match &y { + Some(y) => y.to_fixed(viewport_rows), + None if height.is_some() => viewport_rows.saturating_sub(self.rows.as_usize()) / 2, + None => self.y, + }; + } pub fn adjust_coordinates( &mut self, floating_pane_coordinates: FloatingPaneCoordinates, viewport: Viewport, ) { - if let Some(x) = floating_pane_coordinates.x { - self.x = x.to_fixed(viewport.cols); - } - if let Some(y) = floating_pane_coordinates.y { - self.y = y.to_fixed(viewport.rows); - } - if let Some(height) = floating_pane_coordinates.height { - self.rows = Dimension::from_split_size(height, viewport.rows); - } - if let Some(width) = floating_pane_coordinates.width { - self.cols = Dimension::from_split_size(width, viewport.cols); - } + self.apply_floating_pane_position( + floating_pane_coordinates.x.map(Into::into), + floating_pane_coordinates.y.map(Into::into), + floating_pane_coordinates.width.map(Into::into), + floating_pane_coordinates.height.map(Into::into), + viewport.cols, + viewport.rows, + ); if self.x < viewport.x { self.x = viewport.x; } else if self.x > viewport.x + viewport.cols { |
