feat(plugin-api): let a plugin give its layout space back while empty (#5590)

* feat(plugin-api): let a plugin give its layout space back while empty

A plugin mounted in a layout as a bar keeps the row the layout reserved
for it even when it has nothing to draw, so an empty bar shows as a blank
line. Nothing in the plugin API could give that row up: HideSelf
suppresses the pane and brings it back through add_tiled_pane, which
re-inserts it by splitting rather than at its layout position, and
CloseSelf does not come back at all.

SetSelfCollapsed takes the pane out of the constraint solve without
taking it out of the layout. The pane keeps its size constraint while it
is out, so a Fixed(1) bar is still Fixed(1) when it rejoins and expanding
restores exactly what the layout asked for, while constrain_spans makes
its neighbors grow over the space in the meantime.

Collapsed panes are tracked separately from panes_to_hide, which
fullscreen overwrites wholesale, so a collapse survives entering and
leaving fullscreen. They are also left out of the viewport offset, since
a pane holding no space must not shrink the viewport, and their state is
dropped when the pane closes so a reused pane id does not arrive already
invisible.

* test(tab): cover collapsing a pane out of the layout and back

Pins down what the collapse is for: the neighbors take the space, the
collapsed pane keeps its size constraint so expanding restores the layout
exactly rather than approximately, and the collapse outlives a fullscreen,
which rebuilds the hidden set from scratch.

Also covers the two ways it should do nothing: a second collapse of an
already collapsed pane, and a pane id that is not in the tab. Closing a
collapsed pane drops the state, so a reused id does not come back
invisible.

* fix(screen): keep one deferred collapse per pane instead of a queue

A plugin sends this whenever its content comes and goes, so a plugin
with no pane in any tab would otherwise leave an entry behind on every
change, all of them replayed the next time a tab is created. Only the
most recent word on a pane means anything, so drop the earlier one.

* fix(tab): keep a collapsed pane's geometry current across a relayout

Being filtered out of the constraint solve is what stops a collapsed pane
from holding space, but it also means nothing writes geometry to it. That
was fine while the tab kept its shape and wrong the moment it did not: the
pane was left describing a display area that no longer existed.

Worse than stale, because the solver reads the layout tree back off where
the panes currently sit. A one-row bar collapsed in a 121x20 tab that then
became 118x23 stayed at y 19 in a tab whose last row was 22, so the next
solve was handed a tree that did not match the screen and gave up. The bar
came back three rows above the bottom, at a width wider than the tab, and
its neighbor never gave the row back.

Let collapsed panes take part in the resize and the relayout, then collapse
them again over the result, which is what fullscreen already does a few
lines above for the same reason.

* docs(changelog): note the plugin collapse command

Links the tracking issue rather than a pull request, since no pull
request is open yet. Wants updating to the pull request URL once there
is one, to match every entry in the released sections.

* fix(tab): give a collapsed pane's row back to the viewport after a relayout

A relayout solves with the collapsed panes taking part, and the layout
offsets the viewport while they are still there. Collapsing them again
afterwards handed their rows back to the panes around them, but not to the
viewport, so a collapsed bar went on holding its row out of it until the
next resize. Fullscreen reads the viewport, so it would not cover that row
either.

Offset the viewport again once the collapsed panes are gone, the same as
resize_whole_tab already does.

* refactor(tiled-panes): keep the hidden pane bookkeeping in PanesToHide

Fullscreen and collapse both hide tiled panes, and each needs to undo only
what it hid. Until now that was done by keeping collapsed panes in a set of
their own and rebuilding panes_to_hide from both sources whenever either
changed, which every caller had to remember to do.

panes_to_hide is now a PanesToHide struct that tracks the two reasons
separately: panes beneath a fullscreen pane, and covered panes such as a
plugin that collapsed itself. set_fullscreen and unset_fullscreen only
touch the first, set_covered and unset_covered only the second, so a
covered pane stays hidden across a fullscreen without any rebuilding, and
the fullscreen pane itself is never hidden.

add_to_hidden_panels and remove_from_hidden_panels had no callers and
wrote straight into the set, so they are gone.

* fix(plugin-api): collapse the slot being rendered for a shared plugin

set_self_collapsed sent env.plugin_id, which for a shared plugin is the
instance and not any of its panes, so a shared plugin such as the bundled
bars could not collapse at all. Resolve the pane through self_pane_id like
the other self commands do, and list SetSelfCollapsed in is_self_command so
a shared plugin can only use it while a slot is current.

* feat(plugin-api): let a shared plugin collapse a slot by id

The other self commands have a slot form that names the pane outright, so
a shared plugin can use them from update or pipe, where no slot is current
and the self form is refused. set_collapsed_slot does the same for
set_self_collapsed, with the ownership check and response the other slot
commands give.

* fix: resurrection, covering selectable panes, mouse clicks, closing, paneinfo

* rebuild assets

---------

Co-authored-by: Aram Drevekenin <[email protected]>
This commit is contained in:
Myah Mitchell
2026-10-06 13:23:29 +02:00
committed by GitHub
co-authored by Aram Drevekenin
parent 7554d23b03
commit 6af3a36eb8
36 changed files with 1370 additions and 29 deletions
+1
View File
@@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* fix: screen surface stability (https://github.com/zellij-org/zellij/pull/5683)
* feat: right-click menu, interactive configuration, popups, notifications (https://github.com/zellij-org/zellij/pull/5685)
* fix: preserve order of command panes when changing swap layouts (https://github.com/zellij-org/zellij/pull/5687)
* feat: let a plugin give its layout space back while it has nothing to draw, so a bar plugin does not hold an empty row (https://github.com/zellij-org/zellij/pull/5590)
## [0.45.1] - 2026-08-28
* fix: nested-session detection over SSH (https://github.com/zellij-org/zellij/pull/5522)
@@ -109,6 +109,20 @@ impl ZellijSharedPlugin for State {
.push(format!("{:?}", set_selectable_slot(u32::MAX, false)));
RenderResponse::All
},
"collapse_slots" => {
let results: Vec<String> = self
.slots
.keys()
.copied()
.collect::<Vec<_>>()
.into_iter()
.map(|slot_id| format!("{:?}", set_collapsed_slot(slot_id, true)))
.collect();
self.self_command_results.extend(results);
self.self_command_results
.push(format!("{:?}", set_collapsed_slot(u32::MAX, true)));
RenderResponse::All
},
"close_first_slot" => {
if let Some(slot_id) = self.slots.keys().next().copied() {
let result = close_slot(slot_id);
@@ -123,6 +137,14 @@ impl ZellijSharedPlugin for State {
}
}
fn render(&mut self, rows: usize, cols: usize, slot_id: SlotId, client_id: ClientId) {
let collapse = self
.slots
.get(&slot_id)
.and_then(|slot| slot.configuration.get("collapse_on_render"))
.is_some();
if collapse {
set_self_collapsed(true);
}
print!("{}", render_line(self, rows, cols, slot_id, client_id));
}
}
+297 -15
View File
@@ -1,4 +1,5 @@
mod pane_resizer;
mod panes_to_hide;
mod stacked_panes;
mod tiled_pane_grid;
@@ -26,6 +27,7 @@ use crate::{
ui::pane_contents_and_ui::PaneContentsAndUi,
ClientId,
};
use panes_to_hide::PanesToHide;
use stacked_panes::StackedPanes;
use zellij_utils::{
data::{
@@ -83,7 +85,7 @@ pub struct TiledPanes {
session_is_mirrored: bool,
active_panes: ActivePanes,
pane_frame_style: PaneFrameStyle,
panes_to_hide: HashSet<PaneId>,
panes_to_hide: PanesToHide,
fullscreen_is_active: Option<PaneId>,
fullscreen_covers_ui: Rc<RefCell<bool>>,
senders: ThreadSenders,
@@ -131,7 +133,7 @@ impl TiledPanes {
session_is_mirrored,
active_panes: ActivePanes::new(&os_api),
pane_frame_style,
panes_to_hide: HashSet::new(),
panes_to_hide: PanesToHide::default(),
fullscreen_is_active: None,
fullscreen_covers_ui,
senders,
@@ -196,9 +198,8 @@ impl TiledPanes {
if self.fullscreen_is_active == Some(pane_id) {
self.fullscreen_is_active = Some(with_pane_id);
}
if self.panes_to_hide.remove(&pane_id) {
self.panes_to_hide.insert(with_pane_id);
}
self.panes_to_hide
.replace_beneath_fullscreen(&pane_id, with_pane_id);
self.move_clients_between_panes(pane_id, with_pane_id);
self.active_panes.retarget_last_pane(pane_id, with_pane_id);
@@ -574,6 +575,11 @@ impl TiledPanes {
// for the other panes in this tab
let is_ui_pane =
!p.selectable() || (p.borderless() && matches!(p.pid(), PaneId::Plugin(_)));
// a collapsed pane holds no space, so it must not shrink the viewport either,
// or the panes that grew over it would be sized as if it were still there
if self.panes_to_hide.is_covered(&p.pid()) {
return None;
}
if is_ui_pane && is_inside_viewport(&self.viewport.borrow(), p) {
Some(geom.into())
} else {
@@ -1075,6 +1081,10 @@ impl TiledPanes {
log::error!("Cannot focus pane {:?} as it is not selectable!", pane_id);
return;
}
if self.panes_to_hide.is_covered(&pane_id) && self.fullscreen_is_active != Some(pane_id) {
log::error!("Cannot focus pane {:?} as it is collapsed!", pane_id);
return;
}
if let Some(focused_pane) = self.active_panes.get(&client_id).copied() {
if pane_id != focused_pane {
self.active_panes.set_last_pane(client_id, focused_pane);
@@ -2726,6 +2736,7 @@ impl TiledPanes {
}
pub fn extract_pane(&mut self, pane_id: PaneId) -> Option<Box<dyn Pane>> {
self.reset_boundaries();
self.panes_to_hide.forget(&pane_id);
self.panes.remove(&pane_id)
}
pub fn remove_pane(&mut self, pane_id: PaneId) -> Option<Box<dyn Pane>> {
@@ -2733,13 +2744,24 @@ impl TiledPanes {
.panes
.get(&pane_id)
.and_then(|pane| pane.position_and_size().logical_position);
let neighbors_already_hold_its_space =
self.panes_to_hide.is_covered(&pane_id) && self.fullscreen_is_active != Some(pane_id);
if neighbors_already_hold_its_space {
self.hand_collapsed_pane_constraints_to_neighbors(pane_id);
}
self.panes_to_hide.forget(&pane_id);
let mut pane_grid = TiledPaneGrid::new(
&mut self.panes,
&self.panes_to_hide,
*self.display_area.borrow(),
*self.viewport.borrow(),
);
let closed_pane = if pane_grid.fill_space_over_pane(pane_id) {
let closed_pane = if neighbors_already_hold_its_space {
let closed_pane = self.panes.remove(&pane_id);
self.move_clients_out_of_pane(pane_id);
self.set_pane_frames(self.pane_frame_style);
closed_pane
} else if pane_grid.fill_space_over_pane(pane_id) {
// successfully filled space over pane
let closed_pane = self.panes.remove(&pane_id);
self.move_clients_out_of_pane(pane_id);
@@ -2797,8 +2819,7 @@ impl TiledPanes {
}
pub fn unset_fullscreen(&mut self) {
if let Some(fullscreen_pane_id) = self.fullscreen_is_active {
let panes_to_hide: Vec<_> = self.panes_to_hide.iter().copied().collect();
for pane_id in panes_to_hide {
for pane_id in self.panes_to_hide.unset_fullscreen() {
if let Some(pane) = self.get_pane_mut(pane_id) {
pane.set_should_render(true);
pane.set_should_render_boundaries(true);
@@ -2810,7 +2831,6 @@ impl TiledPanes {
pane.reset_size_and_position_override();
}
}
self.panes_to_hide.clear();
if let Some(fullscreen_pane) = self.get_pane_mut(fullscreen_pane_id) {
fullscreen_pane.reset_size_and_position_override();
}
@@ -2856,10 +2876,11 @@ impl TiledPanes {
}
fn set_fullscreen(&mut self, pane_id: PaneId, covers_ui: bool) {
self.panes_to_hide = self.panes_covered_by_fullscreen(pane_id, covers_ui);
if self.panes_to_hide.is_empty() && !covers_ui {
let covered = self.panes_covered_by_fullscreen(pane_id, covers_ui);
if covered.is_empty() && !covers_ui {
return;
}
self.panes_to_hide.set_fullscreen(pane_id, covered);
if covers_ui {
self.expand_pane_over_whole_display(pane_id);
} else {
@@ -3005,11 +3026,189 @@ impl TiledPanes {
pub fn visible_panes_count(&self) -> usize {
self.panes.len().saturating_sub(self.panes_to_hide.len())
}
pub fn add_to_hidden_panels(&mut self, pid: PaneId) {
self.panes_to_hide.insert(pid);
/// Take a pane out of the layout's space, or put it back, and say whether that changed
/// anything.
///
/// A collapsed pane drops out of the constraint solve, so its neighbors grow over the
/// space it held. It keeps its size constraint the whole time, so expanding gives back
/// exactly the row or column the layout asked for rather than an approximation of it.
///
/// The caller relays out the tab afterwards, which is also where the pane's geometry is
/// kept current: see `take_collapsed_panes`.
pub fn set_pane_collapsed(&mut self, pane_id: PaneId, collapsed: bool) -> bool {
if !collapsed {
return self.panes_to_hide.unset_covered(&pane_id);
}
if !self.panes_to_hide.set_covered(pane_id) {
return false;
}
let another_pane_can_take_focus = self.panes.iter().any(|(id, pane)| {
*id != pane_id && pane.selectable() && !self.panes_to_hide.contains(id)
});
if another_pane_can_take_focus {
self.move_clients_out_of_pane(pane_id);
}
true
}
pub fn remove_from_hidden_panels(&mut self, pid: PaneId) {
self.panes_to_hide.remove(&pid);
pub fn serialization_geoms_with_collapsed_panes_expanded(&self) -> HashMap<PaneId, PaneGeom> {
let collapsed_spaces: Vec<PaneGeom> = self
.panes_to_hide
.covered()
.filter_map(|pane_id| self.panes.get(pane_id))
.map(|pane| pane.position_and_size())
.collect();
self.geoms_with_spaces_given_back(&collapsed_spaces)
}
fn geoms_with_spaces_given_back(
&self,
collapsed_spaces: &[PaneGeom],
) -> HashMap<PaneId, PaneGeom> {
let mut geoms: HashMap<PaneId, PaneGeom> = HashMap::new();
if collapsed_spaces.is_empty() {
return geoms;
}
let mut stacks: BTreeMap<usize, Vec<(PaneId, PaneGeom)>> = BTreeMap::new();
let mut standalone_panes = vec![];
for (pane_id, pane) in &self.panes {
if self.panes_to_hide.is_covered(pane_id) {
continue;
}
let geom = pane.position_and_size();
match geom.stacked {
Some(stack_id) => stacks
.entry(stack_id)
.or_insert_with(Vec::new)
.push((*pane_id, geom)),
None => standalone_panes.push((*pane_id, geom)),
}
}
for (pane_id, geom) in standalone_panes {
let mut expanded = geom;
for space in collapsed_spaces {
if let Some(shrunk) = shrink_off_collapsed_space(expanded, space) {
expanded = shrunk;
}
}
if expanded != geom {
geoms.insert(pane_id, expanded);
}
}
for (_stack_id, mut members) in stacks {
members.sort_by_key(|(_, geom)| geom.y);
let Some(stack_geom) = bounding_geom_of_stack(&members) else {
continue;
};
let mut expanded = stack_geom;
for space in collapsed_spaces {
if let Some(shrunk) = shrink_off_collapsed_space(expanded, space) {
expanded = shrunk;
}
}
if expanded == stack_geom {
continue;
}
for (pane_id, geom) in fit_stack_members_into(&members, expanded) {
geoms.insert(pane_id, geom);
}
}
geoms
}
fn hand_collapsed_pane_constraints_to_neighbors(&mut self, pane_id: PaneId) {
let Some(space) = self
.panes
.get(&pane_id)
.map(|pane| pane.position_and_size())
else {
return;
};
let expanded_geoms = self.geoms_with_spaces_given_back(&[space]);
let mut row_gains = vec![];
let mut col_gains = vec![];
for (neighbor_id, expanded) in &expanded_geoms {
let Some(current) = self.panes.get(neighbor_id).map(|p| p.position_and_size()) else {
continue;
};
let row_gain = current
.rows
.as_usize()
.saturating_sub(expanded.rows.as_usize());
let col_gain = current
.cols
.as_usize()
.saturating_sub(expanded.cols.as_usize());
if row_gain > 0 && current.rows.is_percent() {
row_gains.push((*neighbor_id, row_gain));
}
if col_gain > 0 && current.cols.is_percent() {
col_gains.push((*neighbor_id, col_gain));
}
}
let distribute = |gains: &[(PaneId, usize)], freed_percent: Option<f64>| {
let total_gain: usize = gains.iter().map(|(_, gain)| gain).sum();
freed_percent
.filter(|_| total_gain > 0)
.map(|freed_percent| {
gains
.iter()
.map(|(id, gain)| (*id, freed_percent * *gain as f64 / total_gain as f64))
.collect::<Vec<_>>()
})
.unwrap_or_default()
};
for (neighbor_id, share) in distribute(&row_gains, space.rows.as_percent()) {
if let Some(neighbor) = self.panes.get_mut(&neighbor_id) {
let mut geom = neighbor.position_and_size();
if let Some(percent) = geom.rows.as_percent() {
geom.rows.set_percent(percent + share);
neighbor.set_geom(geom);
}
}
}
for (neighbor_id, share) in distribute(&col_gains, space.cols.as_percent()) {
if let Some(neighbor) = self.panes.get_mut(&neighbor_id) {
let mut geom = neighbor.position_and_size();
if let Some(percent) = geom.cols.as_percent() {
geom.cols.set_percent(percent + share);
neighbor.set_geom(geom);
}
}
}
}
pub fn pane_is_collapsed(&self, pane_id: &PaneId) -> bool {
self.panes_to_hide.is_covered(pane_id)
}
/// Let the collapsed panes take part in the next solve, and hand them back so the caller
/// can collapse them again over the result.
///
/// Being filtered out of the solve is what stops a collapsed pane from holding space, but
/// it also means nothing writes geometry to it, so across a resize or a relayout it would
/// be left describing a display area that no longer exists. That is worse than merely
/// stale: the solver reconstructs the layout tree from where the panes currently sit, so
/// one pane in the wrong place gives the next solve a tree that does not match the screen.
/// Solving with them and then collapsing again over the answer keeps their geometry
/// current for the moment they are expanded.
///
/// Paired with `restore_collapsed_panes`. Nesting is safe: the inner call finds nothing
/// left to take and restores nothing.
pub fn take_collapsed_panes(&mut self) -> HashSet<PaneId> {
self.panes_to_hide.take_covered()
}
/// Put back what `take_collapsed_panes` took, and say whether there was anything to put
/// back. The caller solves again when there was, so the neighbors reclaim the space.
///
/// A pane that closed while the set was taken out was never forgotten by `panes_to_hide`,
/// so anything that is no longer here is dropped on the way in.
pub fn restore_collapsed_panes(&mut self, collapsed: HashSet<PaneId>) -> bool {
let panes = &self.panes;
let still_here: HashSet<PaneId> = collapsed
.into_iter()
.filter(|pane_id| panes.contains_key(pane_id))
.collect();
if still_here.is_empty() {
return false;
}
self.panes_to_hide.restore_covered(still_here);
true
}
pub fn unfocus_all_panes(&mut self) {
let connected_clients = self.connected_clients.borrow().clone();
@@ -3075,6 +3274,7 @@ impl TiledPanes {
let is_focused = self.active_panes.pane_id_is_focused(pane_id);
pane_info_for_pane.is_floating = false;
pane_info_for_pane.is_suppressed = false;
pane_info_for_pane.is_collapsed = self.panes_to_hide.is_covered(pane_id);
pane_info_for_pane.is_focused = is_focused;
pane_info_for_pane.is_fullscreen = is_focused && self.fullscreen_is_active();
pane_infos.push(pane_info_for_pane);
@@ -3195,3 +3395,85 @@ pub fn pane_geom_is_inside_viewport(viewport: &Viewport, geom: &PaneGeom) -> boo
&& geom.x >= viewport.x
&& geom.x + geom.cols.as_usize() <= viewport.x + viewport.cols
}
fn shrink_off_collapsed_space(geom: PaneGeom, space: &PaneGeom) -> Option<PaneGeom> {
let cols = (geom.x, geom.x + geom.cols.as_usize());
let rows = (geom.y, geom.y + geom.rows.as_usize());
let space_cols = (space.x, space.x + space.cols.as_usize());
let space_rows = (space.y, space.y + space.rows.as_usize());
let overlaps = cols.0 < space_cols.1
&& space_cols.0 < cols.1
&& rows.0 < space_rows.1
&& space_rows.0 < rows.1;
if !overlaps {
return None;
}
let mut shrunk = geom;
if space_cols.0 <= cols.0 && cols.1 <= space_cols.1 {
let (start, end) = shrink_span_off(rows, space_rows)?;
shrunk.y = start;
shrunk.rows.set_inner(end - start);
} else if space_rows.0 <= rows.0 && rows.1 <= space_rows.1 {
let (start, end) = shrink_span_off(cols, space_cols)?;
shrunk.x = start;
shrunk.cols.set_inner(end - start);
} else {
return None;
}
Some(shrunk)
}
fn shrink_span_off(
(start, end): (usize, usize),
(space_start, space_end): (usize, usize),
) -> Option<(usize, usize)> {
let (new_start, new_end) = if start < space_start {
(start, space_start.min(end))
} else {
(space_end.max(start), end)
};
if new_end > new_start {
Some((new_start, new_end))
} else {
None
}
}
fn bounding_geom_of_stack(members: &[(PaneId, PaneGeom)]) -> Option<PaneGeom> {
let (_, first) = members.first()?;
let (_, last) = members.last()?;
let mut bounding = *first;
bounding
.rows
.set_inner((last.y + last.rows.as_usize()).saturating_sub(first.y));
Some(bounding)
}
fn fit_stack_members_into(
members: &[(PaneId, PaneGeom)],
stack_geom: PaneGeom,
) -> Vec<(PaneId, PaneGeom)> {
let flexible_position = members
.iter()
.position(|(_, geom)| !geom.rows.is_fixed())
.unwrap_or(0);
let flexible_rows = stack_geom.rows.as_usize().saturating_sub(members.len()) + 1;
members
.iter()
.enumerate()
.map(|(i, (pane_id, geom))| {
let mut fitted = *geom;
fitted.x = stack_geom.x;
fitted.cols.set_inner(stack_geom.cols.as_usize());
if i == flexible_position {
fitted.rows.set_inner(flexible_rows);
}
fitted.y = if i <= flexible_position {
stack_geom.y + i
} else {
stack_geom.y + i + flexible_rows - 1
};
(*pane_id, fitted)
})
.collect()
}
@@ -0,0 +1,121 @@
use std::collections::HashSet;
use crate::panes::PaneId;
/// The tiled panes that are left out of the layout and not rendered.
///
/// A pane can be hidden for two separate reasons, and each is tracked on its own so that one
/// cannot undo the other:
/// - it is beneath a fullscreen pane, which lasts until fullscreen is unset
/// - it is covered, eg. a plugin that collapsed itself to give its space back to its
/// neighbors, which lasts until it is uncovered, across entering and leaving fullscreen
#[derive(Debug, Default)]
pub struct PanesToHide {
fullscreen_pane: Option<PaneId>,
beneath_fullscreen: HashSet<PaneId>,
covered: HashSet<PaneId>,
}
impl PanesToHide {
pub fn contains(&self, pane_id: &PaneId) -> bool {
self.beneath_fullscreen.contains(pane_id)
|| (self.covered.contains(pane_id) && self.fullscreen_pane != Some(*pane_id))
}
pub fn len(&self) -> usize {
let covered_and_not_beneath_fullscreen = self
.covered
.iter()
.filter(|pane_id| {
!self.beneath_fullscreen.contains(pane_id)
&& self.fullscreen_pane != Some(**pane_id)
})
.count();
self.beneath_fullscreen.len() + covered_and_not_beneath_fullscreen
}
/// The fullscreen pane itself is never hidden, even if it is also covered.
pub fn set_fullscreen(&mut self, fullscreen_pane: PaneId, beneath: HashSet<PaneId>) {
self.fullscreen_pane = Some(fullscreen_pane);
self.beneath_fullscreen = beneath;
}
/// Returns the panes that were beneath the fullscreen pane, so they can be rendered again.
/// Covered panes stay hidden.
pub fn unset_fullscreen(&mut self) -> HashSet<PaneId> {
self.fullscreen_pane = None;
std::mem::take(&mut self.beneath_fullscreen)
}
pub fn replace_beneath_fullscreen(&mut self, pane_id: &PaneId, with_pane_id: PaneId) {
if self.beneath_fullscreen.remove(pane_id) {
self.beneath_fullscreen.insert(with_pane_id);
}
}
/// Returns whether this changed anything.
pub fn set_covered(&mut self, pane_id: PaneId) -> bool {
self.covered.insert(pane_id)
}
/// Returns whether this changed anything.
pub fn unset_covered(&mut self, pane_id: &PaneId) -> bool {
self.covered.remove(pane_id)
}
pub fn is_covered(&self, pane_id: &PaneId) -> bool {
self.covered.contains(pane_id)
}
pub fn covered(&self) -> impl Iterator<Item = &PaneId> {
self.covered.iter()
}
/// Uncover every covered pane and return them, to be covered again with `restore_covered`.
pub fn take_covered(&mut self) -> HashSet<PaneId> {
std::mem::take(&mut self.covered)
}
pub fn restore_covered(&mut self, covered: HashSet<PaneId>) {
self.covered.extend(covered);
}
/// Drop every trace of a pane that is leaving, so that a pane id that is handed out again
/// does not arrive already hidden.
pub fn forget(&mut self, pane_id: &PaneId) {
self.beneath_fullscreen.remove(pane_id);
self.covered.remove(pane_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn covered_pane_stays_hidden_after_fullscreen_is_unset() {
let mut panes_to_hide = PanesToHide::default();
panes_to_hide.set_covered(PaneId::Plugin(1));
panes_to_hide.set_fullscreen(
PaneId::Terminal(1),
HashSet::from([PaneId::Terminal(2), PaneId::Plugin(1)]),
);
assert_eq!(panes_to_hide.len(), 2);
let beneath = panes_to_hide.unset_fullscreen();
assert_eq!(beneath.len(), 2);
assert!(panes_to_hide.contains(&PaneId::Plugin(1)));
assert!(!panes_to_hide.contains(&PaneId::Terminal(2)));
assert_eq!(panes_to_hide.len(), 1);
}
#[test]
fn fullscreen_pane_is_not_hidden_even_if_covered() {
let mut panes_to_hide = PanesToHide::default();
panes_to_hide.set_covered(PaneId::Terminal(1));
panes_to_hide.set_fullscreen(PaneId::Terminal(1), HashSet::from([PaneId::Terminal(2)]));
assert!(!panes_to_hide.contains(&PaneId::Terminal(1)));
assert_eq!(panes_to_hide.len(), 1);
panes_to_hide.unset_fullscreen();
assert!(panes_to_hide.contains(&PaneId::Terminal(1)));
}
#[test]
fn forgotten_pane_is_no_longer_hidden() {
let mut panes_to_hide = PanesToHide::default();
panes_to_hide.set_covered(PaneId::Plugin(1));
panes_to_hide.set_fullscreen(PaneId::Terminal(1), HashSet::from([PaneId::Terminal(2)]));
panes_to_hide.forget(&PaneId::Plugin(1));
panes_to_hide.forget(&PaneId::Terminal(2));
assert_eq!(panes_to_hide.len(), 0);
assert!(!panes_to_hide.is_covered(&PaneId::Plugin(1)));
}
}
@@ -1,3 +1,4 @@
use super::panes_to_hide::PanesToHide;
use crate::{
panes::PaneId,
tab::{Pane, MIN_TERMINAL_HEIGHT},
@@ -20,7 +21,7 @@ impl<'a> StackedPanes<'a> {
}
pub fn new_from_btreemap(
panes: impl IntoIterator<Item = (&'a PaneId, &'a mut Box<dyn Pane>)>,
panes_to_hide: &HashSet<PaneId>,
panes_to_hide: &PanesToHide,
) -> Self {
let panes: HashMap<_, _> = panes
.into_iter()
@@ -1,5 +1,6 @@
use super::is_inside_viewport;
use super::pane_resizer::PaneResizer;
use super::panes_to_hide::PanesToHide;
use super::stacked_panes::StackedPanes;
use crate::tab::{MIN_TERMINAL_HEIGHT, MIN_TERMINAL_WIDTH};
use crate::{panes::PaneId, tab::Pane};
@@ -34,7 +35,7 @@ pub struct TiledPaneGrid<'a> {
impl<'a> TiledPaneGrid<'a> {
pub fn new(
panes: impl IntoIterator<Item = (&'a PaneId, &'a mut Box<dyn Pane>)>,
panes_to_hide: &HashSet<PaneId>,
panes_to_hide: &PanesToHide,
display_area: Size,
viewport: Viewport,
) -> Self {
@@ -1,4 +1,5 @@
use super::plugin_tests::create_plugin_thread;
use crate::panes::PaneId;
use crate::plugins::{PluginId, PluginInstruction};
use crate::screen::ScreenInstruction;
use crate::ClientId;
@@ -77,6 +78,7 @@ struct ScreenLog {
cache_path: PathBuf,
renders: Renders,
render_log: Vec<(PluginId, ClientId, String)>,
collapsed: HashSet<PaneId>,
}
impl ScreenLog {
@@ -91,6 +93,7 @@ impl ScreenLog {
cache_path,
renders: HashMap::new(),
render_log: vec![],
collapsed: HashSet::new(),
}
}
fn receive_one(&mut self, timeout: Duration) {
@@ -121,6 +124,9 @@ impl ScreenLog {
Some(self.cache_path.clone()),
));
},
ScreenInstruction::SetPaneCollapsed(pane_id, true) => {
self.collapsed.insert(pane_id);
},
_ => {},
}
}
@@ -464,6 +470,99 @@ pub fn shared_plugin_slot_commands_are_checked() {
);
}
#[test]
#[ignore]
pub fn shared_plugin_collapses_the_slot_it_renders() {
let (sender, mut screen, teardown, _temp_folder) = start();
let client_id = 1;
let _ = sender.send(PluginInstruction::AddClient(client_id));
load(
&sender,
shared_fixture(&[("label", "a"), ("collapse_on_render", "true")]),
0,
client_id,
);
load(
&sender,
shared_fixture(&[("label", "b"), ("collapse_on_render", "true")]),
1,
client_id,
);
screen.wait_until("two slots rendered", |renders| {
slots_of_client(renders, client_id).len() == 2
});
let slots = slots_of_client(&screen.renders, client_id);
let expected: HashSet<PaneId> = slots
.iter()
.map(|slot_id| PaneId::Plugin(*slot_id))
.collect();
let deadline = Instant::now() + Duration::from_secs(30);
while screen.collapsed != expected && Instant::now() < deadline {
screen.receive_one(Duration::from_millis(100));
}
teardown();
assert_eq!(
screen.collapsed, expected,
"each slot collapses its own pane, not the instance's"
);
}
#[test]
#[ignore]
pub fn shared_plugin_collapses_a_slot_by_id() {
let (sender, mut screen, teardown, _temp_folder) = start();
let client_id = 1;
let _ = sender.send(PluginInstruction::AddClient(client_id));
load(&sender, shared_fixture(&[("label", "a")]), 0, client_id);
load(&sender, shared_fixture(&[("label", "b")]), 1, client_id);
screen.wait_until("two slots rendered", |renders| {
slots_of_client(renders, client_id).len() == 2
});
let slots = slots_of_client(&screen.renders, client_id);
let _ = sender.send(PluginInstruction::KeybindPipe {
name: "collapse_slots".to_owned(),
payload: None,
plugin: None,
args: None,
configuration: None,
floating: None,
pane_id_to_replace: None,
pane_title: None,
cwd: None,
skip_cache: false,
cli_client_id: client_id,
plugin_and_client_id: Some((slots[0], client_id)),
notification_end: None,
});
screen.wait_until("slot command results rendered", |renders| {
field(&renders[&(slots[0], client_id)], "self_commands").contains("Err")
});
let expected: HashSet<PaneId> = slots
.iter()
.map(|slot_id| PaneId::Plugin(*slot_id))
.collect();
let deadline = Instant::now() + Duration::from_secs(30);
while screen.collapsed != expected && Instant::now() < deadline {
screen.receive_one(Duration::from_millis(100));
}
teardown();
assert_eq!(
screen.collapsed, expected,
"every slot of the plugin is collapsed, from outside of render"
);
let results = field(&screen.renders[&(slots[0], client_id)], "self_commands");
assert!(
results.starts_with("[\"Ok(())\", \"Ok(())\""),
"own slots are accepted: {}",
results
);
assert!(
results.contains("does not belong"),
"foreign slot is rejected: {}",
results
);
}
#[test]
#[ignore]
pub fn legacy_plugin_keeps_one_instance_per_client() {
@@ -212,6 +212,7 @@ fn is_self_command(command: &PluginCommand) -> bool {
| PluginCommand::ShowSelf(..)
| PluginCommand::CloseSelf
| PluginCommand::SetSelfMouseSelectionSupport(..)
| PluginCommand::SetSelfCollapsed(..)
) || self_command_has_response(command)
}
@@ -313,6 +314,18 @@ fn close_slot(env: &PluginEnv, slot_id: PluginId) {
write_slot_command_response(env, result);
}
fn set_collapsed_slot(env: &PluginEnv, slot_id: PluginId, collapsed: bool) {
let result = slot_command_target(env, slot_id).and_then(|_| {
env.senders
.send_to_screen(ScreenInstruction::SetPaneCollapsed(
PaneId::Plugin(slot_id),
collapsed,
))
.map_err(|e| e.to_string())
});
write_slot_command_response(env, result);
}
fn open_context_menu(env: &PluginEnv, target: ContextMenuTarget, line: usize, column: usize) {
env.senders
.send_to_screen(ScreenInstruction::OpenContextMenuFromPlugin {
@@ -456,6 +469,9 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
show_slot(env, slot_id, should_float_if_hidden)
},
PluginCommand::CloseSlot(slot_id) => close_slot(env, slot_id),
PluginCommand::SetCollapsedSlot(slot_id, collapsed) => {
set_collapsed_slot(env, slot_id, collapsed)
},
PluginCommand::OpenContextMenu {
target,
line,
@@ -1056,6 +1072,9 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
PluginCommand::SetSelfMouseSelectionSupport(selection_support) => {
set_self_mouse_selection_support(env, selection_support);
},
PluginCommand::SetSelfCollapsed(collapsed) => {
set_self_collapsed(env, collapsed);
},
PluginCommand::GenerateWebLoginToken(token_label, read_only) => {
generate_web_login_token(env, token_label, read_only);
},
@@ -6041,6 +6060,22 @@ fn set_self_mouse_selection_support(env: &PluginEnv, selection_support: bool) {
.non_fatal();
}
fn set_self_collapsed(env: &PluginEnv, collapsed: bool) {
env.senders
.send_to_screen(ScreenInstruction::SetPaneCollapsed(
PaneId::Plugin(self_pane_id(env)),
collapsed,
))
.with_context(|| {
format!(
"failed to set collapsed {} from plugin {}",
collapsed,
env.name()
)
})
.non_fatal();
}
fn intercept_key_presses(env: &mut PluginEnv) {
env.intercepting_key_presses = true;
let _ = env
+28 -2
View File
@@ -930,6 +930,7 @@ pub enum ScreenInstruction {
ToggleGroupMarking(ClientId, Option<NotificationEnd>),
SessionSharingStatusChange(bool),
SetMouseSelectionSupport(PaneId, bool),
SetPaneCollapsed(PaneId, bool),
InterceptKeyPresses(PluginId, ClientId),
ClearKeyPressesIntercepts(ClientId),
TogglePaneIdInGroup(PaneId, ClientId, Option<NotificationEnd>),
@@ -1376,6 +1377,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::SetMouseSelectionSupport(..) => {
ScreenContext::SetMouseSelectionSupport
},
ScreenInstruction::SetPaneCollapsed(..) => ScreenContext::SetPaneCollapsed,
ScreenInstruction::InterceptKeyPresses(..) => ScreenContext::InterceptKeyPresses,
ScreenInstruction::ClearKeyPressesIntercepts(..) => {
ScreenContext::ClearKeyPressesIntercepts
@@ -8696,7 +8698,7 @@ impl Screen {
);
}
let stack_list_geoms = tab.stack_list_serialization_geoms();
let serialization_geoms = tab.tiled_pane_serialization_geoms();
let tiled_panes: Vec<PaneLayoutMetadata> = tab
.get_tiled_panes()
.map(|(pane_id, p)| {
@@ -8705,7 +8707,7 @@ impl Screen {
// is currently only the case the scrollback editing panes, and
// when dumping the layout we want the "real" pane and not the
// editor pane
let geom_override = stack_list_geoms.get(pane_id).copied();
let geom_override = serialization_geoms.get(pane_id).copied();
let (pane_id, p) = match suppressed_panes.remove(pane_id) {
Some((is_scrollback_editor, suppressed_pane)) if *is_scrollback_editor => {
(suppressed_pane.pid(), suppressed_pane)
@@ -10966,6 +10968,30 @@ pub(crate) fn screen_thread_main(
screen.render(None)?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::SetPaneCollapsed(pid, collapsed) => {
let all_tabs = screen.get_tabs_mut();
let mut found_pane = false;
for tab in all_tabs.values_mut() {
if tab.has_pane_with_pid(&pid) {
tab.set_pane_collapsed(pid, collapsed);
found_pane = true;
break;
}
}
if !found_pane {
// a plugin can ask for this before its pane has been placed in a tab. Unlike
// the other deferred instructions this one is sent whenever the plugin's
// content comes and goes, so only the last word on a given pane is worth
// keeping and the queue stays one entry deep per pane
pending_events_waiting_for_tab.retain(|event| {
!matches!(event, ScreenInstruction::SetPaneCollapsed(pending_pid, _) if *pending_pid == pid)
});
pending_events_waiting_for_tab
.push(ScreenInstruction::SetPaneCollapsed(pid, collapsed));
}
screen.render(None)?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::ClosePane(
id,
client_id,
+74 -6
View File
@@ -1200,10 +1200,13 @@ impl Tab {
.filter(|(pane_id, _)| self.stack_list_of_member.contains_key(pane_id))
.map(|(pane_id, (_, pane))| (pane_id, pane))
}
pub fn stack_list_serialization_geoms(&self) -> HashMap<PaneId, PaneGeom> {
pub fn tiled_pane_serialization_geoms(&self) -> HashMap<PaneId, PaneGeom> {
let expanded_geoms = self
.tiled_panes
.serialization_geoms_with_collapsed_panes_expanded();
let mut synthetic_geoms = HashMap::new();
if self.stack_lists.is_empty() {
return synthetic_geoms;
return expanded_geoms;
}
let mut next_synthetic_stack_id = self
.tiled_panes
@@ -1216,7 +1219,10 @@ impl Tab {
let Some(visible_pane) = self.tiled_panes.get_pane(list.visible) else {
continue;
};
let full_rect = visible_pane.position_and_size();
let full_rect = expanded_geoms
.get(&list.visible)
.copied()
.unwrap_or_else(|| visible_pane.position_and_size());
let collapsed_member_count = list.members.len().saturating_sub(1);
let mut running_y = full_rect.y;
for member in &list.members {
@@ -1242,12 +1248,15 @@ impl Tab {
}
next_synthetic_stack_id += 1;
}
for (pane_id, geom) in expanded_geoms {
synthetic_geoms.entry(pane_id).or_insert(geom);
}
synthetic_geoms
}
pub fn hidden_stack_list_members_for_serialization(
&self,
) -> Vec<(PaneId, &Box<dyn Pane>, PaneGeom)> {
let synthetic_geoms = self.stack_list_serialization_geoms();
let synthetic_geoms = self.tiled_pane_serialization_geoms();
let mut hidden_members = vec![];
for list in self.stack_lists.values() {
for member in &list.members {
@@ -2106,11 +2115,34 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
self.tiled_panes.unset_fullscreen();
}
// a new layout places every pane, so the collapsed ones take part and are collapsed
// again over the result. See TiledPanes::take_collapsed_panes.
let collapsed_panes_to_restore = self.tiled_panes.take_collapsed_panes();
self.dissolve_stack_lists_for_classic_mutation();
let layout_candidate = self
.swap_layouts
.swap_tiled_panes(&self.tiled_panes, search_backwards);
self.apply_tiled_layout_candidate(layout_candidate)
let result = self.apply_tiled_layout_candidate(layout_candidate);
self.restore_collapsed_panes_after_relayout(collapsed_panes_to_restore);
result
}
/// The second half of `TiledPanes::take_collapsed_panes`: collapse the panes again over
/// the layout that was just applied, and let the rest of the layout take their rows.
///
/// The layout offsets the viewport while the collapsed panes are still taking part, so a
/// collapsed bar would go on holding its row out of the viewport. Offset it again once
/// they are gone, the same as `resize_whole_tab` does.
fn restore_collapsed_panes_after_relayout(&mut self, collapsed_panes: HashSet<PaneId>) {
if self.tiled_panes.restore_collapsed_panes(collapsed_panes) {
let display_area = *self.display_area.borrow();
self.tiled_panes.resize(display_area);
LayoutApplier::offset_viewport(
self.viewport.clone(),
self.display_area.clone(),
&mut self.tiled_panes,
self.pane_frame_style,
);
}
}
fn apply_tiled_layout_candidate(
&mut self,
@@ -2175,10 +2207,14 @@ impl Tab {
pane_count
}
pub fn apply_tiled_swap_layout(&mut self, layout_name: &str) -> Result<bool> {
// taken before the candidate is chosen, so the layout is sized for the collapsed
// panes too, the same as relayout_tiled_panes
let collapsed_panes_to_restore = self.tiled_panes.take_collapsed_panes();
let Some((position, layout_candidate)) = self
.swap_layouts
.tiled_layout_candidate_by_name(layout_name, self.settled_tiled_pane_count())
else {
self.restore_collapsed_panes_after_relayout(collapsed_panes_to_restore);
return Ok(false);
};
if self.tiled_panes.fullscreen_is_active() {
@@ -2187,7 +2223,9 @@ impl Tab {
self.dissolve_stack_lists_for_classic_mutation();
self.swap_layouts
.set_current_tiled_layout_position(position);
self.apply_tiled_layout_candidate(Some(layout_candidate))?;
let result = self.apply_tiled_layout_candidate(Some(layout_candidate));
self.restore_collapsed_panes_after_relayout(collapsed_panes_to_restore);
result?;
Ok(true)
}
pub fn apply_floating_swap_layout(&mut self, layout_name: &str) -> Result<bool> {
@@ -5532,6 +5570,9 @@ impl Tab {
// resize so the user-visible state is preserved. Without this, hidden
// panes retain stale geometry from before the resize and the layout
// solver fails when fullscreen is later toggled off.
// Collapsed panes come along for the same reason, and are collapsed again below once
// the new geometry is theirs. See TiledPanes::take_collapsed_panes.
let collapsed_panes_to_restore = self.tiled_panes.take_collapsed_panes();
let fullscreen_pane_to_restore = self.tiled_panes.fullscreen_pane_id();
let fullscreen_covered_ui = self.tiled_panes.fullscreen_covers_ui();
if fullscreen_pane_to_restore.is_some() {
@@ -5561,6 +5602,13 @@ impl Tab {
self.swap_layouts.set_is_tiled_damaged();
let _ = self.relayout_tiled_panes(false);
}
if self
.tiled_panes
.restore_collapsed_panes(collapsed_panes_to_restore)
{
// solve once more, now without them, so their neighbors take the space back
self.tiled_panes.resize(new_screen_size);
}
self.set_should_clear_display_before_rendering();
LayoutApplier::offset_viewport(
self.viewport.clone(),
@@ -6110,6 +6158,24 @@ impl Tab {
pub fn set_mouse_selection_support(&mut self, pane_id: PaneId, selection_support: bool) {
MouseHandler::set_mouse_selection_support(self, pane_id, selection_support);
}
/// Hand a tiled pane's space to its neighbors, or take it back.
///
/// A pane that has nothing to show, such as a status bar whose content is empty, would
/// otherwise keep the row a layout reserved for it and draw it blank. Collapsing takes it
/// out of the layout's arithmetic without taking it out of the layout, so expanding puts it
/// back at the size the layout asked for.
///
/// Floating panes take no space from their neighbors, so there is nothing to give back.
pub fn set_pane_collapsed(&mut self, pane_id: PaneId, collapsed: bool) {
if !self.tiled_panes.panes_contain(&pane_id) {
return;
}
if !self.tiled_panes.set_pane_collapsed(pane_id, collapsed) {
return;
}
let size = self.size;
self.resize_whole_tab(size).non_fatal();
}
pub fn close_pane(
&mut self,
id: PaneId,
@@ -6971,10 +7037,12 @@ impl Tab {
let found_pane_id = if search_selectable {
self.get_selectable_tiled_panes()
.filter(|(id, _)| !self.tiled_panes.pane_is_collapsed(id))
.find(|(_, p)| pane_contains_point(p, point, &stacked_pane_ids_under_flexible_pane))
.map(|(&id, _)| id)
} else {
self.get_tiled_panes()
.filter(|(id, _)| !self.tiled_panes.pane_is_collapsed(id))
.find(|(_, p)| pane_contains_point(p, point, &stacked_pane_ids_under_flexible_pane))
.map(|(&id, _)| id)
};
@@ -17698,3 +17698,77 @@ fn mouse_events_work_again_when_the_selecting_pane_is_gone() {
.unwrap();
assert!(tab.selecting_with_mouse_in_pane.is_none());
}
#[test]
fn a_collapsed_bar_gives_its_row_to_the_viewport_across_a_swap_layout() {
let size = Size {
cols: 121,
rows: 20,
};
let base_layout = r#"
layout {
pane
pane size=1 borderless=true {
plugin location="zellij:status-bar"
}
}
"#;
let swap_layouts = r#"
layout {
swap_tiled_layout {
tab {
pane size=1 borderless=true {
plugin location="zellij:status-bar"
}
pane
}
}
}
"#;
let (base_layout, base_floating_layout) =
Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None)
.unwrap()
.template
.unwrap();
let mut new_plugin_ids = HashMap::new();
new_plugin_ids.insert(
RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap(),
vec![1],
);
let swap_layout =
Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap();
let mut tab = create_new_tab_with_swap_layouts(
size,
ModeInfo::default(),
(
swap_layout.swap_tiled_layouts.clone(),
swap_layout.swap_floating_layouts.clone(),
),
Some((
base_layout,
base_floating_layout,
vec![(1, None)],
vec![],
new_plugin_ids,
)),
true,
true,
);
tab.set_pane_collapsed(PaneId::Plugin(1), true);
assert_eq!(
tab.viewport.borrow().rows,
20,
"a collapsed bar should give its row to the viewport"
);
tab.next_swap_layout().unwrap();
// the swap is solved with the bar taking part, so the viewport computed during it still
// leaves the bar's row out; collapsing the bar again afterwards must hand the row back
let viewport = *tab.viewport.borrow();
assert_eq!(
(viewport.y, viewport.rows),
(0, 20),
"the viewport should still cover the collapsed bar's row after a swap layout"
);
}
+478
View File
@@ -22,6 +22,7 @@ use zellij_utils::input::layout::{SplitDirection, SplitSize, TiledPaneLayout};
use zellij_utils::input::options::PaneFrameStyle;
use zellij_utils::ipc::IpcReceiverWithContext;
use zellij_utils::pane_size::{PaneGeom, Size, SizeInPixels};
use zellij_utils::position::Position;
use crate::os_input_output::AsyncReader;
use std::cell::RefCell;
@@ -20630,3 +20631,480 @@ pub fn reasons_for_panes_that_were_not_created_do_not_accumulate() {
tab.clear_pane_not_created_reasons();
assert_eq!(tab.pane_not_created_reasons_count(), 0);
}
fn tab_with_a_pane_above_a_pane() -> Tab {
let size = Size {
cols: 121,
rows: 20,
};
let mut tab = create_new_tab(size, true);
tab.horizontal_split(PaneId::Terminal(2), None, 1, None, None)
.unwrap();
tab
}
fn geom_of(tab: &Tab, pane_id: PaneId) -> PaneGeom {
tab.tiled_panes
.panes
.get(&pane_id)
.unwrap()
.position_and_size()
}
#[test]
fn collapsing_a_pane_gives_its_rows_to_its_neighbor() {
let mut tab = tab_with_a_pane_above_a_pane();
let rows_before = geom_of(&tab, PaneId::Terminal(1)).rows.as_usize();
tab.set_pane_collapsed(PaneId::Terminal(2), true);
assert_eq!(
geom_of(&tab, PaneId::Terminal(1)).rows.as_usize(),
20,
"the pane that stayed should hold every row the tab has"
);
assert!(
rows_before < 20,
"the two panes should have been sharing the rows to begin with"
);
}
#[test]
fn expanding_a_pane_restores_the_geometry_it_had() {
let mut tab = tab_with_a_pane_above_a_pane();
let before = (
geom_of(&tab, PaneId::Terminal(1)),
geom_of(&tab, PaneId::Terminal(2)),
);
tab.set_pane_collapsed(PaneId::Terminal(2), true);
tab.set_pane_collapsed(PaneId::Terminal(2), false);
// exact equality rather than a row count: the point of collapsing instead of suppressing
// is that the layout comes back as it was, position and constraint included
assert_eq!(
(
geom_of(&tab, PaneId::Terminal(1)),
geom_of(&tab, PaneId::Terminal(2))
),
before
);
}
#[test]
fn a_collapsed_pane_keeps_the_geometry_it_will_come_back_to() {
let mut tab = tab_with_a_pane_above_a_pane();
let before = geom_of(&tab, PaneId::Terminal(2));
tab.set_pane_collapsed(PaneId::Terminal(2), true);
assert_eq!(
geom_of(&tab, PaneId::Terminal(2)),
before,
"a collapsed pane holds no space, but it still describes where it will come back, \
so collapsing on its own must not move it"
);
}
#[test]
fn collapsing_a_pane_that_is_already_collapsed_does_nothing() {
let mut tab = tab_with_a_pane_above_a_pane();
tab.set_pane_collapsed(PaneId::Terminal(2), true);
let after_first = geom_of(&tab, PaneId::Terminal(1));
tab.set_pane_collapsed(PaneId::Terminal(2), true);
assert_eq!(geom_of(&tab, PaneId::Terminal(1)), after_first);
}
#[test]
fn a_collapsed_pane_is_still_collapsed_after_a_fullscreen_comes_and_goes() {
let mut tab = tab_with_a_pane_above_a_pane();
tab.set_pane_collapsed(PaneId::Terminal(2), true);
tab.focus_pane_with_id(PaneId::Terminal(1), false, false, 1)
.unwrap();
// fullscreen rebuilds the hidden set from scratch, so a collapse that lived only there
// would be handed its rows back on the way out
tab.toggle_active_pane_fullscreen(1);
tab.toggle_active_pane_fullscreen(1);
assert!(tab.tiled_panes.pane_is_collapsed(&PaneId::Terminal(2)));
assert_eq!(
geom_of(&tab, PaneId::Terminal(1)).rows.as_usize(),
20,
"the pane that stayed should still hold every row"
);
}
#[test]
fn closing_a_collapsed_pane_forgets_that_it_was_collapsed() {
let mut tab = tab_with_a_pane_above_a_pane();
tab.set_pane_collapsed(PaneId::Terminal(2), true);
tab.close_pane(PaneId::Terminal(2), false, None);
assert!(
!tab.tiled_panes.pane_is_collapsed(&PaneId::Terminal(2)),
"a pane id Zellij hands out again must not arrive already invisible"
);
}
#[test]
fn collapsing_a_pane_that_is_not_in_the_tab_does_nothing() {
let mut tab = tab_with_a_pane_above_a_pane();
let before = (
geom_of(&tab, PaneId::Terminal(1)),
geom_of(&tab, PaneId::Terminal(2)),
);
tab.set_pane_collapsed(PaneId::Terminal(99), true);
assert!(!tab.tiled_panes.pane_is_collapsed(&PaneId::Terminal(99)));
assert_eq!(
(
geom_of(&tab, PaneId::Terminal(1)),
geom_of(&tab, PaneId::Terminal(2))
),
before
);
}
/// The shape a bar plugin actually has: a full-width pane with a one-row pane pinned
/// under it, the way `pane size=1 borderless` in a layout arrives.
fn tab_with_a_one_row_bar_under_a_pane(size: Size) -> Tab {
let mut layout = TiledPaneLayout::default();
layout.children_split_direction = SplitDirection::Horizontal;
let mut bar = TiledPaneLayout::default();
bar.split_size = Some(SplitSize::Fixed(1));
layout.children = vec![TiledPaneLayout::default(), bar];
create_new_tab_with_layout(size, layout)
}
#[test]
fn a_collapsed_pane_follows_the_tab_when_it_is_resized() {
let mut tab = tab_with_a_one_row_bar_under_a_pane(Size {
cols: 121,
rows: 20,
});
let main_pane = PaneId::Terminal(0);
let bar = PaneId::Terminal(1);
tab.set_pane_collapsed(bar, true);
// the tab changes shape, the way a nested session's does when its host expands it
// over the whole display and takes its own bars off the screen with it
tab.resize_whole_tab(Size {
cols: 118,
rows: 23,
})
.unwrap();
// being left out of the solve is what keeps a collapsed pane from holding space, but
// it must not leave the pane describing a tab that no longer exists: the solver reads
// the layout tree back off pane geometry, so a stale one breaks the next solve
assert_eq!(
geom_of(&tab, bar).y,
22,
"the collapsed bar should still be tracking the bottom of the tab"
);
assert_eq!(
geom_of(&tab, bar).cols.as_usize(),
118,
"the collapsed bar should still be tracking the width of the tab"
);
assert_eq!(
geom_of(&tab, main_pane).rows.as_usize(),
23,
"the pane that stayed should hold every row of the resized tab"
);
tab.set_pane_collapsed(bar, false);
assert_eq!(
geom_of(&tab, main_pane).rows.as_usize(),
22,
"the pane that stayed should give the row back"
);
assert_eq!(
geom_of(&tab, bar).y,
22,
"the bar should come back on the bottom row, not partway up the pane above it"
);
assert_eq!(geom_of(&tab, bar).rows.as_usize(), 1);
assert_eq!(geom_of(&tab, bar).cols.as_usize(), 118);
}
fn tiled_geoms_as_serialized(tab: &Tab) -> HashMap<PaneId, PaneGeom> {
let overrides = tab.tiled_pane_serialization_geoms();
tab.get_tiled_panes()
.map(|(pane_id, pane)| {
(
*pane_id,
overrides
.get(pane_id)
.copied()
.unwrap_or_else(|| pane.position_and_size()),
)
})
.collect()
}
fn all_tiled_geoms(tab: &Tab) -> HashMap<PaneId, PaneGeom> {
tab.get_tiled_panes()
.map(|(pane_id, pane)| (*pane_id, pane.position_and_size()))
.collect()
}
#[test]
fn a_collapsed_bar_is_serialized_as_if_it_were_expanded() {
let mut tab = tab_with_a_one_row_bar_under_a_pane(Size {
cols: 121,
rows: 20,
});
let before = all_tiled_geoms(&tab);
tab.set_pane_collapsed(PaneId::Terminal(1), true);
assert_ne!(all_tiled_geoms(&tab), before);
assert_eq!(tiled_geoms_as_serialized(&tab), before);
}
#[test]
fn a_collapsed_column_is_serialized_as_if_it_were_expanded() {
let mut tab = create_new_tab(
Size {
cols: 121,
rows: 20,
},
true,
);
tab.vertical_split(PaneId::Terminal(2), None, 1, None, None)
.unwrap();
let before = all_tiled_geoms(&tab);
tab.set_pane_collapsed(PaneId::Terminal(2), true);
assert_ne!(all_tiled_geoms(&tab), before);
assert_eq!(tiled_geoms_as_serialized(&tab), before);
}
#[test]
fn a_collapsed_pane_between_two_panes_is_serialized_as_if_it_were_expanded() {
let mut tab = tab_with_a_pane_above_a_pane();
tab.horizontal_split(PaneId::Terminal(3), None, 1, None, None)
.unwrap();
let before = all_tiled_geoms(&tab);
tab.set_pane_collapsed(PaneId::Terminal(2), true);
assert_ne!(all_tiled_geoms(&tab), before);
assert_eq!(tiled_geoms_as_serialized(&tab), before);
}
#[test]
fn a_collapsed_bar_under_a_stack_is_serialized_as_if_it_were_expanded() {
let mut layout = TiledPaneLayout::default();
layout.children_split_direction = SplitDirection::Horizontal;
let mut stack = TiledPaneLayout::default();
stack.children_are_stacked = true;
stack.children = vec![
TiledPaneLayout::default(),
TiledPaneLayout::default(),
TiledPaneLayout::default(),
];
let mut bar = TiledPaneLayout::default();
bar.split_size = Some(SplitSize::Fixed(1));
layout.children = vec![stack, bar];
let mut tab = create_new_tab_with_layout(
Size {
cols: 121,
rows: 20,
},
layout,
);
let bar = PaneId::Terminal(3);
assert!(geom_of(&tab, PaneId::Terminal(0)).is_stacked());
let before = all_tiled_geoms(&tab);
tab.set_pane_collapsed(bar, true);
assert_ne!(all_tiled_geoms(&tab), before);
assert_eq!(tiled_geoms_as_serialized(&tab), before);
}
#[test]
fn nothing_is_overridden_for_serialization_when_no_pane_is_collapsed() {
let tab = tab_with_a_pane_above_a_pane();
assert!(tab.tiled_pane_serialization_geoms().is_empty());
}
#[test]
fn collapsing_the_focused_pane_moves_focus_to_a_visible_pane() {
let mut tab = tab_with_a_pane_above_a_pane();
assert_eq!(tab.get_active_pane_id(1), Some(PaneId::Terminal(2)));
tab.set_pane_collapsed(PaneId::Terminal(2), true);
assert_eq!(tab.get_active_pane_id(1), Some(PaneId::Terminal(1)));
}
#[test]
fn a_collapsed_pane_cannot_be_focused() {
let mut tab = tab_with_a_pane_above_a_pane();
tab.set_pane_collapsed(PaneId::Terminal(2), true);
tab.focus_pane_with_id(PaneId::Terminal(2), false, false, 1)
.unwrap();
assert_eq!(tab.get_active_pane_id(1), Some(PaneId::Terminal(1)));
}
#[test]
fn a_collapsed_pane_cannot_be_focused_while_another_pane_is_fullscreen() {
let mut tab = tab_with_a_pane_above_a_pane();
tab.set_pane_collapsed(PaneId::Terminal(2), true);
tab.toggle_active_pane_fullscreen(1);
assert!(tab.tiled_panes.fullscreen_is_active());
tab.focus_pane_with_id(PaneId::Terminal(2), false, false, 1)
.unwrap();
assert_eq!(tab.get_active_pane_id(1), Some(PaneId::Terminal(1)));
assert!(
tab.tiled_panes.fullscreen_is_active(),
"refusing to focus a collapsed pane should leave fullscreen alone"
);
}
#[test]
fn collapsing_the_only_selectable_pane_keeps_its_focus() {
let mut tab = create_new_tab(
Size {
cols: 121,
rows: 20,
},
true,
);
tab.set_pane_collapsed(PaneId::Terminal(1), true);
assert_eq!(tab.get_active_pane_id(1), Some(PaneId::Terminal(1)));
}
#[test]
fn a_click_on_a_collapsed_panes_space_reaches_the_pane_that_grew_over_it() {
let mut tab = tab_with_a_pane_above_a_pane();
tab.set_pane_collapsed(PaneId::Terminal(1), true);
let point = Position::new(2, 5);
assert!(
geom_of(&tab, PaneId::Terminal(1)).contains(&point),
"the collapsed pane should still describe the space it will come back to"
);
assert_eq!(
tab.get_pane_id_at(&point, true).unwrap(),
Some(PaneId::Terminal(2))
);
assert_eq!(
tab.get_pane_id_at(&point, false).unwrap(),
Some(PaneId::Terminal(2))
);
}
#[test]
fn closing_a_collapsed_pane_leaves_its_neighbors_where_they_are() {
let mut tab = tab_with_a_pane_above_a_pane();
tab.horizontal_split(PaneId::Terminal(3), None, 1, None, None)
.unwrap();
tab.set_pane_collapsed(PaneId::Terminal(2), true);
let place_of = |tab: &Tab, pane_id: PaneId| {
let geom = geom_of(tab, pane_id);
(geom.x, geom.y, geom.cols.as_usize(), geom.rows.as_usize())
};
let top = place_of(&tab, PaneId::Terminal(1));
let bottom = place_of(&tab, PaneId::Terminal(3));
tab.close_pane(PaneId::Terminal(2), false, None);
assert_eq!(place_of(&tab, PaneId::Terminal(1)), top);
assert_eq!(place_of(&tab, PaneId::Terminal(3)), bottom);
assert_eq!(
geom_of(&tab, PaneId::Terminal(1)).rows.as_usize()
+ geom_of(&tab, PaneId::Terminal(3)).rows.as_usize(),
20
);
let percents = geom_of(&tab, PaneId::Terminal(1))
.rows
.as_percent()
.unwrap()
+ geom_of(&tab, PaneId::Terminal(3))
.rows
.as_percent()
.unwrap();
assert!(
(percents - 100.0).abs() < 0.001,
"the closed pane's share should go to the panes that took its space, got {}",
percents
);
}
#[test]
fn closing_a_collapsed_bar_leaves_the_pane_above_it_holding_the_whole_tab() {
let mut tab = tab_with_a_one_row_bar_under_a_pane(Size {
cols: 121,
rows: 20,
});
let main_pane = PaneId::Terminal(0);
let bar = PaneId::Terminal(1);
tab.set_pane_collapsed(bar, true);
let before = geom_of(&tab, main_pane);
tab.close_pane(bar, false, None);
assert_eq!(geom_of(&tab, main_pane), before);
assert_eq!(geom_of(&tab, main_pane).rows.as_usize(), 20);
}
#[test]
fn panes_keep_their_place_after_a_collapsed_pane_closes_and_the_tab_resizes() {
let mut tab = tab_with_a_pane_above_a_pane();
tab.horizontal_split(PaneId::Terminal(3), None, 1, None, None)
.unwrap();
tab.set_pane_collapsed(PaneId::Terminal(2), true);
tab.close_pane(PaneId::Terminal(2), false, None);
tab.resize_whole_tab(Size {
cols: 121,
rows: 40,
})
.unwrap();
let top = geom_of(&tab, PaneId::Terminal(1));
let bottom = geom_of(&tab, PaneId::Terminal(3));
assert_eq!(top.y, 0);
assert_eq!(bottom.y, top.rows.as_usize());
assert_eq!(top.rows.as_usize() + bottom.rows.as_usize(), 40);
}
#[test]
fn pane_info_reports_whether_a_pane_is_collapsed() {
let mut tab = tab_with_a_pane_above_a_pane();
let is_collapsed = |tab: &Tab, id: u32| {
tab.tiled_panes
.pane_info(&HashMap::new())
.into_iter()
.find(|info| info.id == id && !info.is_plugin)
.map(|info| info.is_collapsed)
.unwrap()
};
tab.set_pane_collapsed(PaneId::Terminal(2), true);
assert!(is_collapsed(&tab, 2));
assert!(!is_collapsed(&tab, 1));
tab.set_pane_collapsed(PaneId::Terminal(2), false);
assert!(!is_collapsed(&tab, 2));
}
+31
View File
@@ -16686,3 +16686,34 @@ fn a_plugin_prompt_for_a_user_who_is_not_attached_fails() {
});
assert!(failed);
}
#[test]
fn a_session_with_a_collapsed_pane_serializes_as_if_the_pane_were_expanded() {
let size = Size {
cols: 121,
rows: 20,
};
let mut screen = create_new_screen(size, true, true);
new_tab(&mut screen, 1, 0);
{
let tab = screen.get_active_tab_mut(1).unwrap();
tab.horizontal_split(PaneId::Terminal(2), None, 1, None, None)
.unwrap();
tab.focus_pane_with_id(PaneId::Terminal(1), false, false, 1)
.unwrap();
}
let serialize = |screen: &Screen| {
zellij_utils::session_serialization::serialize_session_layout(
screen.get_layout_metadata(None, None).into(),
)
};
let expanded = serialize(&screen).expect("the expanded session should serialize");
screen
.get_active_tab_mut(1)
.unwrap()
.set_pane_collapsed(PaneId::Terminal(2), true);
let collapsed = serialize(&screen).expect("a collapsed pane should not break serialization");
assert_eq!(collapsed, expanded);
}
+22
View File
@@ -119,6 +119,11 @@ pub fn close_slot(slot_id: SlotId) -> Result<(), String> {
run_slot_command(PluginCommand::CloseSlot(slot_id))
}
/// The slot form of `set_self_collapsed`, for a shared plugin that decides outside of `render`.
pub fn set_collapsed_slot(slot_id: SlotId, collapsed: bool) -> Result<(), String> {
run_slot_command(PluginCommand::SetCollapsedSlot(slot_id, collapsed))
}
pub fn request_permission(permissions: &[PermissionType]) {
let plugin_command = PluginCommand::RequestPluginPermissions(permissions.into());
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
@@ -2983,6 +2988,23 @@ pub fn set_self_mouse_selection_support(selection_support: bool) {
unsafe { host_run_plugin_command() };
}
/// Give this plugin's pane back to its neighbors while it has nothing to draw, or take it
/// again when it does.
///
/// Intended for a plugin mounted in a layout as a bar, whose content is sometimes empty: the
/// row a layout reserves for it stays reserved otherwise, and shows as a blank line. While
/// collapsed the pane keeps its place in the layout and simply takes no space, so expanding
/// restores the exact geometry the layout asked for. Calling it with the size the pane is
/// already at does nothing.
///
/// Has no effect on a floating pane, which takes no space from its neighbors to begin with.
pub fn set_self_collapsed(collapsed: bool) {
let plugin_command = PluginCommand::SetSelfCollapsed(collapsed);
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
pub fn generate_web_login_token(
token_label: Option<String>,
read_only: bool,
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
View File
@@ -786,6 +786,8 @@ pub struct PaneInfo {
pub default_bg: ::core::option::Option<::prost::alloc::string::String>,
#[prost(string, optional, tag="26")]
pub nested_session_name: ::core::option::Option<::prost::alloc::string::String>,
#[prost(bool, tag="27")]
pub is_collapsed: bool,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
@@ -4,7 +4,7 @@
pub struct PluginCommand {
#[prost(enumeration="CommandName", tag="1")]
pub name: i32,
#[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198")]
#[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200")]
pub payload: ::core::option::Option<plugin_command::Payload>,
}
/// Nested message and enum types in `PluginCommand`.
@@ -374,6 +374,10 @@ pub mod plugin_command {
WriteThemeFilePayload(super::WriteThemeFilePayload),
#[prost(string, tag="198")]
DeleteThemeFilePayload(::prost::alloc::string::String),
#[prost(message, tag="199")]
SetSelfCollapsedPayload(super::SetSelfCollapsedPayload),
#[prost(message, tag="200")]
SetCollapsedSlotPayload(super::SetCollapsedSlotPayload),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -712,6 +716,14 @@ pub struct SetSelectableSlotPayload {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SetCollapsedSlotPayload {
#[prost(uint32, tag="1")]
pub slot_id: u32,
#[prost(bool, tag="2")]
pub collapsed: bool,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ShowSlotPayload {
#[prost(uint32, tag="1")]
pub slot_id: u32,
@@ -732,6 +744,12 @@ pub struct GetNestedSessionKeybindsPayload {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SetSelfCollapsedPayload {
#[prost(bool, tag="1")]
pub collapsed: bool,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SetPaneFrameStylePayload {
#[prost(enumeration="super::pane_frame_style::PaneFrameStyle", tag="1")]
pub pane_frame_style: i32,
@@ -2672,6 +2690,8 @@ pub enum CommandName {
ReplyToPrompt = 253,
WriteThemeFile = 254,
DeleteThemeFile = 255,
SetSelfCollapsed = 256,
SetCollapsedSlot = 257,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -2910,6 +2930,8 @@ impl CommandName {
CommandName::ReplyToPrompt => "ReplyToPrompt",
CommandName::WriteThemeFile => "WriteThemeFile",
CommandName::DeleteThemeFile => "DeleteThemeFile",
CommandName::SetSelfCollapsed => "SetSelfCollapsed",
CommandName::SetCollapsedSlot => "SetCollapsedSlot",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -3145,6 +3167,8 @@ impl CommandName {
"ReplyToPrompt" => Some(Self::ReplyToPrompt),
"WriteThemeFile" => Some(Self::WriteThemeFile),
"DeleteThemeFile" => Some(Self::DeleteThemeFile),
"SetSelfCollapsed" => Some(Self::SetSelfCollapsed),
"SetCollapsedSlot" => Some(Self::SetCollapsedSlot),
_ => None,
}
}
+6
View File
@@ -2896,6 +2896,7 @@ pub struct PaneInfo {
/// Whether a pane is suppressed - suppressed panes are not visible to the user, but still run
/// in the background
pub is_suppressed: bool,
pub is_collapsed: bool,
/// The full title of the pane as it appears in the UI (if there is room for it)
pub title: String,
/// Whether a pane exited or not, note that most panes close themselves before setting this
@@ -4423,6 +4424,10 @@ pub enum PluginCommand {
EmbedMultiplePanes(Vec<PaneId>),
QueryWebServerStatus,
SetSelfMouseSelectionSupport(bool),
/// Give this plugin's own row or column in the layout back to its neighbors while it has
/// nothing to draw, and take it again when it does. The pane keeps its place in the layout
/// the whole time, so expanding restores exactly the size the layout asked for.
SetSelfCollapsed(bool),
GenerateWebLoginToken(Option<String>, bool), // (token_label, read_only)
RevokeWebLoginToken(String), // String -> token id (provided name or generated id)
ListWebLoginTokens,
@@ -4504,6 +4509,7 @@ pub enum PluginCommand {
HideSlot(SlotId),
ShowSlot(SlotId, bool),
CloseSlot(SlotId),
SetCollapsedSlot(SlotId, bool),
OpenContextMenu {
target: ContextMenuTarget,
line: usize,
+1
View File
@@ -457,6 +457,7 @@ pub enum ScreenContext {
ToggleGroupMarking,
SessionSharingStatusChange,
SetMouseSelectionSupport,
SetPaneCollapsed,
InterceptKeyPresses,
ClearKeyPressesIntercepts,
ReplacePaneWithExistingPane,
+3
View File
@@ -7516,6 +7516,7 @@ impl PaneInfo {
is_fullscreen,
is_floating,
is_suppressed,
is_collapsed: false,
title,
exited,
exit_status,
@@ -7667,6 +7668,7 @@ fn serialize_and_deserialize_session_info_with_data() {
is_fullscreen: true,
is_floating: false,
is_suppressed: false,
is_collapsed: false,
title: "pane 1".to_owned(),
exited: false,
exit_status: None,
@@ -7695,6 +7697,7 @@ fn serialize_and_deserialize_session_info_with_data() {
is_fullscreen: true,
is_floating: false,
is_suppressed: false,
is_collapsed: false,
title: "pane 1".to_owned(),
exited: false,
exit_status: None,
+1
View File
@@ -607,6 +607,7 @@ message PaneInfo {
optional string default_fg = 24;
optional string default_bg = 25;
optional string nested_session_name = 26;
bool is_collapsed = 27;
}
message IndexInPaneGroup {
+4
View File
@@ -2200,6 +2200,7 @@ impl TryFrom<ProtobufPaneInfo> for PaneInfo {
is_fullscreen: protobuf_pane_info.is_fullscreen,
is_floating: protobuf_pane_info.is_floating,
is_suppressed: protobuf_pane_info.is_suppressed,
is_collapsed: protobuf_pane_info.is_collapsed,
title: protobuf_pane_info.title,
exited: protobuf_pane_info.exited,
exit_status: protobuf_pane_info.exit_status,
@@ -2245,6 +2246,7 @@ impl TryFrom<PaneInfo> for ProtobufPaneInfo {
is_fullscreen: pane_info.is_fullscreen,
is_floating: pane_info.is_floating,
is_suppressed: pane_info.is_suppressed,
is_collapsed: pane_info.is_collapsed,
title: pane_info.title,
exited: pane_info.exited,
exit_status: pane_info.exit_status,
@@ -3337,6 +3339,7 @@ fn serialize_session_update_event_with_non_default_values() {
is_fullscreen: true,
is_floating: false,
is_suppressed: false,
is_collapsed: false,
title: "pane 1".to_owned(),
exited: false,
exit_status: None,
@@ -3365,6 +3368,7 @@ fn serialize_session_update_event_with_non_default_values() {
is_fullscreen: true,
is_floating: false,
is_suppressed: false,
is_collapsed: true,
title: "pane 1".to_owned(),
exited: false,
exit_status: None,
@@ -247,6 +247,8 @@ enum CommandName {
ReplyToPrompt = 253;
WriteThemeFile = 254;
DeleteThemeFile = 255;
SetSelfCollapsed = 256;
SetCollapsedSlot = 257;
}
message PluginCommand {
@@ -434,6 +436,8 @@ message PluginCommand {
ReplyToPromptPayload reply_to_prompt_payload = 196;
WriteThemeFilePayload write_theme_file_payload = 197;
string delete_theme_file_payload = 198;
SetSelfCollapsedPayload set_self_collapsed_payload = 199;
SetCollapsedSlotPayload set_collapsed_slot_payload = 200;
}
}
@@ -639,6 +643,11 @@ message SetSelectableSlotPayload {
bool selectable = 2;
}
message SetCollapsedSlotPayload {
uint32 slot_id = 1;
bool collapsed = 2;
}
message ShowSlotPayload {
uint32 slot_id = 1;
bool should_float_if_hidden = 2;
@@ -652,6 +661,10 @@ message GetNestedSessionKeybindsPayload {
PaneId pane_id = 1;
}
message SetSelfCollapsedPayload {
bool collapsed = 1;
}
message SetPaneFrameStylePayload {
pane_frame_style.PaneFrameStyle pane_frame_style = 1;
}
+29 -3
View File
@@ -133,10 +133,11 @@ pub use super::generated_api::api::{
SaveSessionResponse as ProtobufSaveSessionResponse, ScrollDownInPaneIdPayload,
ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload,
SessionListSnapshot as ProtobufSessionListSnapshot, SetCliPipeExitCodePayload,
SetFloatingPanePinnedPayload, SetPaneBorderStylePayload, SetPaneBorderlessPayload,
SetPaneColorPayload, SetPaneFrameStylePayload as ProtobufSetPaneFrameStylePayload,
SetCollapsedSlotPayload, SetFloatingPanePinnedPayload, SetPaneBorderStylePayload,
SetPaneBorderlessPayload, SetPaneColorPayload,
SetPaneFrameStylePayload as ProtobufSetPaneFrameStylePayload,
SetPaneRegexHighlightsPayload, SetPopupSizePayload, SetSelectableSlotPayload,
SetSelfMouseSelectionSupportPayload,
SetSelfCollapsedPayload, SetSelfMouseSelectionSupportPayload,
SetSoftKeyboardPayload as ProtobufSetSoftKeyboardPayload, SetTimeoutPayload,
ShowCursorPayload, ShowFloatingPanesPayload as ProtobufShowFloatingPanesPayload,
ShowFloatingPanesResponse as ProtobufShowFloatingPanesResponse, ShowPaneWithIdPayload,
@@ -2441,6 +2442,12 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
_ => Err("SetSelfMouseSelectionSupport requires a payload"),
}
},
Some(CommandName::SetSelfCollapsed) => match protobuf_plugin_command.payload {
Some(Payload::SetSelfCollapsedPayload(set_self_collapsed_payload)) => Ok(
PluginCommand::SetSelfCollapsed(set_self_collapsed_payload.collapsed),
),
_ => Err("SetSelfCollapsed requires a payload"),
},
Some(CommandName::GenerateWebLoginToken) => match protobuf_plugin_command.payload {
Some(Payload::GenerateWebLoginTokenPayload(generate_web_login_token_payload)) => {
Ok(PluginCommand::GenerateWebLoginToken(
@@ -2569,6 +2576,12 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
Some(Payload::CloseSlotPayload(slot_id)) => Ok(PluginCommand::CloseSlot(slot_id)),
_ => Err("Mismatched payload for CloseSlot"),
},
Some(CommandName::SetCollapsedSlot) => match protobuf_plugin_command.payload {
Some(Payload::SetCollapsedSlotPayload(payload)) => Ok(
PluginCommand::SetCollapsedSlot(payload.slot_id, payload.collapsed),
),
_ => Err("Mismatched payload for SetCollapsedSlot"),
},
Some(CommandName::OpenContextMenu) => match protobuf_plugin_command.payload {
Some(Payload::OpenContextMenuPayload(payload)) => {
let target = if payload.target_is_tab {
@@ -4431,6 +4444,12 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
)),
})
},
PluginCommand::SetSelfCollapsed(collapsed) => Ok(ProtobufPluginCommand {
name: CommandName::SetSelfCollapsed as i32,
payload: Some(Payload::SetSelfCollapsedPayload(SetSelfCollapsedPayload {
collapsed,
})),
}),
PluginCommand::GenerateWebLoginToken(token_label, read_only) => {
Ok(ProtobufPluginCommand {
name: CommandName::GenerateWebLoginToken as i32,
@@ -4569,6 +4588,13 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
name: CommandName::CloseSlot as i32,
payload: Some(Payload::CloseSlotPayload(slot_id)),
}),
PluginCommand::SetCollapsedSlot(slot_id, collapsed) => Ok(ProtobufPluginCommand {
name: CommandName::SetCollapsedSlot as i32,
payload: Some(Payload::SetCollapsedSlotPayload(SetCollapsedSlotPayload {
slot_id,
collapsed,
})),
}),
PluginCommand::OpenContextMenu {
target,
line,