fix(panes): various fixes for rendering stacked panes without pane frames (#4035)

* initial work

* fix rendering issues with stacked panes without pane frames

* make mouse clicking work

* test: rendering stacked panes without frames

* style(fmt): rustfmt

* docs(changelog): update pr
This commit is contained in:
Aram Drevekenin 2025-03-02 13:18:12 +01:00 committed by GitHub
parent 9f900a7325
commit b7cc3f3a62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 479 additions and 56 deletions

View file

@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* chore(repo): update some dependencies (https://github.com/zellij-org/zellij/pull/4019) * chore(repo): update some dependencies (https://github.com/zellij-org/zellij/pull/4019)
* fix(grid): reap sixel images on clear (https://github.com/zellij-org/zellij/pull/3982) * fix(grid): reap sixel images on clear (https://github.com/zellij-org/zellij/pull/3982)
* chore(repo): remove compile warnings (https://github.com/zellij-org/zellij/pull/4026) * chore(repo): remove compile warnings (https://github.com/zellij-org/zellij/pull/4026)
* fix(panes): properly render stacked panes when pane frames are disabled (https://github.com/zellij-org/zellij/pull/4035)
## [0.41.2] - 2024-11-19 ## [0.41.2] - 2024-11-19
* fix(input): keypresses not being identified properly with kitty keyboard protocol in some terminals (https://github.com/zellij-org/zellij/pull/3725) * fix(input): keypresses not being identified properly with kitty keyboard protocol in some terminals (https://github.com/zellij-org/zellij/pull/3725)

View file

@ -412,13 +412,7 @@ impl Pane for PluginPane {
self.pane_name.clone() self.pane_name.clone()
}; };
let mut frame_geom = self.current_geom(); let frame_geom = self.current_geom();
if !frame_params.should_draw_pane_frames {
// in this case the width of the frame needs not include the pane corners
frame_geom
.cols
.set_inner(frame_geom.cols.as_usize().saturating_sub(1));
}
let is_pinned = frame_geom.is_pinned; let is_pinned = frame_geom.is_pinned;
let mut frame = PaneFrame::new( let mut frame = PaneFrame::new(
frame_geom.into(), frame_geom.into(),
@ -613,6 +607,10 @@ impl Pane for PluginPane {
self.resize_grids(); self.resize_grids();
} }
fn get_content_offset(&self) -> Offset {
self.content_offset
}
fn store_pane_name(&mut self) { fn store_pane_name(&mut self) {
if self.pane_name != self.prev_pane_name { if self.pane_name != self.prev_pane_name {
self.prev_pane_name = self.pane_name.clone() self.prev_pane_name = self.pane_name.clone()

View file

@ -612,6 +612,10 @@ impl Pane for TerminalPane {
self.reflow_lines(); self.reflow_lines();
} }
fn get_content_offset(&self) -> Offset {
self.content_offset
}
fn store_pane_name(&mut self) { fn store_pane_name(&mut self) {
if self.pane_name != self.prev_pane_name { if self.pane_name != self.prev_pane_name {
self.prev_pane_name = self.pane_name.clone() self.prev_pane_name = self.pane_name.clone()

View file

@ -443,6 +443,11 @@ impl TiledPanes {
pub fn set_pane_frames(&mut self, draw_pane_frames: bool) { pub fn set_pane_frames(&mut self, draw_pane_frames: bool) {
self.draw_pane_frames = draw_pane_frames; self.draw_pane_frames = draw_pane_frames;
let viewport = *self.viewport.borrow(); let viewport = *self.viewport.borrow();
let position_and_sizes_of_stacks = {
StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide)
.positions_and_sizes_of_all_stacks()
.unwrap_or_else(|| Default::default())
};
for pane in self.panes.values_mut() { for pane in self.panes.values_mut() {
if !pane.borderless() { if !pane.borderless() {
pane.set_frame(draw_pane_frames); pane.set_frame(draw_pane_frames);
@ -464,12 +469,27 @@ impl TiledPanes {
// no draw_pane_frames and this pane should have a separation to other panes // no draw_pane_frames and this pane should have a separation to other panes
// according to its position in the viewport (eg. no separation if its at the // according to its position in the viewport (eg. no separation if its at the
// viewport bottom) - offset its content accordingly // viewport bottom) - offset its content accordingly
let position_and_size = pane.current_geom(); let mut position_and_size = pane.current_geom();
let is_stacked = position_and_size.is_stacked();
let is_flexible = !position_and_size.rows.is_fixed();
if let Some(position_and_size_of_stack) = position_and_size
.stacked
.and_then(|s_id| position_and_sizes_of_stacks.get(&s_id))
{
// we want to check the offset against the position_and_size of the whole
// stack rather than the pane, because the stack needs to have a consistent
// offset with itself
position_and_size = *position_and_size_of_stack;
};
let (pane_columns_offset, pane_rows_offset) = let (pane_columns_offset, pane_rows_offset) =
pane_content_offset(&position_and_size, &viewport); pane_content_offset(&position_and_size, &viewport);
if !draw_pane_frames && pane.current_geom().is_stacked() { if is_stacked && is_flexible {
// stacked panes should always leave 1 top row for a title // 1 to save room for the pane title
pane.set_content_offset(Offset::shift_right_and_top(pane_columns_offset, 1)); pane.set_content_offset(Offset::shift_right_top_and_bottom(
pane_columns_offset,
1,
pane_rows_offset,
));
} else { } else {
pane.set_content_offset(Offset::shift(pane_rows_offset, pane_columns_offset)); pane.set_content_offset(Offset::shift(pane_rows_offset, pane_columns_offset));
} }
@ -848,10 +868,14 @@ impl TiledPanes {
.collect() .collect()
}; };
let (stacked_pane_ids_under_flexible_pane, stacked_pane_ids_over_flexible_pane) = { let (stacked_pane_ids_under_flexible_pane, stacked_pane_ids_over_flexible_pane) = {
// TODO: do not recalculate this every time on render
StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide) StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide)
.stacked_pane_ids_under_and_over_flexible_panes() .stacked_pane_ids_under_and_over_flexible_panes()
.unwrap() // TODO: no unwrap .with_context(err_context)?
};
let (stacked_pane_ids_on_top_of_stacks, stacked_pane_ids_on_bottom_of_stacks) = {
StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide)
.stacked_pane_ids_on_top_and_bottom_of_stacks()
.with_context(err_context)?
}; };
for (kind, pane) in self.panes.iter_mut() { for (kind, pane) in self.panes.iter_mut() {
if !self.panes_to_hide.contains(&pane.pid()) { if !self.panes_to_hide.contains(&pane.pid()) {
@ -859,8 +883,14 @@ impl TiledPanes {
stacked_pane_ids_under_flexible_pane.contains(&pane.pid()); stacked_pane_ids_under_flexible_pane.contains(&pane.pid());
let pane_is_stacked_over = let pane_is_stacked_over =
stacked_pane_ids_over_flexible_pane.contains(&pane.pid()); stacked_pane_ids_over_flexible_pane.contains(&pane.pid());
let pane_is_on_top_of_stack =
stacked_pane_ids_on_top_of_stacks.contains(&pane.pid());
let pane_is_on_bottom_of_stack =
stacked_pane_ids_on_bottom_of_stacks.contains(&pane.pid());
let should_draw_pane_frames = self.draw_pane_frames; let should_draw_pane_frames = self.draw_pane_frames;
let pane_is_stacked = pane.current_geom().is_stacked(); let pane_is_stacked = pane.current_geom().is_stacked();
let pane_is_one_liner_in_stack =
pane_is_stacked && pane.current_geom().rows.is_fixed();
let mut pane_contents_and_ui = PaneContentsAndUi::new( let mut pane_contents_and_ui = PaneContentsAndUi::new(
pane, pane,
output, output,
@ -882,9 +912,11 @@ impl TiledPanes {
let err_context = let err_context =
|| format!("failed to render tiled panes for client {client_id}"); || format!("failed to render tiled panes for client {client_id}");
if let PaneId::Plugin(..) = kind { if let PaneId::Plugin(..) = kind {
pane_contents_and_ui if !pane_is_one_liner_in_stack {
.render_pane_contents_for_client(*client_id) pane_contents_and_ui
.with_context(err_context)?; .render_pane_contents_for_client(*client_id)
.with_context(err_context)?;
}
} }
let is_floating = false; let is_floating = false;
if self.draw_pane_frames { if self.draw_pane_frames {
@ -916,6 +948,8 @@ impl TiledPanes {
client_mode, client_mode,
boundaries, boundaries,
self.session_is_mirrored, self.session_is_mirrored,
pane_is_on_top_of_stack,
pane_is_on_bottom_of_stack,
); );
} else { } else {
let boundaries = client_id_to_boundaries let boundaries = client_id_to_boundaries
@ -926,6 +960,8 @@ impl TiledPanes {
client_mode, client_mode,
boundaries, boundaries,
self.session_is_mirrored, self.session_is_mirrored,
pane_is_on_top_of_stack,
pane_is_on_bottom_of_stack,
); );
} }
pane_contents_and_ui.render_terminal_title_if_needed( pane_contents_and_ui.render_terminal_title_if_needed(
@ -940,9 +976,13 @@ impl TiledPanes {
.with_context(err_context)?; .with_context(err_context)?;
} }
if let PaneId::Terminal(..) = kind { if let PaneId::Terminal(..) = kind {
pane_contents_and_ui if !pane_is_one_liner_in_stack {
.render_pane_contents_to_multiple_clients(connected_clients.iter().copied()) pane_contents_and_ui
.with_context(err_context)?; .render_pane_contents_to_multiple_clients(
connected_clients.iter().copied(),
)
.with_context(err_context)?;
}
} }
} }
} }
@ -2426,6 +2466,12 @@ impl TiledPanes {
fn is_connected(&self, client_id: &ClientId) -> bool { fn is_connected(&self, client_id: &ClientId) -> bool {
self.connected_clients.borrow().contains(&client_id) self.connected_clients.borrow().contains(&client_id)
} }
pub fn stacked_pane_ids_under_and_over_flexible_panes(
&mut self,
) -> Result<(HashSet<PaneId>, HashSet<PaneId>)> {
StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide)
.stacked_pane_ids_under_and_over_flexible_panes()
}
} }
#[allow(clippy::borrowed_box)] #[allow(clippy::borrowed_box)]

View file

@ -416,6 +416,25 @@ impl<'a> StackedPanes<'a> {
stacked_pane_ids_over_flexible_panes, stacked_pane_ids_over_flexible_panes,
)) ))
} }
pub fn stacked_pane_ids_on_top_and_bottom_of_stacks(
&self,
) -> Result<(HashSet<PaneId>, HashSet<PaneId>)> {
let mut stacked_pane_ids_on_top_of_stacks = HashSet::new();
let mut stacked_pane_ids_on_bottom_of_stacks = HashSet::new();
let all_stacks = self.get_all_stacks()?;
for stack in all_stacks {
if let Some((first_pane_id, _pane)) = stack.iter().next() {
stacked_pane_ids_on_top_of_stacks.insert(*first_pane_id);
}
if let Some((last_pane_id, _pane)) = stack.iter().last() {
stacked_pane_ids_on_bottom_of_stacks.insert(*last_pane_id);
}
}
Ok((
stacked_pane_ids_on_top_of_stacks,
stacked_pane_ids_on_bottom_of_stacks,
))
}
pub fn make_room_for_new_pane(&mut self) -> Result<PaneGeom> { pub fn make_room_for_new_pane(&mut self) -> Result<PaneGeom> {
let err_context = || format!("Failed to add pane to stack"); let err_context = || format!("Failed to add pane to stack");
let all_stacks = self.get_all_stacks()?; let all_stacks = self.get_all_stacks()?;
@ -804,6 +823,19 @@ impl<'a> StackedPanes<'a> {
} }
highest_stack_id highest_stack_id
} }
pub fn positions_and_sizes_of_all_stacks(&self) -> Option<HashMap<usize, PaneGeom>> {
let panes = self.panes.borrow();
let mut positions_and_sizes_of_all_stacks = HashMap::new();
for pane in panes.values() {
if let Some(stack_id) = pane.current_geom().stacked {
if !positions_and_sizes_of_all_stacks.contains_key(&stack_id) {
positions_and_sizes_of_all_stacks
.insert(stack_id, self.position_and_size_of_stack(&pane.pid())?);
}
}
}
Some(positions_and_sizes_of_all_stacks)
}
fn reset_stack_size( fn reset_stack_size(
&self, &self,
new_position_and_size_of_stack: &PaneGeom, new_position_and_size_of_stack: &PaneGeom,

View file

@ -3859,23 +3859,31 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::MouseEvent(event, client_id) => { ScreenInstruction::MouseEvent(event, client_id) => {
let mouse_effect = screen match screen
.get_active_tab_mut(client_id) .get_active_tab_mut(client_id)
.and_then(|tab| tab.handle_mouse_event(&event, client_id))?; .and_then(|tab| tab.handle_mouse_event(&event, client_id))
if mouse_effect.state_changed { {
screen.log_and_report_session_state()?; Ok(mouse_effect) => {
if mouse_effect.state_changed {
screen.log_and_report_session_state()?;
}
if !mouse_effect.leave_clipboard_message {
let _ =
screen
.bus
.senders
.send_to_plugin(PluginInstruction::Update(vec![(
None,
Some(client_id),
Event::InputReceived,
)]));
}
screen.render(None).non_fatal();
},
Err(e) => {
log::error!("Failed to process MouseEvent: {}", e);
},
} }
if !mouse_effect.leave_clipboard_message {
let _ = screen
.bus
.senders
.send_to_plugin(PluginInstruction::Update(vec![(
None,
Some(client_id),
Event::InputReceived,
)]));
}
screen.render(None)?;
}, },
ScreenInstruction::Copy(client_id) => { ScreenInstruction::Copy(client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab

View file

@ -308,6 +308,9 @@ pub trait Pane {
fn set_active_at(&mut self, instant: Instant); fn set_active_at(&mut self, instant: Instant);
fn set_frame(&mut self, frame: bool); fn set_frame(&mut self, frame: bool);
fn set_content_offset(&mut self, offset: Offset); fn set_content_offset(&mut self, offset: Offset);
fn get_content_offset(&self) -> Offset {
Offset::default()
}
fn cursor_shape_csi(&self) -> String { fn cursor_shape_csi(&self) -> String {
"\u{1b}[0 q".to_string() // default to non blinking block "\u{1b}[0 q".to_string() // default to non blinking block
} }
@ -328,9 +331,15 @@ pub trait Pane {
fn right_boundary_x_coords(&self) -> usize { fn right_boundary_x_coords(&self) -> usize {
self.x() + self.cols() self.x() + self.cols()
} }
fn right_boundary_x_content_coords(&self) -> usize {
self.get_content_x() + self.get_content_columns()
}
fn bottom_boundary_y_coords(&self) -> usize { fn bottom_boundary_y_coords(&self) -> usize {
self.y() + self.rows() self.y() + self.rows()
} }
fn bottom_boundary_y_content_coords(&self) -> usize {
self.get_content_y() + self.get_content_rows()
}
fn is_right_of(&self, other: &dyn Pane) -> bool { fn is_right_of(&self, other: &dyn Pane) -> bool {
self.x() > other.x() self.x() > other.x()
} }
@ -3350,7 +3359,11 @@ impl Tab {
} }
} }
fn get_pane_id_at(&self, point: &Position, search_selectable: bool) -> Result<Option<PaneId>> { fn get_pane_id_at(
&mut self,
point: &Position,
search_selectable: bool,
) -> Result<Option<PaneId>> {
let err_context = || format!("failed to get id of pane at position {point:?}"); let err_context = || format!("failed to get id of pane at position {point:?}");
if self.tiled_panes.fullscreen_is_active() if self.tiled_panes.fullscreen_is_active()
@ -3368,15 +3381,50 @@ impl Tab {
.with_context(err_context)?; .with_context(err_context)?;
return Ok(self.tiled_panes.get_active_pane_id(first_client_id)); return Ok(self.tiled_panes.get_active_pane_id(first_client_id));
} }
let (stacked_pane_ids_under_flexible_pane, _stacked_pane_ids_over_flexible_pane) = {
self.tiled_panes
.stacked_pane_ids_under_and_over_flexible_panes()
.with_context(err_context)?
};
let pane_contains_point = |p: &Box<dyn Pane>,
point: &Position,
stacked_pane_ids_under_flexible_pane: &HashSet<PaneId>|
-> bool {
let is_flexible_in_stack =
p.current_geom().is_stacked() && !p.current_geom().rows.is_fixed();
let is_stacked_under = stacked_pane_ids_under_flexible_pane.contains(&p.pid());
let geom_to_compare_against = if is_stacked_under && !self.draw_pane_frames {
// these sort of panes are one-liner panes under a flexible pane in a stack when we
// don't draw pane frames - because the whole stack's content is offset to allow
// room for the boundary between panes, they are actually drawn 1 line above where
// they are
let mut geom = p.current_geom();
geom.y = geom.y.saturating_sub(p.get_content_offset().bottom);
geom
} else if is_flexible_in_stack && !self.draw_pane_frames {
// these sorts of panes are flexible panes inside a stack when we don't draw pane
// frames - because the whole stack's content is offset to give room for the
// boundary between panes, we need to take this offset into account when figuring
// out whether the position is inside them
let mut geom = p.current_geom();
geom.rows.decrease_inner(p.get_content_offset().bottom);
geom
} else {
p.current_geom()
};
geom_to_compare_against.contains(point)
};
if search_selectable { if search_selectable {
Ok(self Ok(self
.get_selectable_tiled_panes() .get_selectable_tiled_panes()
.find(|(_, p)| p.contains(point)) .find(|(_, p)| pane_contains_point(p, point, &stacked_pane_ids_under_flexible_pane))
.map(|(&id, _)| id)) .map(|(&id, _)| id))
} else { } else {
Ok(self Ok(self
.get_tiled_panes() .get_tiled_panes()
.find(|(_, p)| p.contains(point)) .find(|(_, p)| pane_contains_point(p, point, &stacked_pane_ids_under_flexible_pane))
.map(|(&id, _)| id)) .map(|(&id, _)| id))
} }
} }

View file

@ -0,0 +1,46 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
assertion_line: 1134
expression: snapshot
---
00 (C): ─ Pane #1 ────────────────────────────────────────────┼─ Pane #7 ───────────────────────────────────
01 (C): ─ Pane #9 ────────────────────────────────────────────┤
02 (C): ─ Pane #10 ───────────────────────────────────────────┤
03 (C): │
04 (C): │
05 (C): │
06 (C): │
07 (C): │
08 (C): │
09 (C): │
10 (C): │
11 (C): │
12 (C): │
13 (C): │
14 (C): │
15 (C): │
16 (C): │
17 (C): │
18 (C): ├─ Pane #8 ───────────────────────────────────
19 (C): ─────────────────────────────────────────────────┬────┴─────────────────────────────────────────────
20 (C): ─ Pane #2 ───────────────────────────────────────┼─ Pane #4 ────────────────────────────────────────
21 (C): ─ Pane #3 ───────────────────────────────────────┼─ Pane #5 ────────────────────────────────────────
22 (C): │
23 (C): │
24 (C): │
25 (C): │
26 (C): │
27 (C): │
28 (C): │
29 (C): │
30 (C): │
31 (C): │
32 (C): │
33 (C): │
34 (C): │
35 (C): │
36 (C): │
37 (C): │
38 (C): │
39 (C): ├─ Pane #6 ────────────────────────────────────────

View file

@ -201,7 +201,6 @@ impl MockPtyInstructionBus {
} }
} }
// TODO: move to shared thingy with other test file
fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab { fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab {
set_session_name("test".into()); set_session_name("test".into());
let index = 0; let index = 0;
@ -270,6 +269,74 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab {
tab tab
} }
fn create_new_tab_without_pane_frames(size: Size, default_mode: ModeInfo) -> Tab {
set_session_name("test".into());
let index = 0;
let position = 0;
let name = String::new();
let os_api = Box::new(FakeInputOutput::default());
let senders = ThreadSenders::default().silently_fail_on_send();
let max_panes = None;
let mode_info = default_mode;
let style = Style::default();
let draw_pane_frames = false;
let auto_layout = true;
let client_id = 1;
let session_is_mirrored = true;
let mut connected_clients = HashSet::new();
connected_clients.insert(client_id);
let connected_clients = Rc::new(RefCell::new(connected_clients));
let character_cell_info = Rc::new(RefCell::new(None));
let stacked_resize = Rc::new(RefCell::new(true));
let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default()));
let copy_options = CopyOptions::default();
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let debug = false;
let arrow_fonts = true;
let styled_underlines = true;
let explicitly_disable_kitty_keyboard_protocol = false;
let mut tab = Tab::new(
index,
position,
name,
size,
character_cell_info,
stacked_resize,
sixel_image_store,
os_api,
senders,
max_panes,
style,
mode_info,
draw_pane_frames,
auto_layout,
connected_clients,
session_is_mirrored,
Some(client_id),
copy_options,
terminal_emulator_colors,
terminal_emulator_color_codes,
(vec![], vec![]),
PathBuf::from("my_default_shell"),
debug,
arrow_fonts,
styled_underlines,
explicitly_disable_kitty_keyboard_protocol,
None,
);
tab.apply_layout(
TiledPaneLayout::default(),
vec![],
vec![(1, None)],
vec![],
HashMap::new(),
client_id,
)
.unwrap();
tab
}
fn create_new_tab_with_swap_layouts( fn create_new_tab_with_swap_layouts(
size: Size, size: Size,
default_mode: ModeInfo, default_mode: ModeInfo,
@ -980,6 +1047,96 @@ fn split_stack_horizontally() {
assert_snapshot!(snapshot); assert_snapshot!(snapshot);
} }
#[test]
fn render_stacks_without_pane_frames() {
// this checks various cases and gotchas that have to do with rendering stacked panes when we
// don't draw frames around panes
let size = Size {
cols: 100,
rows: 40,
};
let client_id = 1;
let mut tab = create_new_tab_without_pane_frames(size, ModeInfo::default());
let mut output = Output::default();
for i in 2..4 {
let new_pane_id_1 = PaneId::Terminal(i);
tab.new_pane(
new_pane_id_1,
None,
None,
None,
None,
false,
Some(client_id),
)
.unwrap();
}
// the below resizes will end up stacking the panes
tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None))
.unwrap();
tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None))
.unwrap();
tab.vertical_split(PaneId::Terminal(4), None, client_id)
.unwrap();
for i in 5..7 {
let new_pane_id_1 = PaneId::Terminal(i);
tab.new_pane(
new_pane_id_1,
None,
None,
None,
None,
false,
Some(client_id),
)
.unwrap();
}
tab.focus_pane_with_id(PaneId::Terminal(1), false, client_id);
for i in 7..9 {
let new_pane_id_1 = PaneId::Terminal(i);
tab.new_pane(
new_pane_id_1,
None,
None,
None,
None,
false,
Some(client_id),
)
.unwrap();
}
let _ = tab.focus_pane_with_id(PaneId::Terminal(1), false, client_id);
for i in 9..11 {
let new_pane_id_1 = PaneId::Terminal(i);
tab.new_pane(
new_pane_id_1,
None,
None,
None,
None,
false,
Some(client_id),
)
.unwrap();
}
tab.resize(
client_id,
ResizeStrategy::new(Resize::Increase, Some(Direction::Right)),
)
.unwrap();
let _ = tab.focus_pane_with_id(PaneId::Terminal(7), false, client_id);
let _ = tab.focus_pane_with_id(PaneId::Terminal(5), false, client_id);
tab.render(&mut output).unwrap();
let snapshot = take_snapshot(
output.serialize().unwrap().get(&client_id).unwrap(),
size.rows,
size.cols,
Palette::default(),
);
assert_snapshot!(snapshot);
}
#[test] #[test]
fn dump_screen() { fn dump_screen() {
let size = Size { let size = Size {

View file

@ -1,4 +1,4 @@
use zellij_utils::pane_size::Viewport; use zellij_utils::pane_size::{Offset, Viewport};
use crate::output::CharacterChunk; use crate::output::CharacterChunk;
use crate::panes::terminal_character::{TerminalCharacter, EMPTY_TERMINAL_CHARACTER, RESET_STYLES}; use crate::panes::terminal_character::{TerminalCharacter, EMPTY_TERMINAL_CHARACTER, RESET_STYLES};
@ -444,25 +444,40 @@ impl Boundaries {
boundary_characters: HashMap::new(), boundary_characters: HashMap::new(),
} }
} }
pub fn add_rect(&mut self, rect: &dyn Pane, color: Option<PaletteColor>) { pub fn add_rect(
&mut self,
rect: &dyn Pane,
color: Option<PaletteColor>,
pane_is_on_top_of_stack: bool,
pane_is_on_bottom_of_stack: bool,
pane_is_stacked_under: bool,
) {
let pane_is_stacked = rect.current_geom().is_stacked(); let pane_is_stacked = rect.current_geom().is_stacked();
let should_skip_top_boundary = pane_is_stacked && !pane_is_on_top_of_stack;
let should_skip_bottom_boundary = pane_is_stacked && !pane_is_on_bottom_of_stack;
let content_offset = rect.get_content_offset();
if !self.is_fully_inside_screen(rect) { if !self.is_fully_inside_screen(rect) {
return; return;
} }
if rect.x() > self.viewport.x { if rect.x() > self.viewport.x {
// left boundary // left boundary
let boundary_x_coords = rect.x() - 1; let boundary_x_coords = rect.x() - 1;
let first_row_coordinates = self.rect_right_boundary_row_start(rect); let first_row_coordinates =
self.rect_right_boundary_row_start(rect, pane_is_stacked_under, content_offset);
let last_row_coordinates = self.rect_right_boundary_row_end(rect); let last_row_coordinates = self.rect_right_boundary_row_end(rect);
for row in first_row_coordinates..last_row_coordinates { for row in first_row_coordinates..last_row_coordinates {
let coordinates = Coordinates::new(boundary_x_coords, row); let coordinates = Coordinates::new(boundary_x_coords, row);
let symbol_to_add = if row == first_row_coordinates && row != self.viewport.y { let symbol_to_add = if row == first_row_coordinates && row != self.viewport.y {
BoundarySymbol::new(boundary_type::TOP_LEFT).color(color) if pane_is_stacked {
BoundarySymbol::new(boundary_type::VERTICAL_RIGHT).color(color)
} else {
BoundarySymbol::new(boundary_type::TOP_LEFT).color(color)
}
} else if row == first_row_coordinates && pane_is_stacked { } else if row == first_row_coordinates && pane_is_stacked {
BoundarySymbol::new(boundary_type::TOP_LEFT).color(color) BoundarySymbol::new(boundary_type::TOP_LEFT).color(color)
} else if row == last_row_coordinates - 1 } else if row == last_row_coordinates - 1
&& row != self.viewport.y + self.viewport.rows - 1 && row != self.viewport.y + self.viewport.rows - 1
&& !pane_is_stacked && content_offset.bottom > 0
{ {
BoundarySymbol::new(boundary_type::BOTTOM_LEFT).color(color) BoundarySymbol::new(boundary_type::BOTTOM_LEFT).color(color)
} else { } else {
@ -476,7 +491,7 @@ impl Boundaries {
self.boundary_characters.insert(coordinates, next_symbol); self.boundary_characters.insert(coordinates, next_symbol);
} }
} }
if rect.y() > self.viewport.y && !pane_is_stacked { if rect.y() > self.viewport.y && !should_skip_top_boundary {
// top boundary // top boundary
let boundary_y_coords = rect.y() - 1; let boundary_y_coords = rect.y() - 1;
let first_col_coordinates = self.rect_bottom_boundary_col_start(rect); let first_col_coordinates = self.rect_bottom_boundary_col_start(rect);
@ -501,15 +516,22 @@ impl Boundaries {
if self.rect_right_boundary_is_before_screen_edge(rect) { if self.rect_right_boundary_is_before_screen_edge(rect) {
// right boundary // right boundary
let boundary_x_coords = rect.right_boundary_x_coords() - 1; let boundary_x_coords = rect.right_boundary_x_coords() - 1;
let first_row_coordinates = self.rect_right_boundary_row_start(rect); let first_row_coordinates =
self.rect_right_boundary_row_start(rect, pane_is_stacked_under, content_offset);
let last_row_coordinates = self.rect_right_boundary_row_end(rect); let last_row_coordinates = self.rect_right_boundary_row_end(rect);
for row in first_row_coordinates..last_row_coordinates { for row in first_row_coordinates..last_row_coordinates {
let coordinates = Coordinates::new(boundary_x_coords, row); let coordinates = Coordinates::new(boundary_x_coords, row);
let symbol_to_add = if row == first_row_coordinates && row != self.viewport.y { let symbol_to_add = if row == first_row_coordinates && pane_is_stacked {
BoundarySymbol::new(boundary_type::TOP_RIGHT).color(color) BoundarySymbol::new(boundary_type::VERTICAL_LEFT).color(color)
} else if row == first_row_coordinates && row != self.viewport.y {
if pane_is_stacked {
BoundarySymbol::new(boundary_type::VERTICAL_LEFT).color(color)
} else {
BoundarySymbol::new(boundary_type::TOP_RIGHT).color(color)
}
} else if row == last_row_coordinates - 1 } else if row == last_row_coordinates - 1
&& row != self.viewport.y + self.viewport.rows - 1 && row != self.viewport.y + self.viewport.rows - 1
&& !pane_is_stacked && content_offset.bottom > 0
{ {
BoundarySymbol::new(boundary_type::BOTTOM_RIGHT).color(color) BoundarySymbol::new(boundary_type::BOTTOM_RIGHT).color(color)
} else { } else {
@ -523,7 +545,7 @@ impl Boundaries {
self.boundary_characters.insert(coordinates, next_symbol); self.boundary_characters.insert(coordinates, next_symbol);
} }
} }
if self.rect_bottom_boundary_is_before_screen_edge(rect) && !pane_is_stacked { if self.rect_bottom_boundary_is_before_screen_edge(rect) && !should_skip_bottom_boundary {
// bottom boundary // bottom boundary
let boundary_y_coords = rect.bottom_boundary_y_coords() - 1; let boundary_y_coords = rect.bottom_boundary_y_coords() - 1;
let first_col_coordinates = self.rect_bottom_boundary_col_start(rect); let first_col_coordinates = self.rect_bottom_boundary_col_start(rect);
@ -575,11 +597,28 @@ impl Boundaries {
fn rect_bottom_boundary_is_before_screen_edge(&self, rect: &dyn Pane) -> bool { fn rect_bottom_boundary_is_before_screen_edge(&self, rect: &dyn Pane) -> bool {
rect.y() + rect.rows() < self.viewport.y + self.viewport.rows rect.y() + rect.rows() < self.viewport.y + self.viewport.rows
} }
fn rect_right_boundary_row_start(&self, rect: &dyn Pane) -> usize { fn rect_right_boundary_row_start(
&self,
rect: &dyn Pane,
pane_is_stacked_under: bool,
content_offset: Offset,
) -> usize {
let pane_is_stacked = rect.current_geom().is_stacked(); let pane_is_stacked = rect.current_geom().is_stacked();
let horizontal_frame_offset = if pane_is_stacked { 0 } else { 1 }; let horizontal_frame_offset = if pane_is_stacked_under {
// these panes - panes that are in a stack below the flexible pane - need to have their
// content offset taken into account when rendering them (i.e. they are rendered
// one line above their actual y coordinates, since they are only 1 line)
// as opposed to panes that are in a stack above the flexible pane who do not because
// they are rendered in place (the content offset of the stack is "absorbed" by the
// flexible pane below them)
content_offset.bottom
} else if pane_is_stacked {
0
} else {
1
};
if rect.y() > self.viewport.y { if rect.y() > self.viewport.y {
rect.y() - horizontal_frame_offset rect.y().saturating_sub(horizontal_frame_offset)
} else { } else {
self.viewport.y self.viewport.y
} }

View file

@ -4,7 +4,7 @@ use crate::ui::boundaries::boundary_type;
use crate::ClientId; use crate::ClientId;
use zellij_utils::data::{client_id_to_colors, PaletteColor, Style}; use zellij_utils::data::{client_id_to_colors, PaletteColor, Style};
use zellij_utils::errors::prelude::*; use zellij_utils::errors::prelude::*;
use zellij_utils::pane_size::Viewport; use zellij_utils::pane_size::{Offset, Viewport};
use zellij_utils::position::Position; use zellij_utils::position::Position;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
@ -62,6 +62,7 @@ pub struct FrameParams {
pub pane_is_stacked_over: bool, pub pane_is_stacked_over: bool,
pub should_draw_pane_frames: bool, pub should_draw_pane_frames: bool,
pub pane_is_floating: bool, pub pane_is_floating: bool,
pub content_offset: Offset,
} }
#[derive(Default, PartialEq)] #[derive(Default, PartialEq)]
@ -82,6 +83,7 @@ pub struct PaneFrame {
should_draw_pane_frames: bool, should_draw_pane_frames: bool,
is_pinned: bool, is_pinned: bool,
is_floating: bool, is_floating: bool,
content_offset: Offset,
} }
impl PaneFrame { impl PaneFrame {
@ -108,6 +110,7 @@ impl PaneFrame {
should_draw_pane_frames: frame_params.should_draw_pane_frames, should_draw_pane_frames: frame_params.should_draw_pane_frames,
is_pinned: false, is_pinned: false,
is_floating: frame_params.pane_is_floating, is_floating: frame_params.pane_is_floating,
content_offset: frame_params.content_offset,
} }
} }
pub fn is_pinned(mut self, is_pinned: bool) -> Self { pub fn is_pinned(mut self, is_pinned: bool) -> Self {
@ -776,11 +779,26 @@ impl PaneFrame {
// if this is a stacked pane with pane frames off (and it doesn't necessarily have only // if this is a stacked pane with pane frames off (and it doesn't necessarily have only
// 1 row because it could also be a flexible stacked pane) // 1 row because it could also be a flexible stacked pane)
// in this case we should always draw the pane title line, and only the title line // in this case we should always draw the pane title line, and only the title line
let one_line_title = self.render_one_line_title().with_context(err_context)?; let mut one_line_title = self.render_one_line_title().with_context(err_context)?;
if self.content_offset.right != 0 && !self.should_draw_pane_frames {
// here what happens is that the title should be offset to the right
// in order to give room to the boundaries between the panes to be drawn
one_line_title.pop();
}
let y_coords_of_title = if self.pane_is_stacked_under && !self.should_draw_pane_frames {
// we only want to use the bottom offset in this case because panes that are
// stacked above the flexible pane should actually appear exactly where they are on
// screen, the content offset being "absorbed" by the flexible pane below them
self.geom.y.saturating_sub(self.content_offset.bottom)
} else {
self.geom.y
};
character_chunks.push(CharacterChunk::new( character_chunks.push(CharacterChunk::new(
one_line_title, one_line_title,
self.geom.x, self.geom.x,
self.geom.y, y_coords_of_title,
)); ));
} else { } else {
for row in 0..self.geom.rows { for row in 0..self.geom.rows {

View file

@ -223,6 +223,7 @@ impl<'a> PaneContentsAndUi<'a> {
pane_is_stacked_under: self.pane_is_stacked_under, pane_is_stacked_under: self.pane_is_stacked_under,
should_draw_pane_frames: self.should_draw_pane_frames, should_draw_pane_frames: self.should_draw_pane_frames,
pane_is_floating, pane_is_floating,
content_offset: self.pane.get_content_offset(),
} }
} else { } else {
FrameParams { FrameParams {
@ -236,6 +237,7 @@ impl<'a> PaneContentsAndUi<'a> {
pane_is_stacked_under: self.pane_is_stacked_under, pane_is_stacked_under: self.pane_is_stacked_under,
should_draw_pane_frames: self.should_draw_pane_frames, should_draw_pane_frames: self.should_draw_pane_frames,
pane_is_floating, pane_is_floating,
content_offset: self.pane.get_content_offset(),
} }
}; };
@ -260,9 +262,17 @@ impl<'a> PaneContentsAndUi<'a> {
client_mode: InputMode, client_mode: InputMode,
boundaries: &mut Boundaries, boundaries: &mut Boundaries,
session_is_mirrored: bool, session_is_mirrored: bool,
pane_is_on_top_of_stack: bool,
pane_is_on_bottom_of_stack: bool,
) { ) {
let color = self.frame_color(client_id, client_mode, session_is_mirrored); let color = self.frame_color(client_id, client_mode, session_is_mirrored);
boundaries.add_rect(self.pane.as_ref(), color); boundaries.add_rect(
self.pane.as_ref(),
color,
pane_is_on_top_of_stack,
pane_is_on_bottom_of_stack,
self.pane_is_stacked_under,
);
} }
fn frame_color( fn frame_color(
&self, &self,

View file

@ -400,6 +400,22 @@ impl Offset {
} }
} }
pub fn shift_right(right: usize) -> Self {
Self {
right,
..Default::default()
}
}
pub fn shift_right_top_and_bottom(right: usize, top: usize, bottom: usize) -> Self {
Self {
right,
top,
bottom,
..Default::default()
}
}
// FIXME: This should be top and left, not bottom and right, but `boundaries.rs` would need // FIXME: This should be top and left, not bottom and right, but `boundaries.rs` would need
// some changing // some changing
pub fn shift(bottom: usize, right: usize) -> Self { pub fn shift(bottom: usize, right: usize) -> Self {