diff options
author | Aram Drevekenin <aram@poor.dev> | 2021-08-12 14:50:00 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-12 14:50:00 +0200 |
commit | a37d3e5889476bbac209d20f89886daa3e9d3992 (patch) | |
tree | c02fbf779dda57b615a852fd6a43818b20722b19 /zellij-server/src/ui | |
parent | 426cee728a3b8c84edbc2d02cbc22f4840fc6a28 (diff) |
feat(ui): pane frames (new pane UI) (#643)
* work
* resize working
* move focus working
* close pane working
* selection and fullscreen working
* pane title line
* titles and conditional scroll title
* whole tab resize working
* plugin frames working
* plugin splitting working
* truncate pane frame titles
* cleanup
* panes always draw their own borders - also fix gap
* toggle pane frames
* move toggle to screen and fix some bugs
* fix plugin frame toggle
* fix terminal window resize
* fix scrolling and fullscreen bugs
* unit tests passing
* e2e tests passing and new test for new frames added
* refactor: TerminalPane and PluginPane
* refactor: Tab
* refactor: moar Tab
* refactor: Boundaries
* only render and calculate boundaries when there are no pane frames
* refactor: Layout
* fix(grid): properly resize when coming back from alternative viewport
* style: remove commented code
* style: fmt
* style: fmt
* style: fmt + clippy
* docs(changelog): update change
Diffstat (limited to 'zellij-server/src/ui')
-rw-r--r-- | zellij-server/src/ui/boundaries.rs | 159 | ||||
-rw-r--r-- | zellij-server/src/ui/mod.rs | 1 | ||||
-rw-r--r-- | zellij-server/src/ui/pane_boundaries_frame.rs | 277 | ||||
-rw-r--r-- | zellij-server/src/ui/pane_resizer.rs | 64 | ||||
-rw-r--r-- | zellij-server/src/ui/title_telescope.rs | 0 |
5 files changed, 372 insertions, 129 deletions
diff --git a/zellij-server/src/ui/boundaries.rs b/zellij-server/src/ui/boundaries.rs index 42a80ace1..b25f1cc08 100644 --- a/zellij-server/src/ui/boundaries.rs +++ b/zellij-server/src/ui/boundaries.rs @@ -1,3 +1,4 @@ +use zellij_utils::pane_size::PositionAndSize; use zellij_utils::zellij_tile; use crate::tab::Pane; @@ -405,73 +406,22 @@ impl Coordinates { } } -pub(crate) trait Rect { - fn x(&self) -> usize; - fn y(&self) -> usize; - fn rows(&self) -> usize; - fn columns(&self) -> usize; - fn right_boundary_x_coords(&self) -> usize { - self.x() + self.columns() - } - fn bottom_boundary_y_coords(&self) -> usize { - self.y() + self.rows() - } - fn is_directly_right_of(&self, other: &Self) -> bool { - self.x() == other.x() + other.columns() + 1 - } - fn is_directly_left_of(&self, other: &Self) -> bool { - self.x() + self.columns() + 1 == other.x() - } - fn is_directly_below(&self, other: &Self) -> bool { - self.y() == other.y() + other.rows() + 1 - } - fn is_directly_above(&self, other: &Self) -> bool { - self.y() + self.rows() + 1 == other.y() - } - fn horizontally_overlaps_with(&self, other: &Self) -> bool { - (self.y() >= other.y() && self.y() <= (other.y() + other.rows())) - || ((self.y() + self.rows()) <= (other.y() + other.rows()) - && (self.y() + self.rows()) > other.y()) - || (self.y() <= other.y() && (self.y() + self.rows() >= (other.y() + other.rows()))) - || (other.y() <= self.y() && (other.y() + other.rows() >= (self.y() + self.rows()))) - } - fn get_horizontal_overlap_with(&self, other: &Self) -> usize { - std::cmp::min(self.y() + self.rows(), other.y() + other.rows()) - - std::cmp::max(self.y(), other.y()) - } - fn vertically_overlaps_with(&self, other: &Self) -> bool { - (self.x() >= other.x() && self.x() <= (other.x() + other.columns())) - || ((self.x() + self.columns()) <= (other.x() + other.columns()) - && (self.x() + self.columns()) > other.x()) - || (self.x() <= other.x() - && (self.x() + self.columns() >= (other.x() + other.columns()))) - || (other.x() <= self.x() - && (other.x() + other.columns() >= (self.x() + self.columns()))) - } - fn get_vertical_overlap_with(&self, other: &Self) -> usize { - std::cmp::min(self.x() + self.columns(), other.x() + other.columns()) - - std::cmp::max(self.x(), other.x()) - } -} - pub struct Boundaries { - columns: usize, - rows: usize, - // boundary_characters: HashMap<Coordinates, BoundaryType>, + position_and_size: PositionAndSize, boundary_characters: HashMap<Coordinates, BoundarySymbol>, } impl Boundaries { - pub fn new(columns: u16, rows: u16) -> Self { - let columns = columns as usize; - let rows = rows as usize; + pub fn new(position_and_size: &PositionAndSize) -> Self { Boundaries { - columns, - rows, + position_and_size: *position_and_size, boundary_characters: HashMap::new(), } } pub fn add_rect(&mut self, rect: &dyn Pane, input_mode: InputMode, palette: Option<Palette>) { + if !self.is_fully_inside_screen(rect) { + return; + } let color = match palette.is_some() { true => match input_mode { InputMode::Normal | InputMode::Locked => Some(palette.unwrap().green), @@ -479,19 +429,23 @@ impl Boundaries { }, false => None, }; - if rect.x() > 0 { + if rect.x() > self.position_and_size.x { + // left boundary let boundary_x_coords = rect.x() - 1; let first_row_coordinates = self.rect_right_boundary_row_start(rect); let last_row_coordinates = self.rect_right_boundary_row_end(rect); for row in first_row_coordinates..last_row_coordinates { let coordinates = Coordinates::new(boundary_x_coords, row); - let mut symbol_to_add = if row == first_row_coordinates && row != 0 { - BoundarySymbol::new(boundary_type::TOP_LEFT).color(color) - } else if row == last_row_coordinates - 1 && row != self.rows - 1 { - BoundarySymbol::new(boundary_type::BOTTOM_LEFT).color(color) - } else { - BoundarySymbol::new(boundary_type::VERTICAL).color(color) - }; + let mut symbol_to_add = + if row == first_row_coordinates && row != self.position_and_size.y { + BoundarySymbol::new(boundary_type::TOP_LEFT).color(color) + } else if row == last_row_coordinates - 1 + && row != self.position_and_size.y + self.position_and_size.rows - 1 + { + BoundarySymbol::new(boundary_type::BOTTOM_LEFT).color(color) + } else { + BoundarySymbol::new(boundary_type::VERTICAL).color(color) + }; if rect.invisible_borders() { symbol_to_add = symbol_to_add.invisible(); } @@ -503,15 +457,19 @@ impl Boundaries { self.boundary_characters.insert(coordinates, next_symbol); } } - if rect.y() > 0 { + if rect.y() > self.position_and_size.y { + // top boundary let boundary_y_coords = rect.y() - 1; let first_col_coordinates = self.rect_bottom_boundary_col_start(rect); let last_col_coordinates = self.rect_bottom_boundary_col_end(rect); for col in first_col_coordinates..last_col_coordinates { let coordinates = Coordinates::new(col, boundary_y_coords); - let mut symbol_to_add = if col == first_col_coordinates && col != 0 { + let mut symbol_to_add = if col == first_col_coordinates + && col != self.position_and_size.x + { BoundarySymbol::new(boundary_type::TOP_LEFT).color(color) - } else if col == last_col_coordinates - 1 && col != self.columns - 1 { + } else if col == last_col_coordinates - 1 && col != self.position_and_size.cols - 1 + { BoundarySymbol::new(boundary_type::TOP_RIGHT).color(color) } else { BoundarySymbol::new(boundary_type::HORIZONTAL).color(color) @@ -528,19 +486,22 @@ impl Boundaries { } } if self.rect_right_boundary_is_before_screen_edge(rect) { - // let boundary_x_coords = self.rect_right_boundary_x_coords(rect); - let boundary_x_coords = rect.right_boundary_x_coords(); + // right boundary + let boundary_x_coords = rect.right_boundary_x_coords() - 1; let first_row_coordinates = self.rect_right_boundary_row_start(rect); let last_row_coordinates = self.rect_right_boundary_row_end(rect); for row in first_row_coordinates..last_row_coordinates { let coordinates = Coordinates::new(boundary_x_coords, row); - let mut symbol_to_add = if row == first_row_coordinates && row != 0 { - BoundarySymbol::new(boundary_type::TOP_RIGHT).color(color) - } else if row == last_row_coordinates - 1 && row != self.rows - 1 { - BoundarySymbol::new(boundary_type::BOTTOM_RIGHT).color(color) - } else { - BoundarySymbol::new(boundary_type::VERTICAL).color(color) - }; + let mut symbol_to_add = + if row == first_row_coordinates && row != self.position_and_size.y { + BoundarySymbol::new(boundary_type::TOP_RIGHT).color(color) + } else if row == last_row_coordinates - 1 + && row != self.position_and_size.y + self.position_and_size.rows - 1 + { + BoundarySymbol::new(boundary_type::BOTTOM_RIGHT).color(color) + } else { + BoundarySymbol::new(boundary_type::VERTICAL).color(color) + }; if rect.invisible_borders() { symbol_to_add = symbol_to_add.invisible(); } @@ -553,14 +514,18 @@ impl Boundaries { } } if self.rect_bottom_boundary_is_before_screen_edge(rect) { - let boundary_y_coords = rect.bottom_boundary_y_coords(); + // bottom boundary + let boundary_y_coords = rect.bottom_boundary_y_coords() - 1; let first_col_coordinates = self.rect_bottom_boundary_col_start(rect); let last_col_coordinates = self.rect_bottom_boundary_col_end(rect); for col in first_col_coordinates..last_col_coordinates { let coordinates = Coordinates::new(col, boundary_y_coords); - let mut symbol_to_add = if col == first_col_coordinates && col != 0 { + let mut symbol_to_add = if col == first_col_coordinates + && col != self.position_and_size.x + { BoundarySymbol::new(boundary_type::BOTTOM_LEFT).color(color) - } else if col == last_col_coordinates - 1 && col != self.columns - 1 { + } else if col == last_col_coordinates - 1 && col != self.position_and_size.cols - 1 + { BoundarySymbol::new(boundary_type::BOTTOM_RIGHT).color(color) } else { BoundarySymbol::new(boundary_type::HORIZONTAL).color(color) @@ -590,27 +555,20 @@ impl Boundaries { vte_output } fn rect_right_boundary_is_before_screen_edge(&self, rect: &dyn Pane) -> bool { - rect.x() + rect.columns() < self.columns + rect.x() + rect.columns() < self.position_and_size.cols } fn rect_bottom_boundary_is_before_screen_edge(&self, rect: &dyn Pane) -> bool { - rect.y() + rect.rows() < self.rows + rect.y() + rect.rows() < self.position_and_size.y + self.position_and_size.rows } fn rect_right_boundary_row_start(&self, rect: &dyn Pane) -> usize { - if rect.y() == 0 { - 0 - } else { + if rect.y() > self.position_and_size.y { rect.y() - 1 + } else { + self.position_and_size.y } } fn rect_right_boundary_row_end(&self, rect: &dyn Pane) -> usize { - let rect_bottom_row = rect.y() + rect.rows(); - // we do this because unless we're on the screen edge, we'd like to go one extra row to - // connect to whatever boundary is beneath us - if rect_bottom_row == self.rows { - rect_bottom_row - } else { - rect_bottom_row + 1 - } + rect.y() + rect.rows() } fn rect_bottom_boundary_col_start(&self, rect: &dyn Pane) -> usize { if rect.x() == 0 { @@ -620,13 +578,12 @@ impl Boundaries { } } fn rect_bottom_boundary_col_end(&self, rect: &dyn Pane) -> usize { - let rect_right_col = rect.x() + rect.columns(); - // we do this because unless we're on the screen edge, we'd like to go one extra column to - // connect to whatever boundary is right of us - if rect_right_col == self.columns { - rect_right_col - } else { - rect_right_col + 1 - } + rect.x() + rect.columns() + } + fn is_fully_inside_screen(&self, rect: &dyn Pane) -> bool { + rect.x() >= self.position_and_size.x + && rect.x() + rect.columns() <= self.position_and_size.x + self.position_and_size.cols + && rect.y() >= self.position_and_size.y + && rect.y() + rect.rows() <= self.position_and_size.y + self.position_and_size.rows } } diff --git a/zellij-server/src/ui/mod.rs b/zellij-server/src/ui/mod.rs index 0087e54e8..beb70bf04 100644 --- a/zellij-server/src/ui/mod.rs +++ b/zellij-server/src/ui/mod.rs @@ -1,3 +1,4 @@ pub mod boundaries; +pub mod pane_boundaries_frame; pub mod pane_resizer; pub mod pane_resizer_beta; diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs new file mode 100644 index 000000000..0ae454d96 --- /dev/null +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -0,0 +1,277 @@ +use crate::ui::boundaries::boundary_type; +use ansi_term::Colour::{Fixed, RGB}; +use ansi_term::Style; +use zellij_utils::pane_size::PositionAndSize; +use zellij_utils::zellij_tile::prelude::PaletteColor; + +fn color_string(character: &str, color: Option<PaletteColor>) -> String { + match color { + Some(color) => match color { + PaletteColor::Rgb((r, g, b)) => { + format!("{}", RGB(r, g, b).bold().paint(character)) + } + PaletteColor::EightBit(color) => { + format!("{}", Fixed(color).bold().paint(character)) + } + }, + None => format!("{}", Style::new().bold().paint(character)), + } +} + +pub struct PaneBoundariesFrame { + pub position_and_size: PositionAndSize, + base_title: String, + title: String, + scroll_position: (usize, usize), // (position, length) + pub color: Option<PaletteColor>, + draw_title_only: bool, + should_render: bool, +} + +impl PaneBoundariesFrame { + pub fn new(position_and_size: PositionAndSize, title: String) -> Self { + PaneBoundariesFrame { + position_and_size, + color: None, + base_title: title.clone(), + title, + scroll_position: (0, 0), + draw_title_only: false, + should_render: true, + } + } + pub fn frame_title_only(mut self) -> Self { + // TODO: remove this? + self.draw_title_only = true; + self.should_render = true; + self + } + pub fn render_only_title(&mut self, should_render_only_title: bool) { + if should_render_only_title != self.draw_title_only { + self.should_render = true; + self.draw_title_only = should_render_only_title; + } + } + pub fn change_pos_and_size(&mut self, position_and_size: PositionAndSize) { + if position_and_size != self.position_and_size { + self.position_and_size = position_and_size; + self.should_render = true; + } + } + pub fn set_color(&mut self, color: Option<PaletteColor>) { + if color != self.color { + self.color = color; + self.should_render = true; + } + } + pub fn update_scroll(&mut self, scroll_position: (usize, usize)) { + if scroll_position != self.scroll_position { + self.scroll_position = scroll_position; + self.should_render = true; + } + } + pub fn update_title(&mut self, title: Option<&String>) { + match title { + Some(title) => { + if title != &self.title { + self.title = title.clone(); + self.should_render = true; + } + } + None => { + self.title = self.base_title.clone(); + self.should_render = true; + } + } + } + pub fn content_position_and_size(&self) -> PositionAndSize { + if self.draw_title_only { + self.position_and_size.reduce_top_line() + } else { + self.position_and_size.reduce_outer_frame(1) + } + } + pub fn content_offset(&self) -> (usize, usize) { + // (column_difference, row_difference) + let content_position_and_size = self.content_position_and_size(); + let column_difference = content_position_and_size + .x + .saturating_sub(self.position_and_size.x); + let row_difference = content_position_and_size + .y + .saturating_sub(self.position_and_size.y); + (column_difference, row_difference) + } + pub fn set_should_render(&mut self, should_render: bool) { + self.should_render = should_render; + } + fn render_title_right_side(&self, max_length: usize) -> Option<String> { + if self.scroll_position.0 > 0 || self.scroll_position.1 > 0 { + let prefix = " SCROLL: "; + let full_indication = + format!(" {}/{} ", self.scroll_position.0, self.scroll_position.1); + let short_indication = format!(" {} ", self.scroll_position.0); + if prefix.chars().count() + full_indication.chars().count() <= max_length { + Some(format!("{}{}", prefix, full_indication)) + } else if full_indication.chars().count() <= max_length { + Some(full_indication) + } else if short_indication.chars().count() <= max_length { + Some(short_indication) + } else { + None + } + } else { + None + } + } + fn render_title_left_side(&self, max_length: usize) -> Option<String> { + let middle_truncated_sign = "[..]"; + let middle_truncated_sign_long = "[...]"; + let full_text = format!(" {} ", &self.title); + if max_length <= 6 { + None + } else if full_text.chars().count() <= max_length { + Some(full_text) + } else { + let length_of_each_half = (max_length - middle_truncated_sign.chars().count()) / 2; + let first_part: String = full_text.chars().take(length_of_each_half).collect(); + let second_part: String = full_text + .chars() + .skip(full_text.chars().count() - length_of_each_half) + .collect(); + let title_left_side = if first_part.chars().count() + + middle_truncated_sign.chars().count() + + second_part.chars().count() + < max_length + { + // this means we lost 1 character when dividing the total length into halves + format!( + "{}{}{}", + first_part, middle_truncated_sign_long, second_part + ) + } else { + format!("{}{}{}", first_part, middle_truncated_sign, second_part) + }; + Some(title_left_side) + } + } + fn render_title(&self, vte_output: &mut String) { + let total_title_length = self.position_and_size.cols - 2; // 2 for the left and right corners + let left_boundary = if self.draw_title_only { + boundary_type::HORIZONTAL + } else { + boundary_type::TOP_LEFT + }; + let right_boundary = if self.draw_title_only { + boundary_type::HORIZONTAL + } else { + boundary_type::TOP_RIGHT + }; + let left_side = self.render_title_left_side(total_title_length); + let right_side = left_side.as_ref().and_then(|left_side| { + let space_left = total_title_length.saturating_sub(left_side.chars().count() + 1); // 1 for a middle separator + self.render_title_right_side(space_left) + }); + let title_text = match (left_side, right_side) { + (Some(left_side), Some(right_side)) => { + let mut middle = String::new(); + for _ in + (left_side.chars().count() + right_side.chars().count())..total_title_length + { + middle.push_str(boundary_type::HORIZONTAL); + } + format!( + "{}{}{}{}{}", + left_boundary, left_side, middle, right_side, right_boundary + ) + } + (Some(left_side), None) => { + let mut middle_padding = String::new(); + for _ in left_side.chars().count()..total_title_length { + middle_padding.push_str(boundary_type::HORIZONTAL); + } + format!( + "{}{}{}{}", + left_boundary, left_side, middle_padding, right_boundary + ) + } + _ => { + let mut middle_padding = String::new(); + for _ in 0..total_title_length { + middle_padding.push_str(boundary_type::HORIZONTAL); + } + format!("{}{}{}", left_boundary, middle_padding, right_boundary) + } + }; + vte_output.push_str(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.position_and_size.y + 1, // +1 because goto is 1 indexed + self.position_and_size.x + 1, // +1 because goto is 1 indexed + color_string(&title_text, self.color), + )); // goto row/col + boundary character + } + pub fn render(&mut self) -> Option<String> { + if !self.should_render { + return None; + } + let mut vte_output = String::new(); + if self.draw_title_only { + self.render_title(&mut vte_output); + } else { + for row in + self.position_and_size.y..(self.position_and_size.y + self.position_and_size.rows) + { + if row == self.position_and_size.y { + // top row + self.render_title(&mut vte_output); + } else if row == self.position_and_size.y + self.position_and_size.rows - 1 { + // bottom row + for col in self.position_and_size.x + ..(self.position_and_size.x + self.position_and_size.cols) + { + if col == self.position_and_size.x { + // bottom left corner + vte_output.push_str(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + row + 1, // +1 because goto is 1 indexed + col + 1, + color_string(boundary_type::BOTTOM_LEFT, self.color), + )); // goto row/col + boundary character + } else if col == self.position_and_size.x + self.position_and_size.cols - 1 + { + // bottom right corner + vte_output.push_str(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + row + 1, // +1 because goto is 1 indexed + col + 1, + color_string(boundary_type::BOTTOM_RIGHT, self.color), + )); // goto row/col + boundary character + } else { + vte_output.push_str(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + row + 1, // +1 because goto is 1 indexed + col + 1, + color_string(boundary_type::HORIZONTAL, self.color), + )); // goto row/col + boundary character + } + } + } else { + vte_output.push_str(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + row + 1, // +1 because goto is 1 indexed + self.position_and_size.x + 1, + color_string(boundary_type::VERTICAL, self.color), + )); // goto row/col + boundary character + vte_output.push_str(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + row + 1, // +1 because goto is 1 indexed + self.position_and_size.x + self.position_and_size.cols, + color_string(boundary_type::VERTICAL, self.color), + )); // goto row/col + boundary character + } + } + } + self.should_render = false; + Some(vte_output) + } +} diff --git a/zellij-server/src/ui/pane_resizer.rs b/zellij-server/src/ui/pane_resizer.rs index 39f2d40c6..1b5ddabf3 100644 --- a/zellij-server/src/ui/pane_resizer.rs +++ b/zellij-server/src/ui/pane_resizer.rs @@ -112,7 +112,7 @@ impl<'a> PaneResizer<'a> { (pane.x(), pane.y(), pane.columns(), pane.rows()) }; let panes_to_pull = self.panes.values_mut().filter(|p| { - p.x() > pane_x + pane_columns + p.x() >= pane_x + pane_columns && (p.y() <= pane_y && p.y() + p.rows() >= pane_y || p.y() >= pane_y && p.y() + p.rows() <= pane_y + pane_rows) }); @@ -137,7 +137,7 @@ impl<'a> PaneResizer<'a> { (pane.x(), pane.y(), pane.columns(), pane.rows()) }; let panes_to_pull = self.panes.values_mut().filter(|p| { - p.y() > pane_y + pane_rows + p.y() >= pane_y + pane_rows && (p.x() <= pane_x && p.x() + p.columns() >= pane_x || p.x() >= pane_x && p.x() + p.columns() <= pane_x + pane_columns) }); @@ -162,7 +162,7 @@ impl<'a> PaneResizer<'a> { (pane.x(), pane.y(), pane.columns(), pane.rows()) }; let panes_to_push = self.panes.values_mut().filter(|p| { - p.y() > pane_y + pane_rows + p.y() >= pane_y + pane_rows && (p.x() <= pane_x && p.x() + p.columns() >= pane_x || p.x() >= pane_x && p.x() + p.columns() <= pane_x + pane_columns) }); @@ -187,7 +187,7 @@ impl<'a> PaneResizer<'a> { (pane.x(), pane.y(), pane.columns(), pane.rows()) }; let panes_to_push = self.panes.values_mut().filter(|p| { - p.x() > pane_x + pane_columns + p.x() >= pane_x + pane_columns && (p.y() <= pane_y && p.y() + p.rows() >= pane_y || p.y() >= pane_y && p.y() + p.rows() <= pane_y + pane_rows) }); @@ -204,32 +204,44 @@ impl<'a> PaneResizer<'a> { let pane = sel |