Independent tab sizes (#5133)

* feat: allow independent tab resizing

* rustfmt

* add pr
This commit is contained in:
Aram Drevekenin
2026-05-05 16:07:04 +02:00
committed by GitHub
parent f2e02cf1af
commit 5ef8176c43
7 changed files with 505 additions and 114 deletions
+1
View File
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## [Unreleased]
* feat: allow tabs to have different sizes if clients aren't focused on the same one (https://github.com/zellij-org/zellij/pull/5133)
## [0.44.2] - 2026-05-05
* fix: idle CPU + disk i/o, CommandChanged plugin Event, GetSessionList plugin command (https://github.com/zellij-org/zellij/pull/5063)
+1 -91
View File
@@ -642,34 +642,6 @@ impl SessionState {
pub fn set_client_data(&mut self, client_id: ClientId, size: Size, is_web_client: bool) {
self.clients.insert(client_id, Some((size, is_web_client)));
}
pub fn min_client_terminal_size(&self) -> Option<Size> {
// None if there are no client sizes
let mut rows: Vec<usize> = self
.clients
.values()
.filter_map(|size_and_is_web_client| {
size_and_is_web_client.map(|(size, _is_web_client)| size.rows)
})
.collect();
rows.sort_unstable();
let mut cols: Vec<usize> = self
.clients
.values()
.filter_map(|size_and_is_web_client| {
size_and_is_web_client.map(|(size, _is_web_client)| size.cols)
})
.collect();
cols.sort_unstable();
let min_rows = rows.first();
let min_cols = cols.first();
match (min_rows, min_cols) {
(Some(min_rows), Some(min_cols)) => Some(Size {
rows: *min_rows,
cols: *min_cols,
}),
_ => None,
}
}
pub fn client_ids(&self) -> Vec<ClientId> {
self.clients.keys().copied().collect()
}
@@ -1155,20 +1127,12 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
client_attributes.size,
is_web_client,
);
let min_size = session_state
.read()
.unwrap()
.min_client_terminal_size()
.unwrap();
session_data
.senders
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
.unwrap();
session_data
.senders
.send_to_screen(ScreenInstruction::AddClient(
client_id,
is_web_client,
client_attributes.size,
tab_position_to_focus,
pane_id_to_focus,
))
@@ -1319,17 +1283,6 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
// Handle regular client removal
remove_client!(client_id, os_input, session_state, session_data);
drop(completion_tx); // prevent deadlock with route thread
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size()
{
session_data
.write()
.unwrap()
.as_ref()
.unwrap()
.senders
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
.unwrap();
}
session_data
.write()
.unwrap()
@@ -1393,17 +1346,6 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
} else {
// Handle regular client removal
remove_client!(client_id, os_input, session_state, session_data);
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size()
{
session_data
.write()
.unwrap()
.as_ref()
.unwrap()
.senders
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
.unwrap();
}
session_data
.write()
.unwrap()
@@ -1430,16 +1372,6 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
},
);
remove_client!(client_id, os_input, session_state, session_data);
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() {
session_data
.write()
.unwrap()
.as_ref()
.unwrap()
.senders
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
.unwrap();
}
},
ServerInstruction::KillSession => {
let client_ids = session_state.read().unwrap().client_ids();
@@ -1488,17 +1420,6 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
// by us having to wait for session_data to send cleanup
// signals to the various threads
for client_id in client_ids {
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size()
{
session_data
.write()
.unwrap()
.as_ref()
.unwrap()
.senders
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
.unwrap();
}
session_data
.write()
.unwrap()
@@ -1643,17 +1564,6 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
remove_client!(client_id, os_input, session_state, session_data);
drop(completion_tx); // do not deadlock with route thread
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size()
{
session_data
.write()
.unwrap()
.as_ref()
.unwrap()
.senders
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
.unwrap();
}
session_data
.write()
.unwrap()
+10 -20
View File
@@ -2427,26 +2427,16 @@ pub(crate) fn route_thread_main(
.to_anyhow()
.with_context(err_context)?
.set_client_size(client_id, new_size);
// min_client_terminal_size() skips clients whose
// entry is still None — i.e. new_client() was
// called but set_client_data() hasn't been yet.
// set_client_size() above is a no-op in that
// case (it doesn't upgrade None to Some). This
// can happen if a resize arrives before the
// initial connection setup completes; the server
// will query the terminal size once it does.
if let Some(min_size) = session_state
.read()
.to_anyhow()
.with_context(err_context)?
.min_client_terminal_size()
{
let _ = senders.as_ref().map(|s| {
s.send_to_screen(ScreenInstruction::TerminalResize(
min_size,
))
});
}
// Per-tab sizing: Screen's RecomputeTabSize
// handler is a no-op for clients without an
// active tab yet (i.e. resizes arriving
// before AddClient is processed), so no
// session-level gating is needed.
let _ = senders.as_ref().map(|s| {
s.send_to_screen(ScreenInstruction::RecomputeTabSize(
client_id, new_size,
))
});
}
},
ClientToServerMsg::TerminalPixelDimensions { pixel_dimensions } => {
+95 -3
View File
@@ -497,6 +497,9 @@ pub enum ScreenInstruction {
completion_tx: Option<NotificationEnd>,
},
TerminalResize(Size),
/// Update a regular client's known viewport size and recompute the size of
/// that client's active tab. `(client_id, new_size)`.
RecomputeTabSize(ClientId, Size),
TerminalPixelDimensions(PixelDimensions),
TerminalBackgroundColor(String),
TerminalForegroundColor(String),
@@ -551,6 +554,7 @@ pub enum ScreenInstruction {
AddClient(
ClientId,
bool, // is_web_client
Size, // client viewport size — used for per-tab sizing
Option<usize>, // tab position to focus
Option<(u32, bool)>, // (pane_id, is_plugin) => pane_id to focus
),
@@ -975,6 +979,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::RenameTabWithId(..) => ScreenContext::RenameTabWithId,
ScreenInstruction::BreakPanesToTabWithId { .. } => ScreenContext::BreakPanesToTabWithId,
ScreenInstruction::TerminalResize(..) => ScreenContext::TerminalResize,
ScreenInstruction::RecomputeTabSize(..) => ScreenContext::RecomputeTabSize,
ScreenInstruction::TerminalPixelDimensions(..) => {
ScreenContext::TerminalPixelDimensions
},
@@ -1368,6 +1373,8 @@ pub(crate) struct Screen {
connected_clients: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client
/// The indices of this [`Screen`]'s active [`Tab`]s.
active_tab_ids: BTreeMap<ClientId, usize>,
/// Per-regular-client viewport sizes, used to compute per-tab sizing.
client_sizes: HashMap<ClientId, Size>,
global_last_active_tab_id: usize,
tab_history: BTreeMap<ClientId, Vec<usize>>,
pane_history: BTreeMap<ClientId, Vec<PaneId>>,
@@ -1554,6 +1561,7 @@ impl Screen {
style: client_attributes.style,
connected_clients: Rc::new(RefCell::new(HashMap::new())),
active_tab_ids: BTreeMap::new(),
client_sizes: HashMap::new(),
global_last_active_tab_id: 0,
tabs: BTreeMap::new(),
terminal_emulator_colors: Rc::new(RefCell::new(Palette::default())),
@@ -1859,6 +1867,14 @@ impl Screen {
.send_to_background_jobs(BackgroundJob::StopFlashTabBell(tab_id));
}
// Both the source and destination tabs may have changed
// their viewer sets; per-tab sizing is independent so each
// is recomputed against its current viewers.
self.recompute_tab_size(current_tab_index)
.with_context(err_context)?;
self.recompute_tab_size(new_tab_index)
.with_context(err_context)?;
self.log_and_report_session_state()
.with_context(err_context)?;
return self.render(None).with_context(err_context);
@@ -2064,6 +2080,50 @@ impl Screen {
}
}
/// Record the viewport size most recently reported by `client_id`. Used as
/// input to per-tab size computation; does not by itself trigger a resize.
pub fn set_client_size(&mut self, client_id: ClientId, size: Size) {
self.client_sizes.insert(client_id, size);
}
/// Recompute the size of `tab_id` from the viewports of every client whose
/// `active_tab_ids` entry equals `tab_id`. `rows` and `cols` are sorted
/// independently — each axis takes the minimum across viewers. If the tab
/// has no viewers the size is left untouched (the tab retains its most
/// recent viewer-derived dimensions). When the computed size differs from
/// the tab's current size, the tab is resized (via `resize_whole_tab`)
/// and a force-render is scheduled.
pub fn recompute_tab_size(&mut self, tab_id: usize) -> Result<()> {
let err_context = || format!("failed to recompute size for tab {tab_id}");
let mut rows: Vec<usize> = Vec::new();
let mut cols: Vec<usize> = Vec::new();
for (client_id, active_tab) in self.active_tab_ids.iter() {
if *active_tab == tab_id {
if let Some(size) = self.client_sizes.get(client_id) {
rows.push(size.rows);
cols.push(size.cols);
}
}
}
if rows.is_empty() || cols.is_empty() {
return Ok(());
}
rows.sort_unstable();
cols.sort_unstable();
let new_size = Size {
rows: rows[0],
cols: cols[0],
};
if let Some(tab) = self.tabs.get_mut(&tab_id) {
if tab.size != new_size {
tab.resize_whole_tab(new_size).with_context(err_context)?;
tab.set_force_render();
}
}
Ok(())
}
pub fn update_pixel_dimensions(&mut self, pixel_dimensions: PixelDimensions) {
self.pixel_dimensions.merge(pixel_dimensions);
if let Some(character_cell_size) = self.pixel_dimensions.character_cell_size {
@@ -3021,6 +3081,11 @@ impl Screen {
.with_context(err_context)?;
}
// The new tab was just resized to `self.size` above as a default; if
// any clients have been moved onto it, recompute to fit their actual
// viewports.
self.recompute_tab_size(tab_id).with_context(err_context)?;
self.log_and_report_session_state()
.and_then(|_| self.render(None))
.with_context(err_context)
@@ -3064,7 +3129,12 @@ impl Screen {
.get_mut(&tab_index)
.with_context(|| err_context(tab_index))?
.add_client(client_id, None)
.with_context(|| err_context(tab_index))
.with_context(|| err_context(tab_index))?;
// Resize the newly-active tab to the min of all viewers (this client
// may shrink it, or may be the first viewer of an empty tab).
self.recompute_tab_size(tab_index)
.with_context(|| err_context(tab_index))?;
Ok(())
}
pub fn remove_client(&mut self, client_id: ClientId) -> Result<()> {
@@ -3093,15 +3163,23 @@ impl Screen {
tab.visible(false).with_context(err_context)?;
}
}
if self.active_tab_ids.contains_key(&client_id) {
self.global_last_active_tab_id = *self.active_tab_ids.get(&client_id).unwrap();
let previously_active_tab_id = self.active_tab_ids.get(&client_id).copied();
if let Some(prev_tab_id) = previously_active_tab_id {
self.global_last_active_tab_id = prev_tab_id;
self.active_tab_ids.remove(&client_id);
}
if self.tab_history.contains_key(&client_id) {
self.tab_history.remove(&client_id);
}
self.connected_clients.borrow_mut().remove(&client_id);
self.client_sizes.remove(&client_id);
self.pane_render_subscribers.remove(&client_id);
// The vacated tab may have lost its smallest viewer; recompute so it
// can grow back to fit the remaining clients (no-op if none remain).
if let Some(prev_tab_id) = previously_active_tab_id {
self.recompute_tab_size(prev_tab_id)
.with_context(err_context)?;
}
self.log_and_report_session_state()
.with_context(err_context)
}
@@ -7222,6 +7300,15 @@ pub(crate) fn screen_thread_main(
screen.log_and_report_session_state()?; // update tabs so that the ui indication will be send to the plugins
screen.render(None)?;
},
ScreenInstruction::RecomputeTabSize(client_id, new_size) => {
screen.set_client_size(client_id, new_size);
let active_tab_id = screen.active_tab_ids.get(&client_id).copied();
if let Some(tab_id) = active_tab_id {
screen.recompute_tab_size(tab_id)?;
screen.log_and_report_session_state()?;
screen.render(None)?;
}
},
ScreenInstruction::TerminalPixelDimensions(pixel_dimensions) => {
screen.update_pixel_dimensions(pixel_dimensions);
},
@@ -7331,9 +7418,14 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::AddClient(
client_id,
is_web_client,
client_size,
tab_position_to_focus,
pane_id_to_focus,
) => {
// Record the client's viewport BEFORE add_client so that
// add_client's internal recompute sees this client's size and
// sizes the destination tab against all of its viewers.
screen.set_client_size(client_id, client_size);
screen.add_client(client_id, is_web_client)?;
let pane_id = pane_id_to_focus.map(|(pane_id, is_plugin)| {
if is_plugin {
+8
View File
@@ -158,6 +158,11 @@ pub(crate) struct Tab {
pub position: usize,
pub name: String,
pub prev_name: String,
/// The tab's current viewport size, computed as `min(rows)` and `min(cols)`
/// independently across the clients whose `active_tab_id` equals this tab.
/// When the tab has no viewers it retains its most recent value (the
/// recompute path simply skips empty tabs).
pub size: Size,
tiled_panes: TiledPanes,
floating_panes: FloatingPanes,
suppressed_panes: SuppressedPanes,
@@ -769,6 +774,7 @@ impl Tab {
if let Some(client_id) = client_id {
connected_clients.insert(client_id);
}
let initial_size = display_area;
let viewport: Viewport = display_area.into();
let viewport = Rc::new(RefCell::new(viewport));
let display_area = Rc::new(RefCell::new(display_area));
@@ -818,6 +824,7 @@ impl Tab {
suppressed_panes: HashMap::new(),
name: name.clone(),
prev_name: name,
size: initial_size,
max_panes,
viewport,
display_area,
@@ -3504,6 +3511,7 @@ impl Tab {
}
pub fn resize_whole_tab(&mut self, new_screen_size: Size) -> Result<()> {
let err_context = || format!("failed to resize whole tab (id {})", self.id);
self.size = new_screen_size;
// If a tiled pane is fullscreen, exit fullscreen first so that *all*
// tiled panes (including the currently hidden ones) participate in the
// resize/relayout. We re-enter fullscreen on the same pane after the
+389
View File
@@ -9455,3 +9455,392 @@ fn empty_reply_with_paused_pane_drains_buffer_without_phantom_write() {
"queue fully consumed by post-resume processing"
);
}
// --- per-tab viewport sizing (Stage 1 of mobile_view_plan.md) -------------
// All five tests below exercise `Screen::recompute_tab_size`, which the
// production code invokes from `ScreenInstruction::AddClient`,
// `ScreenInstruction::RecomputeTabSize` (runtime resize), `remove_client`,
// and `switch_active_tab`. Tests drive the helper directly so the assertions
// don't depend on instruction-bus plumbing.
/// Variant of `create_new_screen` with `session_is_mirrored = false`. The
/// payoff of per-tab sizing only shows up when clients can sit on different
/// tabs; mirrored mode forces every client to share the active tab.
fn create_non_mirrored_screen(size: Size) -> Screen {
let mut bus: Bus<ScreenInstruction> = Bus::empty();
let fake_os_input = FakeInputOutput::default();
bus.os_input = Some(Box::new(fake_os_input));
let client_attributes = ClientAttributes {
size,
..Default::default()
};
let mut mode_info = ModeInfo::default();
mode_info.session_name = Some("zellij-test".into());
Screen::new(
bus,
&client_attributes,
None, // max_panes
mode_info,
false, // draw_pane_frames
true, // auto_layout
false, // session_is_mirrored
CopyOptions::default(),
false, // debug
Box::new(Layout::default()),
None, // default_layout_name
PathBuf::from("my_default_shell"),
true, // session_serialization
false, // serialize_pane_viewport
None, // scrollback_lines_to_serialize
true, // styled_underlines
true, // osc8_hyperlinks
true, // arrow_fonts
None, // layout_dir
false, // explicitly_disable_kitty_keyboard_protocol
true, // stacked_resize
None,
false,
WebSharing::Off,
true, // advanced_mouse_actions
true, // mouse_hover_effects
true, // visual_bell
false, // focus_follows_mouse
false, // mouse_click_through
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
8080,
)
}
#[test]
fn recompute_tab_size_uses_lone_viewer_size() {
let initial_size = Size {
cols: 200,
rows: 60,
};
let mut screen = create_new_screen(initial_size, true, true);
new_tab(&mut screen, 1, 0);
let client_id = 1;
let client_size = Size { cols: 80, rows: 24 };
screen.set_client_size(client_id, client_size);
screen.recompute_tab_size(0).expect("TEST");
assert_eq!(
screen.tabs.get(&0).unwrap().size,
client_size,
"Tab adopts its lone viewer's size"
);
}
#[test]
fn recompute_tab_size_takes_independent_min_across_axes() {
let initial_size = Size {
cols: 200,
rows: 60,
};
let mut screen = create_new_screen(initial_size, true, true);
new_tab(&mut screen, 1, 0);
// Drop the second client onto the same tab as client 1.
screen.add_client(2, false).expect("TEST");
// Wide-but-short and narrow-but-tall: rows and cols must min independently,
// not be paired by client.
screen.set_client_size(
1,
Size {
cols: 200,
rows: 24,
},
);
screen.set_client_size(2, Size { cols: 80, rows: 60 });
screen.recompute_tab_size(0).expect("TEST");
assert_eq!(
screen.tabs.get(&0).unwrap().size,
Size { cols: 80, rows: 24 },
"Tab size = (min cols across viewers, min rows across viewers)"
);
}
#[test]
fn recompute_tab_size_isolates_tabs_with_different_viewers() {
let initial_size = Size {
cols: 200,
rows: 60,
};
// Non-mirrored: per-tab sizing's whole point is letting clients sit on
// different tabs without one dragging the other's grid.
let mut screen = create_non_mirrored_screen(initial_size);
new_tab(&mut screen, 1, 0);
new_tab(&mut screen, 2, 1);
// Client 1 is on tab 1 (the most recent). Pull client 2 onto tab 0 so
// tabs have one viewer each.
screen.add_client(2, false).expect("TEST");
screen.set_client_size(1, Size { cols: 80, rows: 24 });
screen.set_client_size(
2,
Size {
cols: 160,
rows: 50,
},
);
screen.switch_active_tab(0, None, true, 2).expect("TEST");
assert_eq!(
screen.tabs.get(&1).unwrap().size,
Size { cols: 80, rows: 24 },
"Tab 1 sized to its lone viewer (client 1)"
);
assert_eq!(
screen.tabs.get(&0).unwrap().size,
Size {
cols: 160,
rows: 50,
},
"Tab 0 sized to its lone viewer (client 2), unaffected by client 1's smaller viewport"
);
}
#[test]
fn switching_tabs_recomputes_source_and_destination() {
let initial_size = Size {
cols: 200,
rows: 60,
};
let mut screen = create_non_mirrored_screen(initial_size);
new_tab(&mut screen, 1, 0);
new_tab(&mut screen, 2, 1);
screen.add_client(2, false).expect("TEST");
// Both clients start on tab 1 (most recently created). Give them
// different sizes; the smaller wins on tab 1.
screen.set_client_size(1, Size { cols: 80, rows: 24 });
screen.set_client_size(
2,
Size {
cols: 160,
rows: 50,
},
);
screen.recompute_tab_size(1).expect("TEST");
assert_eq!(
screen.tabs.get(&1).unwrap().size,
Size { cols: 80, rows: 24 },
"Both clients on tab 1 → it sizes to the smaller"
);
// Move the smaller client to tab 0. Tab 1 must grow back to fit its
// remaining viewer; tab 0 must shrink to fit the arriving one.
screen.switch_active_tab(0, None, true, 1).expect("TEST");
assert_eq!(
screen.tabs.get(&1).unwrap().size,
Size {
cols: 160,
rows: 50,
},
"Source tab grows back to fit its remaining viewer"
);
assert_eq!(
screen.tabs.get(&0).unwrap().size,
Size { cols: 80, rows: 24 },
"Destination tab shrinks to fit the arriving smaller viewer"
);
}
#[test]
fn break_pane_to_new_tab_recomputes_source_and_destination() {
// `break_pane_to_new_tab` extracts the active pane and switches the
// requesting client to an adjacent tab via `switch_tab_next`/`prev`. That
// routes through `switch_active_tab`, so per-tab sizing must follow the
// moving client.
let initial_size = Size {
cols: 200,
rows: 60,
};
let mut screen = create_non_mirrored_screen(initial_size);
new_tab(&mut screen, 1, 0);
new_tab(&mut screen, 2, 1);
screen.add_client(2, false).expect("TEST");
// Park each client on its own tab with its own viewport.
screen.set_client_size(1, Size { cols: 80, rows: 24 });
screen.set_client_size(
2,
Size {
cols: 160,
rows: 50,
},
);
// After new_tab(2), client 1 is on tab id=1 (the new one). Pull it back
// onto tab id=0 so the two clients are split across the two tabs.
screen.switch_active_tab(0, None, true, 1).expect("TEST");
assert_eq!(
screen.tabs.get(&0).unwrap().size,
Size { cols: 80, rows: 24 },
"Pre-condition: tab 0 sized to client 1"
);
assert_eq!(
screen.tabs.get(&1).unwrap().size,
Size {
cols: 160,
rows: 50,
},
"Pre-condition: tab 1 sized to client 2"
);
// Give tab 0 a second pane so break_pane_to_new_tab has something to
// extract — but the per-tab sizing assertions are about clients, not
// panes; the second pane is just to satisfy the function's preconditions.
{
let active_tab = screen.get_active_tab_mut(1).unwrap();
active_tab
.new_pane(
PaneId::Terminal(99),
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
None,
)
.unwrap();
}
// Move client 1's active pane rightward into tab 1. The function
// synchronously calls switch_tab_next → switch_active_tab(client 1, tab 1).
screen
.break_pane_to_new_tab(Direction::Right, 1)
.expect("TEST");
// Tab 1 now has both clients → recomputes to the smaller viewport.
// Tab 0 has no viewers → its size is left untouched.
assert_eq!(
screen.tabs.get(&1).unwrap().size,
Size { cols: 80, rows: 24 },
"Destination tab shrinks to fit the arriving smaller viewer"
);
assert_eq!(
screen.tabs.get(&0).unwrap().size,
Size { cols: 80, rows: 24 },
"Source tab is empty — recompute is a no-op, last viewer-derived size is preserved"
);
}
#[test]
fn moving_panes_between_tabs_with_focus_change_recomputes_both() {
// `break_multiple_panes_to_tab_with_index` with `should_change_focus = true`
// calls `go_to_tab` on the requesting client. That hits `switch_active_tab`,
// so the source tab grows back and the destination shrinks to fit.
let initial_size = Size {
cols: 200,
rows: 60,
};
let mut screen = create_non_mirrored_screen(initial_size);
new_tab(&mut screen, 1, 0);
new_tab(&mut screen, 2, 1);
screen.add_client(2, false).expect("TEST");
// Both clients on tab 1 (the most recent), small client wins.
screen.set_client_size(1, Size { cols: 80, rows: 24 });
screen.set_client_size(
2,
Size {
cols: 160,
rows: 50,
},
);
// Park client 2 alone on tab 0 so the two tabs are differently sized.
screen.switch_active_tab(0, None, true, 2).expect("TEST");
assert_eq!(
screen.tabs.get(&0).unwrap().size,
Size {
cols: 160,
rows: 50,
},
"Pre-condition: tab 0 sized to client 2"
);
assert_eq!(
screen.tabs.get(&1).unwrap().size,
Size { cols: 80, rows: 24 },
"Pre-condition: tab 1 sized to client 1"
);
// Add a movable pane on tab 1 (where client 1 lives).
let pane_to_move = PaneId::Terminal(42);
{
let active_tab = screen.get_active_tab_mut(1).unwrap();
active_tab
.new_pane(
pane_to_move,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
None,
)
.unwrap();
}
// Move the pane onto tab at position 0 AND switch client 1's focus there.
// Client 1 leaves tab 1 → tab 1 grows back to client 2's size... wait,
// client 2 already left tab 1 above. Tab 1 will be empty, so it keeps its
// last size. Tab 0 gains client 1 alongside client 2 → recomputes to the
// smaller of the two viewports.
screen
.break_multiple_panes_to_tab_with_index(vec![pane_to_move], 0, true, 1)
.expect("TEST");
assert_eq!(
screen.tabs.get(&0).unwrap().size,
Size { cols: 80, rows: 24 },
"Destination tab shrinks to min(viewers) once the smaller client arrives"
);
assert_eq!(
screen.tabs.get(&1).unwrap().size,
Size { cols: 80, rows: 24 },
"Source tab is empty — last viewer-derived size is preserved"
);
}
#[test]
fn detaching_client_grows_vacated_tab_back() {
let initial_size = Size {
cols: 200,
rows: 60,
};
let mut screen = create_new_screen(initial_size, true, true);
new_tab(&mut screen, 1, 0);
screen.add_client(2, false).expect("TEST");
// Tab 0 has both clients; the smaller one defines its size.
screen.set_client_size(
1,
Size {
cols: 160,
rows: 50,
},
);
screen.set_client_size(2, Size { cols: 80, rows: 24 });
screen.recompute_tab_size(0).expect("TEST");
assert_eq!(
screen.tabs.get(&0).unwrap().size,
Size { cols: 80, rows: 24 },
"Smaller viewer wins while both clients are on the tab"
);
// The smaller client leaves; the tab must grow to fit the remaining one.
screen.remove_client(2).expect("TEST");
assert_eq!(
screen.tabs.get(&0).unwrap().size,
Size {
cols: 160,
rows: 50,
},
"Tab grows back to fit the remaining viewer after the smaller one detaches"
);
}
+1
View File
@@ -306,6 +306,7 @@ pub enum ScreenContext {
RenameTabWithId,
BreakPanesToTabWithId,
TerminalResize,
RecomputeTabSize,
TerminalPixelDimensions,
TerminalBackgroundColor,
TerminalForegroundColor,