zellij/default-plugins/multiple-select/src/ui.rs
Aram Drevekenin 27c8986939
feat: multiple Select and Bulk Pane Actions (#4169)
* initial implementation with break panes to new tab

* break pane group left/right

* group embed/eject panes

* stack pane group on resize

* close pane group

* style(fmt): rustfmt

* fix tests

* group drag and ungroup with the mouse

* fix mouse hover for multiple clients

* fix for multiple clients

* multiple select plugin initial

* use real data in plugin

* adjust functionality

* fix some ux issues

* reflect group mouse group selections in plugin

* group/ungroup panes in Zellij

* highlight frames when marked by the plugin

* refactor: render function in plugin

* some ui responsiveness

* some more responsiveness and adjust hover text

* break out functionality

* stack functionality

* break panes left/right and close multiple panes

* fix(tab): only relayout the relevant layout when non-focused pane is closed

* status bar UI

* embed and float panes

* work

* fix some ui/ux issues

* refactor: move stuff around

* some responsiveness and fix search result browsing bug

* change plugin pane title

* differentiate group from focused pane

* add keyboard shortcut

* add ui to compact bar

* make boundary colors appear properly without pane frames

* get plugins to also display their frame color

* make hover shortcuts appear on command panes

* fix: do not render search string component if it's empty

* BeforeClose Event and unhighlight panes on exit

* some UI/UX fixes

* some fixes to the catppuccin-latte theme

* remove ungroup shortcut

* make some ui components opaque

* fix more opaque elements

* fix some issues with stacking pane order

* keyboard shortcuts for grouping

* config to opt out of advanced mouse actions

* make selected + focused frame color distinct

* group marking mode

* refactor: multiple-select plugin

* adjust stacking group behavior

* adjust flashing periods

* render common modifier in group controls

* add to compact bar

* adjust key hint wording

* add key to presets and default config

* some cleanups

* some refactoring

* fix tests

* fix plugin system tests

* tests: group/ungroup/hover

* test: BeforeClose plugin event

* new plugin assets

* style(fmt): rustfmt

* remove warnings

* tests: give plugin more time to load
2025-04-29 20:52:17 +02:00

873 lines
33 KiB
Rust

use crate::{App, VisibilityAndFocus};
use zellij_tile::prelude::*;
const TOP_LEFT_CORNER_CHARACTER: &'static str = "";
const TOP_RIGHT_CORNER_CHARACTER: &'static str = "";
const BOTTOM_LEFT_CORNER_CHARACTER: &'static str = "";
const BOTTOM_RIGHT_CORNER_CHARACTER: &'static str = "";
const BOUNDARY_CHARACTER: &'static str = "";
const HORIZONTAL_BOUNDARY_CHARACTER: &'static str = "";
#[derive(Debug, Clone)]
pub struct PaneItem {
pub text: String,
pub id: PaneId,
pub color_indices: Vec<usize>,
}
impl PaneItem {
pub fn clear(&mut self) {
self.color_indices.clear();
}
pub fn render(&self, max_width_for_item: usize) -> NestedListItem {
let pane_item_text_len = self.text.chars().count();
if pane_item_text_len <= max_width_for_item {
NestedListItem::new(&self.text)
.color_range(0, ..)
.color_indices(3, self.color_indices.iter().copied().collect())
} else {
let length_of_each_half = max_width_for_item.saturating_sub(3) / 2;
let first_half: String = self.text.chars().take(length_of_each_half).collect();
let second_half: String = self
.text
.chars()
.rev()
.take(length_of_each_half)
.collect::<Vec<_>>()
.iter()
.rev()
.collect();
let second_half_start_index = pane_item_text_len.saturating_sub(length_of_each_half);
let adjusted_indices: Vec<usize> = self
.color_indices
.iter()
.filter_map(|i| {
if i < &length_of_each_half {
Some(*i)
} else if i >= &second_half_start_index {
Some(i.saturating_sub(second_half_start_index) + length_of_each_half + 3)
//3 for the bulletin
} else {
None
}
})
.collect();
NestedListItem::new(format!("{}...{}", first_half, second_half))
.color_range(0, ..)
.color_indices(3, adjusted_indices)
}
}
}
// rendering code
impl App {
pub fn render_close_shortcut(&self, cols: usize) {
let should_render_close_shortcut =
self.visibility_and_focus.left_side_is_focused() && self.marked_index.is_none();
if should_render_close_shortcut {
let x_coordinates_right_padding =
if self.visibility_and_focus.only_left_side_is_focused() {
5
} else {
1
};
let ctrl_c_shortcut_text = "<Ctrl c> - Close";
let ctrl_c_shortcut = Text::new(ctrl_c_shortcut_text).color_range(3, ..=7);
print_text_with_coordinates(
ctrl_c_shortcut,
cols.saturating_sub(ctrl_c_shortcut_text.chars().count())
.saturating_sub(x_coordinates_right_padding),
0,
None,
None,
);
}
}
pub fn render_tab_shortcut(&self, cols: usize, rows: usize) {
match self.visibility_and_focus {
VisibilityAndFocus::BothSidesVisibleRightSideFocused => {
let side_width = self.calculate_side_width(cols);
let tab_shortcut = Text::new("<TAB> - select more panes").color_range(3, ..=4);
print_text_with_coordinates(
tab_shortcut,
side_width + 6,
rows.saturating_sub(2),
None,
None,
);
},
VisibilityAndFocus::BothSidesVisibleLeftSideFocused => {
let side_width = self.calculate_side_width(cols);
let tab_shortcut_text = "<TAB> - browse selected panes";
let tab_shortcut = Text::new(tab_shortcut_text).color_range(3, ..=4);
print_text_with_coordinates(
tab_shortcut,
side_width.saturating_sub(tab_shortcut_text.chars().count() + 1),
rows.saturating_sub(2),
None,
None,
);
},
VisibilityAndFocus::OnlyRightSideVisible => {
let tab_shortcut = Text::new("<TAB> - select more panes").color_range(3, ..=4);
print_text_with_coordinates(tab_shortcut, 4, rows.saturating_sub(2), None, None);
},
VisibilityAndFocus::OnlyLeftSideVisible => {
// not visible
},
};
}
pub fn render_left_side(&self, rows: usize, cols: usize, is_focused: bool) {
let title_y = 0;
let left_side_base_x = 1;
let list_y = 2;
let side_width = self.calculate_side_width(cols);
let max_left_list_height = rows.saturating_sub(8);
let (
extra_pane_count_on_top_left,
extra_pane_count_on_bottom_left,
extra_selected_item_count_on_top_left,
extra_selected_item_count_on_bottom_left,
left_side_panes,
) = self.left_side_panes_list(side_width, max_left_list_height);
let (filter_prompt_text, filter_prompt) = self.filter_panes_prompt();
let filter = self.filter(side_width.saturating_sub(filter_prompt_text.chars().count() + 1));
let (
_enter_select_panes_text,
enter_select_panes,
space_shortcut_text,
space_shortcut,
_escape_shortcut_text,
escape_shortcut,
) = self.left_side_controls(side_width);
print_text_with_coordinates(filter_prompt, left_side_base_x, title_y, None, None);
if is_focused {
print_text_with_coordinates(
filter,
left_side_base_x + filter_prompt_text.chars().count(),
title_y,
None,
None,
);
}
print_nested_list_with_coordinates(
left_side_panes.clone(),
left_side_base_x,
list_y,
Some(side_width),
None,
);
if is_focused {
if let Some(marked_index) = self.marked_index.as_ref().map(|i| i.main_index) {
print_text_with_coordinates(
Text::new(">").color_range(3, ..).selected(),
left_side_base_x + 1,
(list_y + marked_index).saturating_sub(extra_pane_count_on_top_left),
None,
None,
);
}
}
if extra_pane_count_on_top_left > 0 {
self.print_extra_pane_count(
extra_pane_count_on_top_left,
extra_selected_item_count_on_top_left,
list_y.saturating_sub(1),
left_side_base_x,
side_width,
);
}
if extra_pane_count_on_bottom_left > 0 {
self.print_extra_pane_count(
extra_pane_count_on_bottom_left,
extra_selected_item_count_on_bottom_left,
list_y + left_side_panes.len(),
left_side_base_x,
side_width,
);
}
if is_focused && !left_side_panes.is_empty() {
let controls_x = 1;
print_text_with_coordinates(
enter_select_panes,
controls_x,
list_y + left_side_panes.len() + 1,
None,
None,
);
if self.marked_index.is_some() {
print_text_with_coordinates(
space_shortcut.clone(),
controls_x,
list_y + left_side_panes.len() + 2,
None,
None,
);
print_text_with_coordinates(
escape_shortcut.clone(),
controls_x + space_shortcut_text.chars().count() + 1,
list_y + left_side_panes.len() + 2,
None,
None,
);
}
}
}
pub fn render_right_side(&self, rows: usize, cols: usize, is_focused: bool) {
let side_width = self.calculate_side_width(cols);
let right_side_base_x = match self.visibility_and_focus {
VisibilityAndFocus::OnlyLeftSideVisible | VisibilityAndFocus::OnlyRightSideVisible => 1,
VisibilityAndFocus::BothSidesVisibleLeftSideFocused
| VisibilityAndFocus::BothSidesVisibleRightSideFocused => side_width + 4,
};
let title_y = 0;
let list_y: usize = 2;
let max_right_list_height = rows.saturating_sub(11);
let selected_prompt = self.selected_panes_title();
let (
extra_pane_count_on_top_right,
extra_pane_count_on_bottom_right,
extra_selected_item_count_on_top_right,
extra_selected_item_count_on_bottom_right,
right_side_panes,
) = self.right_side_panes_list(side_width, max_right_list_height);
let right_side_pane_count = right_side_panes.len();
let (
right_side_controls_1,
right_side_controls_2,
right_side_controls_3,
right_side_controls_4,
) = self.right_side_controls(side_width);
if extra_pane_count_on_top_right > 0 {
self.print_extra_pane_count(
extra_pane_count_on_top_right,
extra_selected_item_count_on_top_right,
list_y.saturating_sub(1),
right_side_base_x,
side_width,
);
}
if extra_pane_count_on_bottom_right > 0 {
self.print_extra_pane_count(
extra_pane_count_on_bottom_right,
extra_selected_item_count_on_bottom_right,
list_y + right_side_panes.len(),
right_side_base_x,
side_width,
);
}
print_text_with_coordinates(selected_prompt, right_side_base_x + 3, title_y, None, None);
print_nested_list_with_coordinates(
right_side_panes,
right_side_base_x,
list_y,
Some(side_width),
None,
);
if is_focused {
if let Some(marked_index) = self.marked_index.as_ref().map(|i| i.main_index) {
print_text_with_coordinates(
Text::new(">").color_range(3, ..).selected(),
right_side_base_x + 1,
(list_y + marked_index).saturating_sub(extra_pane_count_on_top_right),
None,
None,
);
}
}
if is_focused && !self.right_side_panes.is_empty() {
print_text_with_coordinates(
right_side_controls_1,
right_side_base_x + 1,
list_y + right_side_pane_count + 1,
None,
None,
);
print_text_with_coordinates(
right_side_controls_2,
right_side_base_x + 1,
list_y + right_side_pane_count + 3,
None,
None,
);
print_text_with_coordinates(
right_side_controls_3,
right_side_base_x + 1,
list_y + right_side_pane_count + 4,
None,
None,
);
print_text_with_coordinates(
right_side_controls_4,
right_side_base_x + 1,
list_y + right_side_pane_count + 5,
None,
None,
);
}
}
pub fn render_help_line(&self, rows: usize, cols: usize) {
let help_line_text = match self.visibility_and_focus {
VisibilityAndFocus::OnlyLeftSideVisible => {
let full_help_line = "Help: Select one or more panes to group for bulk operations";
let short_help_line = "Help: Select panes to group";
if cols >= full_help_line.chars().count() {
full_help_line
} else {
short_help_line
}
},
VisibilityAndFocus::OnlyRightSideVisible => {
let full_help_line = "Help: Perform bulk operations on all selected panes";
let short_help_line = "Help: Perform bulk operations";
if cols >= full_help_line.chars().count() {
full_help_line
} else {
short_help_line
}
},
_ => {
let full_help_line =
"Help: Select panes on the left, then perform operations on the right.";
let short_help_line = "Help: Group panes for bulk operations";
if cols >= full_help_line.chars().count() {
full_help_line
} else {
short_help_line
}
},
};
let help_line = Text::new(help_line_text);
print_text_with_coordinates(help_line, 0, rows, None, None);
}
pub fn render_focus_boundary(&self, rows: usize, cols: usize) {
let side_width = self.calculate_side_width(cols);
let x = match self.visibility_and_focus {
VisibilityAndFocus::OnlyRightSideVisible => 0,
VisibilityAndFocus::BothSidesVisibleLeftSideFocused
| VisibilityAndFocus::BothSidesVisibleRightSideFocused
| VisibilityAndFocus::OnlyLeftSideVisible => side_width + 2,
};
let y = 0;
let height = rows.saturating_sub(2);
for i in y..=height {
if i == y && self.visibility_and_focus.left_side_is_focused() {
print_text_with_coordinates(
Text::new(TOP_RIGHT_CORNER_CHARACTER),
x,
i,
None,
None,
);
print_text_with_coordinates(
Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
x.saturating_sub(1),
i,
None,
None,
);
print_text_with_coordinates(
Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
x.saturating_sub(2),
i,
None,
None,
);
} else if i == y && !self.visibility_and_focus.left_side_is_focused() {
print_text_with_coordinates(Text::new(TOP_LEFT_CORNER_CHARACTER), x, i, None, None);
print_text_with_coordinates(
Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
x + 1,
i,
None,
None,
);
print_text_with_coordinates(
Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
x + 2,
i,
None,
None,
);
} else if i == height && self.visibility_and_focus.left_side_is_focused() {
print_text_with_coordinates(
Text::new(BOTTOM_RIGHT_CORNER_CHARACTER),
x,
i,
None,
None,
);
print_text_with_coordinates(
Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
x.saturating_sub(1),
i,
None,
None,
);
print_text_with_coordinates(
Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
x.saturating_sub(2),
i,
None,
None,
);
} else if i == height && !self.visibility_and_focus.left_side_is_focused() {
print_text_with_coordinates(
Text::new(BOTTOM_LEFT_CORNER_CHARACTER),
x,
i,
None,
None,
);
print_text_with_coordinates(
Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
x + 1,
i,
None,
None,
);
print_text_with_coordinates(
Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
x + 2,
i,
None,
None,
);
} else {
print_text_with_coordinates(Text::new(BOUNDARY_CHARACTER), x, i, None, None);
}
}
}
pub fn calculate_side_width(&self, cols: usize) -> usize {
match self.visibility_and_focus {
VisibilityAndFocus::OnlyLeftSideVisible | VisibilityAndFocus::OnlyRightSideVisible => {
cols.saturating_sub(4)
},
VisibilityAndFocus::BothSidesVisibleLeftSideFocused
| VisibilityAndFocus::BothSidesVisibleRightSideFocused => (cols / 2).saturating_sub(3),
}
}
}
// ui components
impl App {
fn filter_panes_prompt(&self) -> (&'static str, Text) {
let search_prompt_text = if self.search_string.is_empty() {
"ALL PANES "
} else {
"FILTER: "
};
let search_prompt = if self.visibility_and_focus.left_side_is_focused() {
Text::new(&search_prompt_text).color_range(2, ..)
} else {
Text::new(&search_prompt_text)
};
(search_prompt_text, search_prompt)
}
fn filter(&self, max_width: usize) -> Text {
let search_string_text = if self.marked_index.is_none() && self.search_string.is_empty() {
let full = "[Type filter term...]";
let short = "[...]";
if max_width >= full.chars().count() {
full.to_owned()
} else {
short.to_owned()
}
} else if self.marked_index.is_none() && !self.search_string.is_empty() {
if max_width >= self.search_string.chars().count() + 1 {
format!("{}_", self.search_string)
} else {
let truncated: String = self
.search_string
.chars()
.rev()
.take(max_width.saturating_sub(4))
.collect::<Vec<_>>()
.iter()
.rev()
.collect();
format!("...{}_", truncated)
}
} else if self.marked_index.is_some() && !self.search_string.is_empty() {
if max_width >= self.search_string.chars().count() {
format!("{}", self.search_string)
} else {
let truncated: String = self
.search_string
.chars()
.rev()
.take(max_width.saturating_sub(4))
.collect::<Vec<_>>()
.iter()
.rev()
.collect();
format!("...{}", truncated)
}
} else {
format!("")
};
Text::new(&search_string_text).color_range(3, ..)
}
fn left_side_panes_list(
&self,
max_width: usize,
max_list_height: usize,
) -> (usize, usize, usize, usize, Vec<NestedListItem>) {
// returns: extra_pane_count_on_top, extra_pane_count_on_bottom,
// extra_selected_item_count_on_top, extra_selected_item_count_on_bottom, list
let mut left_side_panes = vec![];
let pane_items_on_the_left = self
.search_results
.as_ref()
.unwrap_or_else(|| &self.left_side_panes);
let max_width_for_item = max_width.saturating_sub(3); // 3 for the list bulletin
let item_count = pane_items_on_the_left.iter().count();
let first_item_index = if self.visibility_and_focus.left_side_is_focused() {
self.marked_index
.as_ref()
.map(|s| s.main_index.saturating_sub(max_list_height / 2))
.unwrap_or(0)
} else {
0
};
let last_item_index = std::cmp::min(
(max_list_height + first_item_index).saturating_sub(1),
item_count.saturating_sub(1),
);
for (i, pane_item) in pane_items_on_the_left
.iter()
.enumerate()
.skip(first_item_index)
{
if i > last_item_index {
break;
}
let mut item = pane_item.render(max_width_for_item);
if Some(i) == self.marked_index.as_ref().map(|s| s.main_index)
&& self.visibility_and_focus.left_side_is_focused()
{
item = item.selected();
if self
.marked_index
.as_ref()
.map(|s| s.additional_indices.contains(&i))
.unwrap_or(false)
{
item = item.selected().color_range(1, ..);
}
} else if self
.marked_index
.as_ref()
.map(|s| s.additional_indices.contains(&i))
.unwrap_or(false)
&& self.visibility_and_focus.left_side_is_focused()
{
item = item.selected();
}
left_side_panes.push(item);
}
let extra_panes_on_top = first_item_index;
let extra_panes_on_bottom = item_count.saturating_sub(last_item_index + 1);
let extra_selected_item_count_on_top = if self.visibility_and_focus.left_side_is_focused() {
self.marked_index
.as_ref()
.map(|s| {
s.additional_indices
.iter()
.filter(|a| a < &&first_item_index)
.count()
})
.unwrap_or(0)
} else {
0
};
let extra_selected_item_count_on_bottom =
if self.visibility_and_focus.left_side_is_focused() {
self.marked_index
.as_ref()
.map(|s| {
s.additional_indices
.iter()
.filter(|a| a > &&last_item_index)
.count()
})
.unwrap_or(0)
} else {
0
};
(
extra_panes_on_top,
extra_panes_on_bottom,
extra_selected_item_count_on_top,
extra_selected_item_count_on_bottom,
left_side_panes,
)
}
fn left_side_controls(
&self,
max_width: usize,
) -> (&'static str, Text, &'static str, Text, &'static str, Text) {
// returns three components and their text
let (enter_select_panes_text, enter_select_panes) = if self.marked_index.is_some() {
let enter_select_panes_text_full = "<ENTER> - select, <↓↑→> - navigate";
let enter_select_panes_text_short = "<ENTER> / <↓↑→>...";
if max_width >= enter_select_panes_text_full.chars().count() {
let enter_select_panes_full = Text::new(enter_select_panes_text_full)
.color_range(3, ..=6)
.color_range(3, 18..=22);
(enter_select_panes_text_full, enter_select_panes_full)
} else {
let enter_select_panes_short = Text::new(enter_select_panes_text_short)
.color_range(3, ..=6)
.color_range(3, 10..=14);
(enter_select_panes_text_short, enter_select_panes_short)
}
} else {
let enter_select_panes_text_full = "<ENTER> - select all, <↓↑> - navigate";
let enter_select_panes_text_short = "<ENTER> / <↓↑>...";
if max_width >= enter_select_panes_text_full.chars().count() {
let enter_select_panes_full = Text::new(enter_select_panes_text_full)
.color_range(3, ..=6)
.color_range(3, 21..=25);
(enter_select_panes_text_full, enter_select_panes_full)
} else {
let enter_select_panes_short = Text::new(enter_select_panes_text_short)
.color_range(3, ..=6)
.color_range(3, 10..=13);
(enter_select_panes_text_short, enter_select_panes_short)
}
};
let space_shortcut_text_full = "<SPACE> - mark many,";
let space_shortcut_text_short = "<SPACE> /";
if self.marked_index.is_some() {
let escape_shortcut_text_full = "<Ctrl c> - remove marks";
let escape_shortcut_text_short = "<Ctrl c>...";
let (escape_shortcut, space_shortcut, escape_shortcut_text, space_shortcut_text) =
if max_width
>= space_shortcut_text_full.chars().count()
+ escape_shortcut_text_full.chars().count()
{
(
Text::new(escape_shortcut_text_full).color_range(3, ..=7),
Text::new(space_shortcut_text_full).color_range(3, ..=6),
escape_shortcut_text_full,
space_shortcut_text_full,
)
} else {
(
Text::new(escape_shortcut_text_short).color_range(3, ..=7),
Text::new(space_shortcut_text_short).color_range(3, ..=6),
escape_shortcut_text_short,
space_shortcut_text_short,
)
};
(
enter_select_panes_text,
enter_select_panes,
space_shortcut_text,
space_shortcut,
escape_shortcut_text,
escape_shortcut,
)
} else {
let escape_shortcut_text = if self.right_side_panes.is_empty() {
"<Ctrl c> - Close"
} else {
""
};
let escape_shortcut = Text::new(escape_shortcut_text).color_range(3, ..=7);
let space_shortcut = Text::new(space_shortcut_text_full).color_range(3, ..=6);
(
enter_select_panes_text,
enter_select_panes,
space_shortcut_text_full,
space_shortcut,
escape_shortcut_text,
escape_shortcut,
)
}
}
fn selected_panes_title(&self) -> Text {
let selected_prompt_text = "SELECTED PANES: ";
let selected_prompt = if self.visibility_and_focus.left_side_is_focused() {
Text::new(selected_prompt_text)
} else {
Text::new(selected_prompt_text).color_range(2, ..)
};
selected_prompt
}
fn right_side_panes_list(
&self,
max_width: usize,
max_list_height: usize,
) -> (usize, usize, usize, usize, Vec<NestedListItem>) {
// returns: extra_pane_count_on_top, extra_pane_count_on_bottom,
// extra_selected_item_count_on_top, extra_selected_item_count_on_bottom, list
let mut right_side_panes = vec![];
let item_count = self.right_side_panes.iter().count();
let first_item_index = if self.visibility_and_focus.left_side_is_focused() {
0
} else {
self.marked_index
.as_ref()
.map(|s| s.main_index.saturating_sub(max_list_height / 2))
.unwrap_or(0)
};
let last_item_index = std::cmp::min(
(max_list_height + first_item_index).saturating_sub(1),
item_count.saturating_sub(1),
);
let max_width_for_item = max_width.saturating_sub(3); // 3 for the list bulletin
for (i, pane_item) in self
.right_side_panes
.iter()
.enumerate()
.skip(first_item_index)
{
if i > last_item_index {
break;
}
let mut item = pane_item.render(max_width_for_item);
if &Some(i) == &self.marked_index.as_ref().map(|s| s.main_index)
&& self.visibility_and_focus.right_side_is_focused()
{
item = item.selected();
if self
.marked_index
.as_ref()
.map(|s| s.additional_indices.contains(&i))
.unwrap_or(false)
{
item = item.selected().color_range(1, ..);
}
} else if self
.marked_index
.as_ref()
.map(|s| s.additional_indices.contains(&i))
.unwrap_or(false)
&& self.visibility_and_focus.right_side_is_focused()
{
item = item.selected();
}
right_side_panes.push(item);
}
let extra_panes_on_top = first_item_index;
let extra_panes_on_bottom = self
.right_side_panes
.iter()
.len()
.saturating_sub(last_item_index + 1);
let extra_selected_item_count_on_top = if self.visibility_and_focus.left_side_is_focused() {
0
} else {
self.marked_index
.as_ref()
.map(|s| {
s.additional_indices
.iter()
.filter(|a| a < &&first_item_index)
.count()
})
.unwrap_or(0)
};
let extra_selected_item_count_on_bottom =
if self.visibility_and_focus.left_side_is_focused() {
0
} else {
self.marked_index
.as_ref()
.map(|s| {
s.additional_indices
.iter()
.filter(|a| a > &&last_item_index)
.count()
})
.unwrap_or(0)
};
(
extra_panes_on_top,
extra_panes_on_bottom,
extra_selected_item_count_on_top,
extra_selected_item_count_on_bottom,
right_side_panes,
)
}
fn right_side_controls(&self, cols: usize) -> (Text, Text, Text, Text) {
let right_side_controls_text_1_full = "<←↓↑> - navigate, <Ctrl c> - clear";
let right_side_controls_text_1_short = "<←↓↑>/<Ctrl c>...";
let right_side_controls_1 = if cols >= right_side_controls_text_1_full.chars().count() {
Text::new(right_side_controls_text_1_full)
.color_range(3, ..=4)
.color_range(3, 18..=25)
} else {
Text::new(right_side_controls_text_1_short)
.color_range(3, ..=4)
.color_range(3, 6..=13)
};
let right_side_controls_text_2_full = "<b> - break out, <s> - stack, <c> - close";
let right_side_controls_text_2_short = "<b>/<s>/<c>...";
let right_side_controls_2 = if cols >= right_side_controls_text_2_full.chars().count() {
Text::new(right_side_controls_text_2_full)
.color_range(3, ..=2)
.color_range(3, 17..=19)
.color_range(3, 30..=32)
} else {
Text::new(right_side_controls_text_2_short)
.color_range(3, ..=2)
.color_range(3, 4..=6)
.color_range(3, 8..=10)
};
let right_side_controls_text_3_full = "<r> - break right, <l> - break left";
let right_side_controls_text_3_short = "<r>/<l>...";
let right_side_controls_3 = if cols >= right_side_controls_text_3_full.chars().count() {
Text::new(right_side_controls_text_3_full)
.color_range(3, ..=2)
.color_range(3, 19..=21)
} else {
Text::new(right_side_controls_text_3_short)
.color_range(3, ..=2)
.color_range(3, 4..=6)
};
let right_side_controls_text_4_full = "<e> - embed, <f> - float";
let right_side_controls_text_4_short = "<e>/<f>...";
let right_side_controls_4 = if cols >= right_side_controls_text_4_full.chars().count() {
Text::new(right_side_controls_text_4_full)
.color_range(3, ..=2)
.color_range(3, 13..=15)
} else {
Text::new(right_side_controls_text_4_short)
.color_range(3, ..=2)
.color_range(3, 4..=6)
};
(
right_side_controls_1,
right_side_controls_2,
right_side_controls_3,
right_side_controls_4,
)
}
fn print_extra_pane_count(
&self,
count: usize,
selected_count: usize,
y: usize,
list_x: usize,
list_width: usize,
) {
let extra_count_text = if selected_count > 0 {
format!("[+{} ({} selected)]", count, selected_count)
} else {
format!("[+{}]", count)
};
let extra_count = Text::new(&extra_count_text).color_range(1, ..);
print_text_with_coordinates(
extra_count,
(list_x + list_width).saturating_sub(extra_count_text.chars().count()),
y,
None,
None,
);
}
}