diff options
author | Aram Drevekenin <aram@poor.dev> | 2021-11-25 16:21:59 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-25 16:21:59 +0100 |
commit | 6c6a4393f403359838ea9b32b09520de4f50c019 (patch) | |
tree | d1b553cab1a22c271be99bbc35a5e608bc96676d /zellij-server/src/ui | |
parent | 9fb2c7ca168679905d79dcdaf686d40f4a6dc958 (diff) |
This adds a UI for multiple users in panes (behind a feature flag) (#897)
* feat(ui): multiple users in panes
* style(fmt): make rustfmt happy
* style(fmt): make clippy happy
Diffstat (limited to 'zellij-server/src/ui')
-rw-r--r-- | zellij-server/src/ui/boundaries.rs | 11 | ||||
-rw-r--r-- | zellij-server/src/ui/mod.rs | 1 | ||||
-rw-r--r-- | zellij-server/src/ui/pane_boundaries_frame.rs | 491 | ||||
-rw-r--r-- | zellij-server/src/ui/pane_contents_and_ui.rs | 165 |
4 files changed, 610 insertions, 58 deletions
diff --git a/zellij-server/src/ui/boundaries.rs b/zellij-server/src/ui/boundaries.rs index a912664ca..317c2e5dc 100644 --- a/zellij-server/src/ui/boundaries.rs +++ b/zellij-server/src/ui/boundaries.rs @@ -3,7 +3,7 @@ use zellij_utils::{pane_size::Viewport, zellij_tile}; use crate::tab::Pane; use ansi_term::Colour::{Fixed, RGB}; use std::collections::HashMap; -use zellij_tile::data::{InputMode, Palette, PaletteColor}; +use zellij_tile::data::PaletteColor; use zellij_utils::shared::colors; use std::fmt::{Display, Error, Formatter}; @@ -413,17 +413,10 @@ impl Boundaries { boundary_characters: HashMap::new(), } } - pub fn add_rect(&mut self, rect: &dyn Pane, input_mode: InputMode, palette: Option<Palette>) { + pub fn add_rect(&mut self, rect: &dyn Pane, color: Option<PaletteColor>) { 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), - _ => Some(palette.unwrap().orange), - }, - false => None, - }; if rect.x() > self.viewport.x { // left boundary let boundary_x_coords = rect.x() - 1; diff --git a/zellij-server/src/ui/mod.rs b/zellij-server/src/ui/mod.rs index 08c381836..1bf630712 100644 --- a/zellij-server/src/ui/mod.rs +++ b/zellij-server/src/ui/mod.rs @@ -1,4 +1,5 @@ pub mod boundaries; pub mod overlay; pub mod pane_boundaries_frame; +pub mod pane_contents_and_ui; pub mod pane_resizer; diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs index a283a6b3b..5ab381582 100644 --- a/zellij-server/src/ui/pane_boundaries_frame.rs +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -1,8 +1,9 @@ use crate::ui::boundaries::boundary_type; +use crate::ClientId; use ansi_term::Colour::{Fixed, RGB}; use ansi_term::Style; use zellij_utils::pane_size::Viewport; -use zellij_utils::zellij_tile::prelude::PaletteColor; +use zellij_utils::zellij_tile::prelude::{Palette, PaletteColor}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; @@ -20,27 +21,104 @@ fn color_string(character: &str, color: Option<PaletteColor>) -> String { } } +fn background_color(character: &str, color: Option<PaletteColor>) -> String { + match color { + Some(PaletteColor::Rgb((r, g, b))) => { + format!("{}", Style::new().on(RGB(r, g, b)).paint(character)) + } + Some(PaletteColor::EightBit(color)) => { + format!("{}", Style::new().on(Fixed(color)).paint(character)) + } + None => String::from(character), + } +} + +// TODO: move elsewhere +pub(crate) fn client_id_to_colors( + client_id: ClientId, + colors: Palette, +) -> Option<(PaletteColor, PaletteColor)> { + // (primary color, secondary color) + match client_id { + 1 => Some((colors.green, colors.black)), + 2 => Some((colors.blue, colors.black)), + 3 => Some((colors.cyan, colors.black)), + 4 => Some((colors.magenta, colors.black)), + 5 => Some((colors.yellow, colors.black)), + _ => None, + } +} + +pub struct FrameParams { + pub focused_client: Option<ClientId>, + pub is_main_client: bool, + pub other_focused_clients: Vec<ClientId>, + pub colors: Palette, + pub color: Option<PaletteColor>, + pub other_cursors_exist_in_session: bool, +} + #[derive(Default, PartialEq)] pub struct PaneFrame { pub geom: Viewport, pub title: String, pub scroll_position: (usize, usize), // (position, length) + pub colors: Palette, pub color: Option<PaletteColor>, + pub focused_client: Option<ClientId>, + pub is_main_client: bool, + pub other_cursors_exist_in_session: bool, + pub other_focused_clients: Vec<ClientId>, } impl PaneFrame { - fn render_title_right_side(&self, max_length: usize) -> Option<String> { + pub fn new( + geom: Viewport, + scroll_position: (usize, usize), + main_title: String, + frame_params: FrameParams, + ) -> Self { + PaneFrame { + geom, + title: main_title, + scroll_position, + colors: frame_params.colors, + color: frame_params.color, + focused_client: frame_params.focused_client, + is_main_client: frame_params.is_main_client, + other_focused_clients: frame_params.other_focused_clients, + other_cursors_exist_in_session: frame_params.other_cursors_exist_in_session, + } + } + fn client_cursor(&self, client_id: ClientId) -> String { + let color = client_id_to_colors(client_id, self.colors); + background_color(" ", color.map(|c| c.0)) + } + fn render_title_right_side(&self, max_length: usize) -> Option<(String, usize)> { + // string and length because of color 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.width() + full_indication.width() <= max_length { - Some(format!("{}{}", prefix, full_indication)) - } else if full_indication.width() <= max_length { - Some(full_indication) - } else if short_indication.width() <= max_length { - Some(short_indication) + let full_indication_len = full_indication.chars().count(); + let short_indication_len = short_indication.chars().count(); + let prefix_len = prefix.chars().count(); + if prefix_len + full_indication_len <= max_length { + Some(( + color_string(&format!("{}{}", prefix, full_indication), self.color), + prefix_len + full_indication_len, + )) + } else if full_indication_len <= max_length { + Some(( + color_string(&full_indication, self.color), + full_indication_len, + )) + } else if short_indication_len <= max_length { + Some(( + color_string(&short_indication, self.color), + short_indication_len, + )) } else { None } @@ -48,14 +126,148 @@ impl PaneFrame { None } } - fn render_title_left_side(&self, max_length: usize) -> Option<String> { + fn render_my_focus(&self, max_length: usize) -> Option<(String, usize)> { + let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color); + let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color); + let full_indication_text = "MY FOCUS"; + let full_indication = format!( + "{} {} {}", + left_separator, + color_string(full_indication_text, self.color), + right_separator + ); + let full_indication_len = full_indication_text.width() + 4; // 2 for separators 2 for padding + let short_indication_text = "ME"; + let short_indication = format!( + "{} {} {}", + left_separator, + color_string(short_indication_text, self.color), + right_separator + ); + let short_indication_len = short_indication_text.width() + 4; // 2 for separators 2 for padding + if full_indication_len <= max_length { + Some((full_indication, full_indication_len)) + } else if short_indication_len <= max_length { + Some((short_indication, short_indication_len)) + } else { + None + } + } + fn render_my_and_others_focus(&self, max_length: usize) -> Option<(String, usize)> { + let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color); + let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color); + let full_indication_text = "MY FOCUS AND:"; + let short_indication_text = "+"; + let mut full_indication = color_string(full_indication_text, self.color); + let mut full_indication_len = full_indication_text.width(); + let mut short_indication = color_string(short_indication_text, self.color); + let mut short_indication_len = short_indication_text.width(); + for client_id in &self.other_focused_clients { + let text = format!(" {}", self.client_cursor(*client_id)); + full_indication_len += 2; + full_indication.push_str(&text); + short_indication_len += 2; + short_indication.push_str(&text); + } + if full_indication_len + 4 <= max_length { + // 2 for separators, 2 for padding + Some(( + format!("{} {} {}", left_separator, full_indication, right_separator), + full_indication_len + 4, + )) + } else if short_indication_len + 4 <= max_length { + // 2 for separators, 2 for padding + Some(( + format!( + "{} {} {}", + left_separator, short_indication, right_separator + ), + short_indication_len + 4, + )) + } else { + None + } + } + fn render_other_focused_users(&self, max_length: usize) -> Option<(String, usize)> { + let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color); + let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color); + let full_indication_text = if self.other_focused_clients.len() == 1 { + "FOCUSED USER:" + } else { + "FOCUSED USERS:" + }; + let middle_indication_text = "U:"; + let mut full_indication = color_string(full_indication_text, self.color); + let mut full_indication_len = full_indication_text.width(); + let mut middle_indication = color_string(middle_indication_text, self.color); + let mut middle_indication_len = middle_indication_text.width(); + let mut short_indication = String::from(""); + let mut short_indication_len = 0; + for client_id in &self.other_focused_clients { + let text = format!(" {}", self.client_cursor(*client_id)); + full_indication_len += 2; + full_indication.push_str(&text); + middle_indication_len += 2; + middle_indication.push_str(&text); + short_indication_len += 2; + short_indication.push_str(&text); + } + if full_indication_len + 4 <= max_length { + // 2 for separators, 2 for padding + Some(( + format!("{} {} {}", left_separator, full_indication, right_separator), + full_indication_len + 4, + )) + } else if middle_indication_len + 4 <= max_length { + // 2 for separators, 2 for padding + Some(( + format!( + "{} {} {}", + left_separator, middle_indication, right_separator + ), + middle_indication_len + 4, + )) + } else if short_indication_len + 3 <= max_length { + // 2 for separators, 1 for padding + Some(( + format!("{}{} {}", left_separator, short_indication, right_separator), + short_indication_len + 3, + )) + } else { + None + } + } + fn render_title_middle(&self, max_length: usize) -> Option<(String, usize)> { + // string and length because of color + if self.is_main_client + && self.other_focused_clients.is_empty() + && !self.other_cursors_exist_in_session + { + None + } else if self.is_main_client + && self.other_focused_clients.is_empty() + && self.other_cursors_exist_in_session + { + self.render_my_focus(max_length) + } else if self.is_main_client && !self.other_focused_clients.is_empty() { + self.render_my_and_others_focus(max_length) + } else if !self.other_focused_clients.is_empty() { + self.render_other_focused_users(max_length) + } else { + None + } + } + fn render_title_left_side(&self, max_length: usize) -> Option<(String, usize)> { let middle_truncated_sign = "[..]"; let middle_truncated_sign_long = "[...]"; let full_text = format!(" {} ", &self.title); if max_length <= 6 || self.title.is_empty() { None } else if full_text.width() <= max_length { - Some(full_text) + Some(( + color_string(&full_text, self.color), + full_text.chars().count(), + )) } else { let length_of_each_half = (max_length - middle_truncated_sign.width()) / 2; @@ -89,53 +301,234 @@ impl PaneFrame { } else { format!("{}{}{}", first_part, middle_truncated_sign, second_part) }; - Some(title_left_side) + Some(( + color_string(&title_left_side, self.color), + title_left_side.chars().count(), + )) } } - fn render_title(&self, vte_output: &mut String) { + fn three_part_title_line( + &self, + left_side: &str, + left_side_len: &usize, + middle: &str, + middle_len: &usize, + right_side: &str, + right_side_len: &usize, + ) -> String { + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut title_line = String::new(); + let left_side_start_position = self.geom.x + 1; + let middle_start_position = self.geom.x + (total_title_length / 2) - (middle_len / 2) + 1; + let right_side_start_position = + (self.geom.x + self.geom.cols - 1).saturating_sub(*right_side_len); + + let mut col = self.geom.x; + loop { + if col == self.geom.x { + title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color)); + } else if col == self.geom.x + self.geom.cols - 1 { + title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color)); + } else if col == left_side_start_position { + title_line.push_str(left_side); + col += left_side_len; + continue; + } else if col == middle_start_position { + title_line.push_str(middle); + col += middle_len; + continue; + } else if col == right_side_start_position { + title_line.push_str(right_side); + col += right_side_len; + continue; + } else { + title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color)); + // TODO: BETTER + } + if col == self.geom.x + self.geom.cols - 1 { + break; + } + col += 1; + } + title_line + } + fn left_and_middle_title_line( + &self, + left_side: &str, + left_side_len: &usize, + middle: &str, + middle_len: &usize, + ) -> String { + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut title_line = String::new(); + let left_side_start_position = self.geom.x + 1; + let middle_start_position = self.geom.x + (total_title_length / 2) - (*middle_len / 2) + 1; + + let mut col = self.geom.x; + loop { + if col == self.geom.x { + title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color)); + } else if col == self.geom.x + self.geom.cols - 1 { + title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color)); + } else if col == left_side_start_position { + title_line.push_str(left_side); + col += *left_side_len; + continue; + } else if col == middle_start_position { + title_line.push_str(middle); + col += *middle_len; + continue; + } else { + title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color)); + // TODO: BETTER + } + if col == self.geom.x + self.geom.cols - 1 { + break; + } + col += 1; + } + title_line + } + fn middle_only_title_line(&self, middle: &str, middle_len: &usize) -> String { + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut title_line = String::new(); + let middle_start_position = self.geom.x + (total_title_length / 2) - (*middle_len / 2) + 1; + + let mut col = self.geom.x; + loop { + if col == self.geom.x { + title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color)); + } else if col == self.geom.x + self.geom.cols - 1 { + title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color)); + } else if col == middle_start_position { + title_line.push_str(middle); + col += *middle_len; + continue; + } else { + title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color)); + // TODO: BETTER + } + if col == self.geom.x + self.geom.cols - 1 { + break; + } + col += 1; + } + title_line + } + fn two_part_title_line( + &self, + left_side: &str, + left_side_len: &usize, + right_side: &str, + right_side_len: &usize, + ) -> String { + let left_boundary = color_string(boundary_type::TOP_LEFT, self.color); + let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color); + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut middle = String::new(); + for _ in (left_side_len + right_side_len)..total_title_length { + middle.push_str(boundary_type::HORIZONTAL); + } + format!( + "{}{}{}{}{}", + left_boundary, + left_side, + color_string(&middle, self.color), + color_string(right_side, self.color), + &right_boundary + ) + } + fn left_only_title_line(&self, left_side: &str, left_side_len: &usize) -> String { + let left_boundary = color_string(boundary_type::TOP_LEFT, self.color); + let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color); + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut middle_padding = String::new(); + for _ in *left_side_len..total_title_length { + middle_padding.push_str(boundary_type::HORIZONTAL); + } + format!( + "{}{}{}{}", + left_boundary, + left_side, + color_string(&middle_padding, self.color), + &right_boundary + ) + } + fn empty_title_line(&self) -> String { + let left_boundary = color_string(boundary_type::TOP_LEFT, self.color); + let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color); + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut middle_padding = String::new(); + for _ in 0..total_title_length { + middle_padding.push_str(boundary_type::HORIZONTAL); + } + format!( + "{}{}{}", + left_boundary, + color_string(&middle_padding, self.color), + right_boundary + ) + } + fn title_line_with_middle(&self, middle: &str, middle_len: &usize) -> String { + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let length_of_each_side = total_title_length.saturating_sub(*middle_len + 2) / 2; + let mut left_side = self.render_title_left_side(length_of_each_side); + let mut right_side = self.render_title_right_side(length_of_each_side); + + match (left_side.as_mut(), right_side.as_mut()) { + (Some((left_side, left_side_len)), Some((right_side, right_side_len))) => self + .three_part_title_line( + left_side, + left_side_len, + middle, + middle_len, + right_side, + right_side_len, + ), + (Some((left_side, left_side_len)), None) => { + self.left_and_middle_title_line(left_side, left_side_len, middle, middle_len) + } + _ => self.middle_only_title_line(middle, middle_len), + } + } + fn title_line_without_middle(&self) -> String { let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners - let left_boundary = boundary_type::TOP_LEFT; - let right_boundary = 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.width() + 1); // 1 for a middle separator + let right_side = left_side.as_ref().and_then(|(_left_side, left_side_len)| { + let space_left = total_title_length.saturating_sub(*left_side_len + 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.width() + right_side.width())..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.width()..total_title_length { - middle_padding.push_str(boundary_type::HORIZONTAL); - } - format!( - "{}{}{}{}", - left_boundary, left_side, middle_padding, right_boundary - ) + match (left_side, right_side) { + (Some((left_side, left_side_len)), Some((right_side, right_side_len))) => { + self.two_part_title_line(&left_side, &left_side_len, &right_side, &right_side_len) } - _ => { - 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) + (Some((left_side, left_side_len)), None) => { + self.left_only_title_line(&left_side, &left_side_len) } - }; - vte_output.push_str(&format!( - "\u{1b}[{};{}H\u{1b}[m{}", - self.geom.y + 1, // +1 because goto is 1 indexed - self.geom.x + 1, // +1 because goto is 1 indexed - color_string(&title_text, self.color), - )); // goto row/col + boundary character + _ => self.empty_title_line(), + } + } + fn render_title(&self, vte_output: &mut String) { + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + + if let Some((middle, middle_length)) = self.render_title_middle(total_title_length).as_mut() + { + let title_text = self.title_line_with_middle(middle, middle_length); + vte_output.push_str(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.geom.y + 1, // +1 because goto is 1 indexed + self.geom.x + 1, // +1 because goto is 1 indexed + color_string(&title_text, self.color), + )); // goto row/col + boundary character + } else { + let title_text = self.title_line_without_middle(); + vte_output.push_str(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.geom.y + 1, // +1 because goto is 1 indexed + self.geom.x + 1, // +1 because goto is 1 indexed + color_string(&title_text, self.color), + )); // goto row/col + boundary character + } } pub fn render(&self) -> String { let mut vte_output = String::new(); diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs new file mode 100644 index 000000000..4f700f5dc --- /dev/null +++ b/zellij-server/src/ui/pane_contents_and_ui.rs @@ -0,0 +1,165 @@ +use crate::panes::PaneId; +use crate::tab::{Output, Pane}; +use crate::ui::boundaries::Boundaries; +use crate::ui::pane_boundaries_frame::client_id_to_colors; +use crate::ui::pane_boundaries_frame::FrameParams; +use crate::ClientId; +use std::collections::HashMap; +use zellij_tile::data::{InputMode, Palette, PaletteColor}; + +pub struct PaneContentsAndUi<'a> { + pane: &'a mut Box<dyn Pane>, + output: &'a mut Output, + colors: Palette, + focused_clients: Vec<ClientId>, + multiple_users_exist_in_session: bool, + mode: InputMode, // TODO: per client +} + +impl<'a> PaneContentsAndUi<'a> { + pub fn new( + pane: &'a mut Box<dyn Pane>, + output: &'a mut Output, + colors: Palette, + active_panes: &HashMap<ClientId, PaneId>, + mode: InputMode, + ) -> Self { + let focused_clients: Vec<ClientId> = active_panes + .iter() + .filter(|(_c_id, p_id)| **p_id == pane.pid()) + .map(|(c_id, _p_id)| *c_id) + .collect(); + let multiple_users_exist_in_session = active_panes.len() > 1; + PaneContentsAndUi { + pane, + output, + colors, + focused_clients, + multiple_users_exist_in_session, + |