diff options
author | Aram Drevekenin <aram@poor.dev> | 2022-12-24 15:48:04 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-24 15:48:04 +0100 |
commit | 799fa5de8dfd0b826be0f903796bba48a470127e (patch) | |
tree | 8c202e116b757162cadd0cc350da256b9741e248 /zellij-server/src/tab | |
parent | 17205793e4cd2da5eb431dacfc87dab574080b5b (diff) |
Floating panes in layouts (#2047)
* work
* tests passing
* tests: floating panes in layouts
* panes(plugins): floating plugins working
* refactor(tab): layout applier
* style(comment): remove outdated
* style(fmt): rustfmt
Diffstat (limited to 'zellij-server/src/tab')
5 files changed, 526 insertions, 198 deletions
diff --git a/zellij-server/src/tab/layout_applier.rs b/zellij-server/src/tab/layout_applier.rs new file mode 100644 index 000000000..a0b641df5 --- /dev/null +++ b/zellij-server/src/tab/layout_applier.rs @@ -0,0 +1,376 @@ +use zellij_utils::errors::prelude::*; + +use crate::resize_pty; +use crate::tab::{get_next_terminal_position, HoldForCommand, Pane}; + +use crate::{ + os_input_output::ServerOsApi, + panes::sixel::SixelImageStore, + panes::{FloatingPanes, TiledPanes}, + panes::{LinkHandler, PaneId, PluginPane, TerminalPane}, + plugins::PluginInstruction, + pty::PtyInstruction, + thread_bus::ThreadSenders, + ClientId, +}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use zellij_utils::{ + data::{Palette, Style}, + input::layout::{FloatingPanesLayout, PaneLayout, Run, RunPluginLocation}, + pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport}, +}; + +pub struct LayoutApplier<'a> { + viewport: Rc<RefCell<Viewport>>, // includes all non-UI panes + senders: ThreadSenders, + sixel_image_store: Rc<RefCell<SixelImageStore>>, + link_handler: Rc<RefCell<LinkHandler>>, + terminal_emulator_colors: Rc<RefCell<Palette>>, + terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>, + character_cell_size: Rc<RefCell<Option<SizeInPixels>>>, + style: Style, + display_area: Rc<RefCell<Size>>, // includes all panes (including eg. the status bar and tab bar in the default layout) + tiled_panes: &'a mut TiledPanes, + floating_panes: &'a mut FloatingPanes, + draw_pane_frames: bool, + focus_pane_id: &'a mut Option<PaneId>, + os_api: Box<dyn ServerOsApi>, +} + +impl<'a> LayoutApplier<'a> { + pub fn new( + viewport: &Rc<RefCell<Viewport>>, + senders: &ThreadSenders, + sixel_image_store: &Rc<RefCell<SixelImageStore>>, + link_handler: &Rc<RefCell<LinkHandler>>, + terminal_emulator_colors: &Rc<RefCell<Palette>>, + terminal_emulator_color_codes: &Rc<RefCell<HashMap<usize, String>>>, + character_cell_size: &Rc<RefCell<Option<SizeInPixels>>>, + style: &Style, + display_area: &Rc<RefCell<Size>>, // includes all panes (including eg. the status bar and tab bar in the default layout) + tiled_panes: &'a mut TiledPanes, + floating_panes: &'a mut FloatingPanes, + draw_pane_frames: bool, + focus_pane_id: &'a mut Option<PaneId>, + os_api: &Box<dyn ServerOsApi>, + ) -> Self { + let viewport = viewport.clone(); + let senders = senders.clone(); + let sixel_image_store = sixel_image_store.clone(); + let link_handler = link_handler.clone(); + let terminal_emulator_colors = terminal_emulator_colors.clone(); + let terminal_emulator_color_codes = terminal_emulator_color_codes.clone(); + let character_cell_size = character_cell_size.clone(); + let style = style.clone(); + let display_area = display_area.clone(); + let os_api = os_api.clone(); + LayoutApplier { + viewport, + senders, + sixel_image_store, + link_handler, + terminal_emulator_colors, + terminal_emulator_color_codes, + character_cell_size, + style, + display_area, + tiled_panes, + floating_panes, + draw_pane_frames, + focus_pane_id, + os_api, + } + } + pub fn apply_layout( + &mut self, + layout: PaneLayout, + floating_panes_layout: Vec<FloatingPanesLayout>, + new_terminal_ids: Vec<(u32, HoldForCommand)>, + new_floating_terminal_ids: Vec<(u32, HoldForCommand)>, + mut new_plugin_ids: HashMap<RunPluginLocation, Vec<u32>>, + client_id: ClientId, + ) -> Result<bool> { + // true => layout has floating panes + let layout_name = layout.name.clone(); + self.apply_tiled_panes_layout(layout, new_terminal_ids, &mut new_plugin_ids, client_id)?; + let layout_has_floating_panes = self.apply_floating_panes_layout( + floating_panes_layout, + new_floating_terminal_ids, + &mut new_plugin_ids, + layout_name, + )?; + return Ok(layout_has_floating_panes); + } + fn apply_tiled_panes_layout( + &mut self, + layout: PaneLayout, + new_terminal_ids: Vec<(u32, HoldForCommand)>, + new_plugin_ids: &mut HashMap<RunPluginLocation, Vec<u32>>, + client_id: ClientId, + ) -> Result<()> { + let err_context = || format!("failed to apply tiled panes layout"); + let (viewport_cols, viewport_rows) = { + let viewport = self.viewport.borrow(); + (viewport.cols, viewport.rows) + }; + let mut free_space = PaneGeom::default(); + free_space.cols.set_inner(viewport_cols); + free_space.rows.set_inner(viewport_rows); + match layout.position_panes_in_space(&free_space) { + Ok(positions_in_layout) => { + let positions_and_size = positions_in_layout.iter(); + let mut new_terminal_ids = new_terminal_ids.iter(); + + let mut focus_pane_id: Option<PaneId> = None; + let mut set_focus_pane_id = |layout: &PaneLayout, pane_id: PaneId| { + if layout.focus.unwrap_or(false) && focus_pane_id.is_none() { + focus_pane_id = Some(pane_id); + } + }; + + for (layout, position_and_size) in positions_and_size { + // A plugin pane + if let Some(Run::Plugin(run)) = layout.run.clone() { + let pane_title = run.location.to_string(); + let pid = new_plugin_ids + .get_mut(&run.location) + .and_then(|ids| ids.pop()) + .with_context(err_context)?; + let mut new_plugin = PluginPane::new( + pid, + *position_and_size, + self.senders + .to_plugin + .as_ref() + .with_context(err_context)? + .clone(), + pane_title, + layout.name.clone().unwrap_or_default(), + self.sixel_image_store.clone(), + self.terminal_emulator_colors.clone(), + self.terminal_emulator_color_codes.clone(), + self.link_handler.clone(), + self.character_cell_size.clone(), + self.style, + ); + new_plugin.set_borderless(layout.borderless); + self.tiled_panes + .add_pane_with_existing_geom(PaneId::Plugin(pid), Box::new(new_plugin)); + set_focus_pane_id(layout, PaneId::Plugin(pid)); + } else { + // there are still panes left to fill, use the pids we received in this method + if let Some((pid, hold_for_command)) = new_terminal_ids.next() { + let next_terminal_position = + get_next_terminal_position(&self.tiled_panes, &self.floating_panes); + let initial_title = match &layout.run { + Some(Run::Command(run_command)) => Some(run_command.to_string()), + _ => None, + }; + let mut new_pane = TerminalPane::new( + *pid, + *position_and_size, + self.style, + next_terminal_position, + layout.name.clone().unwrap_or_default(), + self.link_handler.clone(), + self.character_cell_size.clone(), + self.sixel_image_store.clone(), + self.terminal_emulator_colors.clone(), + self.terminal_emulator_color_codes.clone(), + initial_title, + ); + new_pane.set_borderless(layout.borderless); + if let Some(held_command) = hold_for_command { + new_pane.hold(None, true, held_command.clone()); + } + self.tiled_panes.add_pane_with_existing_geom( + PaneId::Terminal(*pid), + Box::new(new_pane), + ); + set_focus_pane_id(layout, PaneId::Terminal(*pid)); + } + } + } + for (unused_pid, _) in new_terminal_ids { + self.senders + .send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(*unused_pid))) + .with_context(err_context)?; + } + self.adjust_viewport(); + self.set_focused_tiled_pane(focus_pane_id, client_id); + }, + Err(e) => { + for (unused_pid, _) in new_terminal_ids { + self.senders + .send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(unused_pid))) + .with_context(err_context)?; + } + Err::<(), _>(anyError::msg(e)) + .with_context(err_context) + .non_fatal(); // TODO: propagate this to the user + }, + }; + Ok(()) + } + fn apply_floating_panes_layout( + &mut self, + floating_panes_layout: Vec<FloatingPanesLayout>, + new_floating_terminal_ids: Vec<(u32, HoldForCommand)>, + new_plugin_ids: &mut HashMap<RunPluginLocation, Vec<u32>>, + layout_name: Option<String>, + ) -> Result<bool> { + // true => has floating panes + let err_context = || format!("Failed to apply_floating_panes_layout"); + let mut layout_has_floating_panes = false; + let floating_panes_layout = floating_panes_layout.iter(); + let mut focused_floating_pane = None; + let mut new_floating_terminal_ids = new_floating_terminal_ids.iter(); + for floating_pane_layout in floating_panes_layout { + layout_has_floating_panes = true; + if let Some(Run::Plugin(run)) = floating_pane_layout.run.clone() { + let position_and_size = self + .floating_panes + .position_floating_pane_layout(&floating_pane_layout); + let pane_title = run.location.to_string(); + let pid = new_plugin_ids + .get_mut(&run.location) + .and_then(|ids| ids.pop()) + .with_context(err_context)?; + let mut new_pane = PluginPane::new( + pid, + position_and_size, + self.senders + .to_plugin + .as_ref() + .with_context(err_context)? + .clone(), + pane_title, + layout_name.clone().unwrap_or_default(), + self.sixel_image_store.clone(), + self.terminal_emulator_colors.clone(), + self.terminal_emulator_color_codes.clone(), + self.link_handler.clone(), + self.character_cell_size.clone(), + self.style, + ); + new_pane.set_borderless(false); + new_pane.set_content_offset(Offset::frame(1)); + resize_pty!(new_pane, self.os_api, self.senders)?; + self.floating_panes + .add_pane(PaneId::Plugin(pid), Box::new(new_pane)); + if floating_pane_layout.focus.unwrap_or(false) { + focused_floating_pane = Some(PaneId::Plugin(pid)); + } + } else if let Some((pid, hold_for_command)) = new_floating_terminal_ids.next() { + let position_and_size = self + .floating_panes + .position_floating_pane_layout(&floating_pane_layout); + let next_terminal_position = + get_next_terminal_position(&self.tiled_panes, &self.floating_panes); + let initial_title = match &floating_pane_layout.run { + Some(Run::Command(run_command)) => Some(run_command.to_string()), + _ => None, + }; + let mut new_pane = TerminalPane::new( + *pid, + position_and_size, + self.style, + next_terminal_position, + floating_pane_layout.name.clone().unwrap_or_default(), + self.link_handler.clone(), + self.character_cell_size.clone(), + self.sixel_image_store.clone(), + self.terminal_emulator_colors.clone(), + self.terminal_emulator_color_codes.clone(), + initial_title, + ); + new_pane.set_borderless(false); + new_pane.set_content_offset(Offset::frame(1)); + if let Some(held_command) = hold_for_command { + new_pane.hold(None, true, held_command.clone()); + } + resize_pty!(new_pane, self.os_api, self.senders)?; + self.floating_panes + .add_pane(PaneId::Terminal(*pid), Box::new(new_pane)); + if floating_pane_layout.focus.unwrap_or(false) { + focused_floating_pane = Some(PaneId::Terminal(*pid)); + } + } + } + if let Some(focused_floating_pane) = focused_floating_pane { + self.floating_panes + .focus_pane_for_all_clients(focused_floating_pane); + } + if layout_has_floating_panes { + Ok(true) + } else { + Ok(false) + } + } + fn resize_whole_tab(&mut self, new_screen_size: Size) { + self.floating_panes.resize(new_screen_size); + self.floating_panes + .resize_pty_all_panes(&mut self.os_api) + .unwrap(); // we need to do this explicitly because floating_panes.resize does not do this + self.tiled_panes.resize(new_screen_size); + } + fn offset_viewport(&mut self, position_and_size: &Viewport) { + let mut viewport = self.viewport.borrow_mut(); + if position_and_size.x == viewport.x + && position_and_size.x + position_and_size.cols == viewport.x + viewport.cols + { + if position_and_size.y == viewport.y { + viewport.y += position_and_size.rows; + viewport.rows -= position_and_size.rows; + } else if position_and_size.y + position_and_size.rows == viewport.y + viewport.rows { + viewport.rows -= position_and_size.rows; + } + } + if position_and_size.y == viewport.y + && position_and_size.y + position_and_size.rows == viewport.y + viewport.rows + { + if position_and_size.x == viewport.x { + viewport.x += position_and_size.cols; + viewport.cols -= position_and_size.cols; + } else if position_and_size.x + position_and_size.cols == viewport.x + viewport.cols { + viewport.cols -= position_and_size.cols; + } + } + } + fn adjust_viewport(&mut self) { + // here we offset the viewport after applying a tiled panes layout + // from borderless panes that are on the edges of the + // screen, this is so that when we don't have pane boundaries (eg. when they were + // disabled by the user) boundaries won't be drawn around these panes + // geometrically, we can only do this with panes that are on the edges of the + // screen - so it's mostly a best-effort thing + let display_area = { + let display_area = self.display_area.borrow(); + *display_area + }; + self.resize_whole_tab(display_area); + let boundary_geoms = self.tiled_panes.borderless_pane_geoms(); + for geom in boundary_geoms { + self.offset_viewport(&geom) + } + self.tiled_panes.set_pane_frames(self.draw_pane_frames); + } + fn set_focused_tiled_pane(&mut self, focus_pane_id: Option<PaneId>, client_id: ClientId) { + if let Some(pane_id) = focus_pane_id { + *self.focus_pane_id = Some(pane_id); + self.tiled_panes.focus_pane(pane_id, client_id); + } else { + let next_selectable_pane_id = self.tiled_panes.first_selectable_pane_id(); + match next_selectable_pane_id { + Some(active_pane_id) => { + self.tiled_panes.focus_pane(active_pane_id, client_id); + }, + None => { + self.tiled_panes.clear_active_panes(); + }, + } + } + } +} diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index c2b26ecf8..0cf3a6977 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -3,6 +3,7 @@ mod clipboard; mod copy_command; +mod layout_applier; use copy_command::CopyCommand; use std::env::temp_dir; @@ -17,6 +18,7 @@ use crate::background_jobs::BackgroundJob; use crate::pty_writer::PtyWriteInstruction; use crate::screen::CopyOptions; use crate::ui::pane_boundaries_frame::FrameParams; +use layout_applier::LayoutApplier; use self::clipboard::ClipboardProvider; use crate::{ @@ -24,7 +26,7 @@ use crate::{ output::{CharacterChunk, Output, SixelImageChunk}, panes::sixel::SixelImageStore, panes::{FloatingPanes, TiledPanes}, - panes::{LinkHandler, PaneId, PluginPane, TerminalPane}, + panes::{LinkHandler, PaneId, TerminalPane}, plugins::PluginInstruction, pty::{ClientOrTabIndex, PtyInstruction, VteBytes}, thread_bus::ThreadSenders, @@ -42,7 +44,7 @@ use zellij_utils::{ data::{Event, InputMode, ModeInfo, Palette, PaletteColor, Style}, input::{ command::TerminalAction, - layout::{PaneLayout, Run, RunPluginLocation}, + layout::{FloatingPanesLayout, PaneLayout, RunPluginLocation}, parse_keys, }, pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport}, @@ -398,6 +400,26 @@ pub enum AdjustedInput { ReRunCommandInThisPane(RunCommand), CloseThisPane, } +pub fn get_next_terminal_position( + tiled_panes: &TiledPanes, + floating_panes: &FloatingPanes, +) -> usize { + let tiled_panes_count = tiled_panes + .get_panes() + .filter(|(k, _)| match k { + PaneId::Plugin(_) => false, + PaneId::Terminal(_) => true, + }) + .count(); + let floating_panes_count = floating_panes + .get_panes() + .filter(|(k, _)| match k { + PaneId::Plugin(_) => false, + PaneId::Terminal(_) => true, + }) + .count(); + tiled_panes_count + floating_panes_count + 1 +} impl Tab { // FIXME: Still too many arguments for clippy to be happy... @@ -509,170 +531,45 @@ impl Tab { pub fn apply_layout( &mut self, layout: PaneLayout, + floating_panes_layout: Vec<FloatingPanesLayout>, new_terminal_ids: Vec<(u32, HoldForCommand)>, - mut new_plugin_ids: HashMap<RunPluginLocation, Vec<u32>>, - tab_index: usize, + new_floating_terminal_ids: Vec<(u32, HoldForCommand)>, + new_plugin_ids: HashMap<RunPluginLocation, Vec<u32>>, client_id: ClientId, ) -> Result<()> { - let err_context = || { - format!( - "failed to apply layout {layout:#?} in tab {tab_index} for client id {client_id}" - ) - }; - - if self.tiled_panes.has_panes() { - Err::<(), _>(anyhow!( - "Applying a layout to a tab with existing panes - this is not yet supported!" - )) - .with_context(err_context) - .non_fatal(); - } - let (viewport_cols, viewport_rows) = { - let viewport = self.viewport.borrow(); - (viewport.cols, viewport.rows) - }; - let mut free_space = PaneGeom::default(); - free_space.cols.set_inner(viewport_cols); - free_space.rows.set_inner(viewport_rows); - - match layout.position_panes_in_space(&free_space) { - Ok(positions_in_layout) => { - let positions_and_size = positions_in_layout.iter(); - let mut new_terminal_ids = new_terminal_ids.iter(); - - let mut focus_pane_id: Option<PaneId> = None; - let mut set_focus_pane_id = |layout: &PaneLayout, pane_id: PaneId| { - if layout.focus.unwrap_or(false) && focus_pane_id.is_none() { - focus_pane_id = Some(pane_id); - } - }; - - for (layout, position_and_size) in positions_and_size { - // A plugin pane - if let Some(Run::Plugin(run)) = layout.run.clone() { - // let (pid_tx, pid_rx) = channel(); - let pane_title = run.location.to_string(); - let pid = new_plugin_ids - .get_mut(&run.location) - .unwrap() - .pop() - .unwrap(); // TODO: - // err_context - // and - // stuff - let mut new_plugin = PluginPane::new( - pid, - *position_and_size, - self.senders - .to_plugin - .as_ref() - .with_context(err_context)? - .clone(), - pane_title, - layout.name.clone().unwrap_or_default(), - self.sixel_image_store.clone(), - self.terminal_emulator_colors.clone(), - self.terminal_emulator_color_codes.clone(), - self.link_handler.clone(), - self.character_cell_size.clone(), - self.style, - ); - new_plugin.set_borderless(layout.borderless); - self.tiled_panes - .add_pane_with_existing_geom(PaneId::Plugin(pid), Box::new(new_plugin)); - set_focus_pane_id(layout, PaneId::Plugin(pid)); - } else { - // there are still panes left to fill, use the pids we received in this method - if let Some((pid, hold_for_command)) = new_terminal_ids.next() { - let next_terminal_position = self.get_next_terminal_position(); - let initial_title = match &layout.run { - Some(Run::Command(run_command)) => Some(run_command.to_string()), - _ => None, - }; - let mut new_pane = TerminalPane::new( - *pid, - *position_and_size, - self.style, - next_terminal_position, - layout.name.clone().unwrap_or_default(), - self.link_handler.clone(), - self.character_cell_size.clone(), - self.sixel_image_store.clone(), - self.terminal_emulator_colors.clone(), - self.terminal_emulator_color_codes.clone(), - initial_title, - ); - new_pane.set_borderless(layout.borderless); - if let Some(held_command) = hold_for_command { - new_pane.hold(None, true, held_command.clone()); - } - self.tiled_panes.add_pane_with_existing_geom( - PaneId::Terminal(*pid), - Box::new(new_pane), - ); - set_focus_pane_id(layout, PaneId::Terminal(*pid)); - } - } - } - for (unused_pid, _) in new_terminal_ids { - // this is a bit of a hack and happens because we don't have any central location that - // can query the screen as to how many panes it needs to create a layout - // fixing this will require a bit of an architecture change - self.senders - .send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(*unused_pid))) - .with_context(err_context)?; - } - - // here we offset the viewport from borderless panes that are on the edges of the - // screen, this is so that when we don't have pane boundaries (eg. when they were - // disabled by the user) boundaries won't be drawn around these panes - // geometrically, we can only do this with panes that are on the edges of the - // screen - so it's mostly a best-effort thing - let display_area = { - let display_area = self.display_area.borrow(); - *display_area - }; - self.resize_whole_tab(display_area); - let boundary_geoms = self.tiled_panes.borderless_pane_geoms(); - for geom in boundary_geoms { - self.offset_viewport(&geom) - } - self.tiled_panes.set_pane_frames(self.draw_pane_frames); - self.should_clear_display_before_rendering = true; - - if let Some(pane_id) = focus_pane_id { - self.focus_pane_id = Some(pane_id); - self.tiled_panes.focus_pane(pane_id, client_id); - } else { - // This is the end of the nasty viewport hack... - let next_selectable_pane_id = self.tiled_panes.first_selectable_pane_id(); - match next_selectable_pane_id { - Some(active_pane_id) => { - self.tiled_panes.focus_pane(active_pane_id, client_id); |