diff options
author | Aram Drevekenin <aram@poor.dev> | 2023-02-17 12:05:50 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-17 12:05:50 +0100 |
commit | f1ff272b0b65f6d328fef24531ada67ea585ce85 (patch) | |
tree | 0e33aa4a1af21c0785fa30118ee42a21972497cf /zellij-server/src/panes | |
parent | 1517036c2489b2a7dc43230e2ba8a05841a69dbe (diff) |
feat(ui): swap layouts and stacked panes (#2167)
* relayout working with hard coded layout
* work
* refactor(layout): PaneLayout => TiledPaneLayout
* tests passing
* tests passing
* tests passing
* stacked panes and passing tests
* tests for stacked panes
* refactor(panes): stacked panes
* fix: focusing into stacked panes from the left/right
* fix(layouts): handle stacked layouts in the middle of the screen
* fix(pane-stack): focus correctly when coming to stack from above/below
* fix(stacked-panes): resize stack
* fix(stacked-panes): focus with mouse
* fix(stacked-panes): focus next pane
* fix(layout-applier): sane focus order
* fix(stacked-panes): better titles for one-liners
* fix(stacked-panes): handle moving pane location in stack
* fix(relayout): properly calculate display area
* fix(relayout): properly calculate rounding errors
* fix(stacked-panes): properly handle closing a pane near a stack
* fix(swap-layouts): adjust swap layout sort order
* feat(swap-layouts): ui + ux
* fix(swap-layouts): include base layout
* refactor(layout): remove unused method
* fix(swap-layouts): respect pane contents and focus
* work
* fix(swap-layouts): load swap layouts from external file
* fix(swap-layouts): properly truncate layout children
* fix(stacked-panes): allow stacked panes to become fullscreen
* fix(swap-layouts): work with multiple tabs
* fix(swap-layouts): embed/eject panes properly with auto-layout
* fix(stacked-panes): close last pane in stack
* fix(stacked-panes): move focus for all clients in stack
* fix(floating-panes): set layout damaged when moving panes
* fix(relayout): move out of unfitting layout when resizing whole tab
* fix(ui): background color for swap layout indicator
* fix(keybinds): add switch next layout in tmux
* fix(ui): swap layout indication in compact layout
* fix(compact): correct swap constraint
* fix(tests): tmux swap config shortcut
* fix(resizes): cache resizes so as not to confuse panes (eg. vim) with multiple resizes that it debounces weirdly
* feat(cli): dump swap layouts
* fix(ui): stacked panes without pane frames
* fix(ux): move pane forward/backwards also with floating panes
* refactor(lint): remove unused stuff
* refactor(tab): move swap layouts to separate file
* style(fmt): rustfmt
* style(fmt): rustfmt
* refactor(panes): various cleanups
* chore(deps): upgrade termwiz to get alt left-bracket
* fix(assets): merge conflicts of binary files
* style(fmt): rustfmt
* style(clippy): no thank you!
* chore(repo): remove garbage file
Diffstat (limited to 'zellij-server/src/panes')
-rw-r--r-- | zellij-server/src/panes/active_panes.rs | 1 | ||||
-rw-r--r-- | zellij-server/src/panes/floating_panes/floating_pane_grid.rs | 55 | ||||
-rw-r--r-- | zellij-server/src/panes/floating_panes/mod.rs | 126 | ||||
-rw-r--r-- | zellij-server/src/panes/plugin_pane.rs | 24 | ||||
-rw-r--r-- | zellij-server/src/panes/terminal_pane.rs | 32 | ||||
-rw-r--r-- | zellij-server/src/panes/tiled_panes/mod.rs | 364 | ||||
-rw-r--r-- | zellij-server/src/panes/tiled_panes/pane_resizer.rs | 137 | ||||
-rw-r--r-- | zellij-server/src/panes/tiled_panes/stacked_panes.rs | 630 | ||||
-rw-r--r-- | zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs | 445 | ||||
-rw-r--r-- | zellij-server/src/panes/unit/search_in_pane_tests.rs | 1 | ||||
-rw-r--r-- | zellij-server/src/panes/unit/terminal_pane_tests.rs | 12 |
11 files changed, 1584 insertions, 243 deletions
diff --git a/zellij-server/src/panes/active_panes.rs b/zellij-server/src/panes/active_panes.rs index 3fce0a9ff..c726bafe1 100644 --- a/zellij-server/src/panes/active_panes.rs +++ b/zellij-server/src/panes/active_panes.rs @@ -3,6 +3,7 @@ use crate::tab::Pane; use crate::{os_input_output::ServerOsApi, panes::PaneId, ClientId}; use std::collections::{BTreeMap, HashMap}; +#[derive(Clone)] pub struct ActivePanes { active_panes: HashMap<ClientId, PaneId>, os_api: Box<dyn ServerOsApi>, diff --git a/zellij-server/src/panes/floating_panes/floating_pane_grid.rs b/zellij-server/src/panes/floating_panes/floating_pane_grid.rs index 2245ee16a..afeecc8d2 100644 --- a/zellij-server/src/panes/floating_panes/floating_pane_grid.rs +++ b/zellij-server/src/panes/floating_panes/floating_pane_grid.rs @@ -703,6 +703,56 @@ impl<'a> FloatingPaneGrid<'a> { .copied(); next_index } + pub fn next_selectable_pane_id(&self, current_pane_id: &PaneId) -> Option<PaneId> { + let panes = self.panes.borrow(); + let mut panes: Vec<(PaneId, &&mut Box<dyn Pane>)> = panes + .iter() + .filter(|(_, p)| p.selectable()) + .map(|(p_id, p)| (*p_id, p)) + .collect(); + panes.sort_by(|(_a_id, a_pane), (_b_id, b_pane)| { + if a_pane.y() == b_pane.y() { + a_pane.x().cmp(&b_pane.x()) + } else { + a_pane.y().cmp(&b_pane.y()) + } + }); + let active_pane_position = panes + .iter() + .position(|(id, _)| id == current_pane_id) + .unwrap(); + + let next_active_pane_id = panes + .get(active_pane_position + 1) + .or_else(|| panes.get(0)) + .map(|p| p.0) + .unwrap(); + Some(next_active_pane_id) + } + pub fn previous_selectable_pane_id(&self, current_pane_id: &PaneId) -> Option<PaneId> { + let panes = self.panes.borrow(); + let mut panes: Vec<(PaneId, &&mut Box<dyn Pane>)> = panes + .iter() + .filter(|(_, p)| p.selectable()) + .map(|(p_id, p)| (*p_id, p)) + .collect(); + panes.sort_by(|(_a_id, a_pane), (_b_id, b_pane)| { + if a_pane.y() == b_pane.y() { + a_pane.x().cmp(&b_pane.x()) + } else { + a_pane.y().cmp(&b_pane.y()) + } + }); + let active_pane_position = panes.iter().position(|(id, _)| id == current_pane_id)?; + + let last_pane = panes.last()?; + let previous_active_pane_id = if active_pane_position == 0 { + last_pane.0 + } else { + panes.get(active_pane_position - 1)?.0 + }; + Some(previous_active_pane_id) + } pub fn find_room_for_new_pane(&self) -> Option<PaneGeom> { let panes = self.panes.borrow(); let pane_geoms: Vec<PaneGeom> = panes.values().map(|p| p.position_and_size()).collect(); @@ -766,6 +816,7 @@ fn half_size_middle_geom(space: &Viewport, offset: usize) -> PaneGeom { y: space.y + (space.rows as f64 / 4.0).round() as usize + offset, cols: Dimension::fixed(space.cols / 2), rows: Dimension::fixed(space.rows / 2), + is_stacked: false, }; geom.cols.set_inner(space.cols / 2); geom.rows.set_inner(space.rows / 2); @@ -778,6 +829,7 @@ fn half_size_top_left_geom(space: &Viewport, offset: usize) -> PaneGeom { y: space.y + 2 + offset, cols: Dimension::fixed(space.cols / 3), rows: Dimension::fixed(space.rows / 3), + is_stacked: false, }; geom.cols.set_inner(space.cols / 3); geom.rows.set_inner(space.rows / 3); @@ -790,6 +842,7 @@ fn half_size_top_right_geom(space: &Viewport, offset: usize) -> PaneGeom { y: space.y + 2 + offset, cols: Dimension::fixed(space.cols / 3), rows: Dimension::fixed(space.rows / 3), + is_stacked: false, }; geom.cols.set_inner(space.cols / 3); geom.rows.set_inner(space.rows / 3); @@ -802,6 +855,7 @@ fn half_size_bottom_left_geom(space: &Viewport, offset: usize) -> PaneGeom { y: ((space.y + space.rows) - (space.rows / 3) - 2).saturating_sub(offset), cols: Dimension::fixed(space.cols / 3), rows: Dimension::fixed(space.rows / 3), + is_stacked: false, }; geom.cols.set_inner(space.cols / 3); geom.rows.set_inner(space.rows / 3); @@ -814,6 +868,7 @@ fn half_size_bottom_right_geom(space: &Viewport, offset: usize) -> PaneGeom { y: ((space.y + space.rows) - (space.rows / 3) - 2).saturating_sub(offset), cols: Dimension::fixed(space.cols / 3), rows: Dimension::fixed(space.rows / 3), + is_stacked: false, }; geom.cols.set_inner(space.cols / 3); geom.rows.set_inner(space.rows / 3); diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index 47b3e2f25..399d16c98 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -25,7 +25,7 @@ use zellij_utils::{ data::{ModeInfo, Style}, errors::prelude::*, input::command::RunCommand, - input::layout::FloatingPanesLayout, + input::layout::FloatingPaneLayout, pane_size::{Dimension, Offset, PaneGeom, Size, Viewport}, }; @@ -200,6 +200,14 @@ impl FloatingPanes { pub fn active_pane_id(&self, client_id: ClientId) -> Option<PaneId> { self.active_panes.get(&client_id).copied() } + pub fn active_pane_id_or_focused_pane_id(&self, client_id: Option<ClientId>) -> Option<PaneId> { + // returns the focused pane of any client_id - should be safe because the way things are + // set up at the time of writing, all clients are focused on the same floating pane due to + // z_index issues + client_id + .and_then(|client_id| self.active_panes.get(&client_id).copied()) + .or_else(|| self.panes.keys().next().copied()) + } pub fn toggle_show_panes(&mut self, should_show_floating_panes: bool) { self.show_panes = should_show_floating_panes; if should_show_floating_panes { @@ -227,7 +235,7 @@ impl FloatingPanes { } pub fn position_floating_pane_layout( &mut self, - floating_pane_layout: &FloatingPanesLayout, + floating_pane_layout: &FloatingPaneLayout, ) -> PaneGeom { let display_area = *self.display_area.borrow(); let viewport = *self.viewport.borrow(); @@ -239,32 +247,32 @@ impl FloatingPanes { ); let mut position = floating_pane_grid.find_room_for_new_pane().unwrap(); // TODO: no unwrap if let Some(x) = &floating_pane_layout.x { - position.x = x.to_position(display_area.cols); + position.x = x.to_position(viewport.cols); } if let Some(y) = &floating_pane_layout.y { - position.y = y.to_position(display_area.rows); + position.y = y.to_position(viewport.rows); } if let Some(width) = &floating_pane_layout.width { - position.cols = Dimension::fixed(width.to_position(display_area.cols)); + position.cols = Dimension::fixed(width.to_position(viewport.cols)); } if let Some(height) = &floating_pane_layout.height { - position.rows = Dimension::fixed(height.to_position(display_area.rows)); + position.rows = Dimension::fixed(height.to_position(viewport.rows)); } - if position.cols.as_usize() > display_area.cols { - position.cols = Dimension::fixed(display_area.cols); + if position.cols.as_usize() > viewport.cols { + position.cols = Dimension::fixed(viewport.cols); } - if position.rows.as_usize() > display_area.rows { - position.rows = Dimension::fixed(display_area.rows); + if position.rows.as_usize() > viewport.rows { + position.rows = Dimension::fixed(viewport.rows); } - if position.x + position.cols.as_usize() > display_area.cols { + if position.x + position.cols.as_usize() > viewport.cols { position.x = position .x - .saturating_sub((position.x + position.cols.as_usize()) - display_area.cols); + .saturating_sub((position.x + position.cols.as_usize()) - viewport.cols); } - if position.y + position.rows.as_usize() > display_area.rows { + if position.y + position.rows.as_usize() > viewport.rows { position.y = position .y - .saturating_sub((position.y + position.rows.as_usize()) - display_area.rows); + .saturating_sub((position.y + position.rows.as_usize()) - viewport.rows); } position } @@ -333,6 +341,9 @@ impl FloatingPanes { &active_panes, multiple_users_exist_in_session, Some(z_index + 1), // +1 because 0 is reserved for non-floating panes + false, + false, + true, ); for client_id in &connected_clients { let client_mode = self @@ -570,6 +581,50 @@ impl FloatingPanes { self.set_force_render(); } } + pub fn move_active_pane( + &mut self, + search_backwards: bool, + os_api: &mut Box<dyn ServerOsApi>, + client_id: ClientId, + ) { + let active_pane_id = self.get_active_pane_id(client_id).unwrap(); + + let new_position_id = { + let pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + *self.display_area.borrow(), + *self.viewport.borrow(), + ); + if search_backwards { + pane_grid.previous_selectable_pane_id(&active_pane_id) + } else { + pane_grid.next_selectable_pane_id(&active_pane_id) + } + }; + if let Some(new_position_id) = new_position_id { + let current_position = self.panes.get(&active_pane_id).unwrap(); + let prev_geom = current_position.position_and_size(); + let prev_geom_override = current_position.geom_override(); + + let new_position = self.panes.get_mut(&new_position_id).unwrap(); + let next_geom = new_position.position_and_size(); + let next_geom_override = new_position.geom_override(); + new_position.set_geom(prev_geom); + if let Some(geom) = prev_geom_override { + new_position.set_geom_override(geom); + } + new_position.set_should_render(true); + + let current_position = self.panes.get_mut(&active_pane_id).unwrap(); + current_position.set_geom(next_geom); + if let Some(geom) = next_geom_override { + current_position.set_geom_override(geom); + } + current_position.set_should_render(true); + let _ = self.set_pane_frames(os_api); + } + } pub fn move_clients_out_of_pane(&mut self, pane_id: PaneId) { let active_panes: Vec<(ClientId, PaneId)> = self .active_panes @@ -735,6 +790,17 @@ impl FloatingPanes { pub fn get_panes(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> { self.panes.iter() } + pub fn visible_panes_count(&self) -> usize { + self.panes.len() + } + pub fn drain(&mut self) -> BTreeMap<PaneId, Box<dyn Pane>> { + self.z_indices.clear(); + self.desired_pane_positions.clear(); + match self.panes.iter().next().map(|(pid, _p)| *pid) { + Some(first_pid) => self.panes.split_off(&first_pid), + None => BTreeMap::new(), + } + } fn move_clients_between_panes(&mut self, from_pane_id: PaneId, to_pane_id: PaneId) { let clients_in_pane: Vec<ClientId> = self .active_panes @@ -748,4 +814,36 @@ impl FloatingPanes { .insert(client_id, to_pane_id, &mut self.panes); } } + pub fn reapply_pane_focus(&mut self) { + if let Some(focused_pane) = self.first_active_floating_pane_id() { + // floating pane focus is the same for all clients + self.focus_pane_for_all_clients(focused_pane); + } + } + pub fn switch_active_pane_with(&mut self, os_api: &mut Box<dyn ServerOsApi>, pane_id: PaneId) { + if let Some(active_pane_id) = self.first_active_floating_pane_id() { + let current_position = self.panes.get(&active_pane_id).unwrap(); + let prev_geom = current_position.position_and_size(); + let prev_geom_override = current_position.geom_override(); + + let new_position = self.panes.get_mut(&pane_id).unwrap(); + let next_geom = new_position.position_and_size(); + let next_geom_override = new_position.geom_override(); + new_position.set_geom(prev_geom); + if let Some(geom) = prev_geom_override { + new_position.set_geom_override(geom); + } + resize_pty!(new_position, os_api, self.senders).unwrap(); + new_position.set_should_render(true); + + let current_position = self.panes.get_mut(&active_pane_id).unwrap(); + current_position.set_geom(next_geom); + if let Some(geom) = next_geom_override { + current_position.set_geom_override(geom); + } + resize_pty!(current_position, os_api, self.senders).unwrap(); + current_position.set_should_render(true); + self.focus_pane_for_all_clients(active_pane_id); + } + } } diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index babb66b67..c6896f90d 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -16,6 +16,7 @@ use zellij_utils::{ channels::SenderWithContext, data::{Event, InputMode, Mouse, Palette, PaletteColor, Style}, errors::prelude::*, + input::layout::Run, pane_size::PaneGeom, shared::make_terminal_title, vte, @@ -65,6 +66,7 @@ pub(crate) struct PluginPane { frame: HashMap<ClientId, PaneFrame>, borderless: bool, pane_frame_color_override: Option<(PaletteColor, Option<String>)>, + invoked_with: Option<Run>, } impl PluginPane { @@ -80,6 +82,7 @@ impl PluginPane { link_handler: Rc<RefCell<LinkHandler>>, character_cell_size: Rc<RefCell<Option<SizeInPixels>>>, style: Style, + invoked_with: Option<Run>, ) -> Self { Self { pid, @@ -104,6 +107,7 @@ impl PluginPane { grids: HashMap::new(), style, pane_frame_color_override: None, + invoked_with, } } } @@ -222,6 +226,11 @@ impl Pane for PluginPane { if self.should_render.get(&client_id).copied().unwrap_or(false) { let content_x = self.get_content_x(); let content_y = self.get_content_y(); + let rows = self.get_content_rows(); + let columns = self.get_content_columns(); + if rows < 1 || columns < 1 { + return Ok(None); + } if let Some(grid) = self.grids.get_mut(&client_id) { match grid.render(content_x, content_y, &self.style) { Ok(rendered_assets) => { @@ -265,8 +274,15 @@ impl Pane for PluginPane { self.pane_name.clone() }; + let mut 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 mut frame = PaneFrame::new( - self.current_geom().into(), + frame_geom.into(), grid.scrollback_position_and_length(), pane_title, frame_params, @@ -491,6 +507,12 @@ impl Pane for PluginPane { .as_ref() .map(|(color, _text)| *color) } + fn invoked_with(&self) -> &Option<Run> { + &self.invoked_with + } + fn set_title(&mut self, title: String) { + self.pane_title = title; + } } impl PluginPane { diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index d206245ad..e9fffb186 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -18,6 +18,7 @@ use zellij_utils::pane_size::Offset; use zellij_utils::{ data::{InputMode, Palette, PaletteColor, Style}, errors::prelude::*, + input::layout::Run, pane_size::PaneGeom, pane_size::SizeInPixels, position::Position, @@ -111,6 +112,7 @@ pub struct TerminalPane { banner: Option<String>, // a banner to be rendered inside this TerminalPane, used for panes // held on startup and can possibly be used to display some errors pane_frame_color_override: Option<(PaletteColor, Option<String>)>, + invoked_with: Option<Run>, } impl Pane for TerminalPane { @@ -165,6 +167,10 @@ impl Pane for TerminalPane { } fn cursor_coordinates(&self) -> Option<(usize, usize)> { // (x, y) + if self.get_content_rows() < 1 || self.get_content_columns() < 1 { + // do not render cursor if there's no room for it + return None; + } let Offset { top, left, .. } = self.content_offset; self.grid .cursor_coordinates() @@ -283,6 +289,11 @@ impl Pane for TerminalPane { if self.should_render() { let content_x = self.get_content_x(); let content_y = self.get_content_y(); + let rows = self.get_content_rows(); + let columns = self.get_content_columns(); + if rows < 1 || columns < 1 { + return Ok(None); + } match self.grid.render(content_x, content_y, &self.style) { Ok(rendered_assets) => { self.set_should_render(false); @@ -347,8 +358,15 @@ impl Pane for TerminalPane { self.pane_name.clone() }; + let mut 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 mut frame = PaneFrame::new( - self.current_geom().into(), + frame_geom.into(), self.grid.scrollback_position_and_length(), pane_title, frame_params, @@ -690,6 +708,12 @@ impl Pane for TerminalPane { .as_ref() .map(|(color, _text)| *color) } + fn invoked_with(&self) -> &Option<Run> { + &self.invoked_with + } + fn set_title(&mut self, title: String) { + self.pane_title = title; + } } impl TerminalPane { @@ -706,6 +730,7 @@ impl TerminalPane { terminal_emulator_colors: Rc<RefCell<Palette>>, terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>, initial_pane_title: Option<String>, + invoked_with: Option<Run>, ) -> TerminalPane { let initial_pane_title = initial_pane_title.unwrap_or_else(|| format!("Pane #{}", pane_index)); @@ -739,6 +764,7 @@ impl TerminalPane { is_held: None, banner: None, pane_frame_color_override: None, + invoked_with, } } pub fn get_x(&self) -> usize { @@ -780,6 +806,10 @@ impl TerminalPane { } pub fn cursor_coordinates(&self) -> Option<(usize, usize)> { // (x, y) + if self.get_content_rows() < 1 || self.get_content_columns() < 1 { + // do not render cursor if there's no room for it + return None; + } self.grid.cursor_coordinates() } fn render_first_run_banner(&mut self) { diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index cad53e3f1..bd6470c90 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -1,4 +1,5 @@ mod pane_resizer; +mod stacked_panes; mod tiled_pane_grid; use crate::resize_pty; @@ -15,6 +16,7 @@ use crate::{ ui::pane_contents_and_ui::PaneContentsAndUi, ClientId, }; +use stacked_panes::StackedPanes; use zellij_utils::{ data::{ModeInfo, ResizeStrategy, Style}, errors::prelude::*, @@ -249,6 +251,10 @@ impl TiledPanes { self.set_pane_frames(self.draw_pane_frames); } + pub fn reapply_pane_frames(&mut self) { + // same as set_pane_frames except it reapplies the current situation + self.set_pane_frames(self.draw_pane_frames); + } pub fn set_pane_frames(&mut self, draw_pane_frames: bool) { self.draw_pane_frames = draw_pane_frames; let viewport = *self.viewport.borrow(); @@ -276,7 +282,12 @@ impl TiledPanes { let position_and_size = pane.current_geom(); let (pane_columns_offset, pane_rows_offset) = pane_content_offset(&position_and_size, &viewport); - pane.set_content_offset(Offset::shift(pane_rows_offset, pane_columns_offset)); + if !draw_pane_frames && pane.current_geom().is_stacked { + // stacked panes should always leave 1 top row for a title + pane.set_content_offset(Offset::shift_right_and_top(pane_columns_offset, 1)); + } else { + pane.set_content_offset(Offset::shift(pane_rows_offset, pane_columns_offset)); + } } resize_pty!(pane, self.os_api, self.senders).unwrap(); @@ -287, |