zellij/default-plugins/about/src/pages.rs
Aram Drevekenin b634a57de8
style: link and help fixes (#4339)
* style: link and help fixes

* fix: tip coloring
2025-08-04 10:40:17 +02:00

959 lines
36 KiB
Rust

use zellij_tile::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
use crate::active_component::{ActiveComponent, ClickAction};
#[derive(Debug)]
pub struct Page {
title: Option<Text>,
components_to_render: Vec<RenderedComponent>,
has_hover: bool,
hovering_over_link: bool,
menu_item_is_selected: bool,
pub is_main_screen: bool,
}
impl Page {
pub fn new_main_screen(
link_executable: Rc<RefCell<String>>,
zellij_version: String,
_base_mode: Rc<RefCell<InputMode>>,
is_release_notes: bool,
) -> Self {
Page::new()
.main_screen()
.with_title(main_screen_title(zellij_version.clone(), is_release_notes))
.with_bulletin_list(BulletinList::new(whats_new_title()).with_items(vec![
ActiveComponent::new(TextOrCustomRender::Text(main_menu_item(
"Web Client",
)))
.with_hover(TextOrCustomRender::Text(
main_menu_item("Web Client").selected(),
))
.with_left_click_action(ClickAction::new_change_page({
let link_executable = link_executable.clone();
move || Page::new_web_client(link_executable.clone())
})),
ActiveComponent::new(TextOrCustomRender::Text(main_menu_item(
"Multiple Pane Select",
)))
.with_hover(TextOrCustomRender::Text(
main_menu_item("Multiple Pane Select").selected(),
))
.with_left_click_action(ClickAction::new_change_page(move || {
Page::new_multiple_select()
})),
ActiveComponent::new(TextOrCustomRender::Text(main_menu_item(
"Key Tooltips for the compact-bar",
)))
.with_hover(TextOrCustomRender::Text(
main_menu_item("Key Tooltips for the compact-bar").selected(),
))
.with_left_click_action(ClickAction::new_change_page({
let link_executable = link_executable.clone();
move || Page::new_key_tooltips_for_compact_bar(link_executable.clone())
})),
ActiveComponent::new(TextOrCustomRender::Text(main_menu_item(
"Stack Keybinding",
)))
.with_hover(TextOrCustomRender::Text(
main_menu_item("Stack Keybinding").selected(),
))
.with_left_click_action(ClickAction::new_change_page(move || {
Page::new_stack_keybinding()
})),
ActiveComponent::new(TextOrCustomRender::Text(main_menu_item(
"Performance Improvements",
)))
.with_hover(TextOrCustomRender::Text(
main_menu_item("Performance Improvements").selected(),
))
.with_left_click_action(ClickAction::new_change_page({
move || Page::new_performance_improvements()
})),
]))
.with_paragraph(vec![ComponentLine::new(vec![
ActiveComponent::new(TextOrCustomRender::Text(Text::new("Full Changelog: "))),
ActiveComponent::new(TextOrCustomRender::Text(changelog_link_unselected(
zellij_version.clone(),
)))
.with_hover(TextOrCustomRender::CustomRender(
Box::new(changelog_link_selected(zellij_version.clone())),
Box::new(changelog_link_selected_len(zellij_version.clone())),
))
.with_left_click_action(ClickAction::new_open_link(
format!(
"https://github.com/zellij-org/zellij/releases/tag/v{}",
zellij_version.clone()
),
link_executable.clone(),
)),
])])
.with_paragraph(vec![ComponentLine::new(vec![
ActiveComponent::new(TextOrCustomRender::Text(support_the_developer_text())),
ActiveComponent::new(TextOrCustomRender::Text(sponsors_link_text_unselected()))
.with_hover(TextOrCustomRender::CustomRender(
Box::new(sponsors_link_text_selected),
Box::new(sponsors_link_text_selected_len),
))
.with_left_click_action(ClickAction::new_open_link(
"https://github.com/sponsors/imsnif".to_owned(),
link_executable.clone(),
)),
])])
.with_help(if is_release_notes {
Box::new(|hovering_over_link, menu_item_is_selected| {
release_notes_main_help(hovering_over_link, menu_item_is_selected)
})
} else {
Box::new(|hovering_over_link, menu_item_is_selected| {
main_screen_help_text(hovering_over_link, menu_item_is_selected)
})
})
}
pub fn new_web_client(link_executable: Rc<RefCell<String>>) -> Page {
Page::new()
.with_title(Text::new("Web Client").color_range(0, ..))
.with_paragraph(vec![
ComponentLine::new(vec![
// ActiveComponent::new(TextOrCustomRender::Text(Text::new("This version includes a new resizing algorithm that helps better manage panes"))),
ActiveComponent::new(TextOrCustomRender::Text(Text::new("This version includes a web client, allowing you to share sessions in the browser."))),
]),
])
.with_bulletin_list(BulletinList::new(Text::new("The web client:").color_range(2, ..))
.with_items(vec![
ActiveComponent::new(TextOrCustomRender::Text(
Text::new("Allows you to bookmark sessions")
.color_substring(3, "bookmark sessions")
)),
ActiveComponent::new(TextOrCustomRender::Text(
Text::new("Includes built-in authentication")
)),
ActiveComponent::new(TextOrCustomRender::Text(
Text::new("Can be used as a daily-driver, making your terminal emulator optional")
)),
ActiveComponent::new(TextOrCustomRender::Text(
Text::new("Is completely opt-in")
)),
])
)
.with_paragraph(vec![
ComponentLine::new(vec![
ActiveComponent::new(TextOrCustomRender::Text(
Text::new("For more details, see: ")
.color_range(2, ..)
)),
ActiveComponent::new(TextOrCustomRender::Text(Text::new("https://zellij.dev/tutorials/web-client")))
.with_hover(TextOrCustomRender::CustomRender(Box::new(web_client_screencast_link_selected), Box::new(web_client_screencast_link_selected_len)))
.with_left_click_action(ClickAction::new_open_link("https://zellij.dev/tutorials/web-client".to_owned(), link_executable.clone()))
])
])
.with_help(Box::new(|hovering_over_link, menu_item_is_selected| esc_go_back_plus_link_hover(hovering_over_link, menu_item_is_selected)))
}
fn new_multiple_select() -> Page {
Page::new()
.with_title(Text::new("Multiple Pane Select").color_range(0, ..))
.with_paragraph(vec![
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
Text::new("This version adds the ability to perform bulk operations on panes"),
))]),
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
Text::new("eg. close, make floating, break to a new tab, etc."),
))]),
])
.with_bulletin_list(
BulletinList::new(
Text::new(format!("To select multiple panes: ")).color_range(2, ..),
)
.with_items(vec![
ActiveComponent::new(TextOrCustomRender::Text(
Text::new(format!("Alt <left-click> them"))
.color_substring(3, "Alt <left-click>"),
)),
ActiveComponent::new(TextOrCustomRender::Text(
Text::new(format!("Toggle with Alt p")).color_substring(3, "Alt p"),
)),
]),
)
.with_paragraph(vec![
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
Text::new("To disable this behavior (and the associated hover effects)"),
))]),
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
Text::new(format!("add advanced_mouse_actions false to the config."))
.color_substring(3, "advanced_mouse_actions false"),
))]),
])
.with_help(Box::new(|_hovering_over_link, _menu_item_is_selected| {
esc_to_go_back_help()
}))
}
fn new_key_tooltips_for_compact_bar(link_executable: Rc<RefCell<String>>) -> Page {
Page::new()
.with_title(Text::new("Key Tooltips for the compact-bar").color_range(0, ..))
.with_paragraph(vec![
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
Text::new(
"Starting this version, it's possible to add toggle-able key tooltips",
)
.color_range(3, 37..=58),
))]),
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
Text::new("when using the compact-bar.").color_substring(3, "compact-bar"),
))]),
])
.with_paragraph(vec![ComponentLine::new(vec![
ActiveComponent::new(TextOrCustomRender::Text(
Text::new("For more information: ").color_range(2, ..),
)),
ActiveComponent::new(TextOrCustomRender::Text(Text::new(
"https://zellij.dev/documentation/faq.html",
)))
.with_hover(TextOrCustomRender::CustomRender(
Box::new(compact_bar_link_selected),
Box::new(compact_bar_link_selected_len),
))
.with_left_click_action(ClickAction::new_open_link(
"https://zellij.dev/documentation/faq.html".to_owned(),
link_executable.clone(),
)),
])])
.with_help(Box::new(|hovering_over_link, menu_item_is_selected| {
esc_go_back_plus_link_hover(hovering_over_link, menu_item_is_selected)
}))
}
fn new_stack_keybinding() -> Page {
Page::new()
.with_title(Text::new("New Stack Keybinding").color_range(0, ..))
.with_paragraph(vec![
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
Text::new("It's now possible to open a stacked pane directly on top of the current pane").color_substring(2, "stacked pane"),
))]),
])
.with_paragraph(vec![
ComponentLine::new(vec![
ActiveComponent::new(TextOrCustomRender::Text(
Text::new("By default: Ctrl p + s").color_substring(3, "Ctrl p").color_substring(3, " s"),
)),
]),
ComponentLine::new(vec![
ActiveComponent::new(TextOrCustomRender::Text(
Text::new("In unlock first: Ctrl g + p + s").color_substring(3, "Ctrl g").color_substring(3, " p").color_substring(3, " s"),
)),
]),
])
.with_paragraph(vec![
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
Text::new("To add to an existing config, see the release notes.")
))]),
])
.with_help(Box::new(|_hovering_over_link, _menu_item_is_selected| {
esc_to_go_back_help()
}))
}
fn new_performance_improvements() -> Page {
Page::new()
.with_title(Text::new("Performance Improvements").color_range(0, ..))
.with_paragraph(vec![
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
Text::new("This version adds a debounced asynchronous render mechanism"),
))]),
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
Text::new("making rendering much smoother across the whole application."),
))]),
])
.with_help(Box::new(|_hovering_over_link, _menu_item_is_selected| {
esc_to_go_back_help()
}))
}
}
impl Page {
pub fn new() -> Self {
Page {
title: None,
components_to_render: vec![],
has_hover: false,
hovering_over_link: false,
menu_item_is_selected: false,
is_main_screen: false,
}
}
pub fn main_screen(mut self) -> Self {
self.is_main_screen = true;
self
}
pub fn with_title(mut self, title: Text) -> Self {
self.title = Some(title);
self
}
pub fn with_bulletin_list(mut self, bulletin_list: BulletinList) -> Self {
self.components_to_render
.push(RenderedComponent::BulletinList(bulletin_list));
self
}
pub fn with_paragraph(mut self, paragraph: Vec<ComponentLine>) -> Self {
self.components_to_render
.push(RenderedComponent::Paragraph(paragraph));
self
}
pub fn with_help(mut self, help_text_fn: Box<dyn Fn(bool, bool) -> Text>) -> Self {
self.components_to_render
.push(RenderedComponent::HelpText(help_text_fn));
self
}
pub fn handle_key(&mut self, key: KeyWithModifier) -> bool {
let mut should_render = false;
if key.bare_key == BareKey::Down && key.has_no_modifiers() {
self.move_selection_down();
should_render = true;
} else if key.bare_key == BareKey::Up && key.has_no_modifiers() {
self.move_selection_up();
should_render = true;
}
should_render
}
pub fn handle_mouse_left_click(&mut self, x: usize, y: usize) -> Option<Page> {
for rendered_component in &mut self.components_to_render {
match rendered_component {
RenderedComponent::BulletinList(bulletin_list) => {
let page_to_render = bulletin_list.handle_left_click_at_position(x, y);
if page_to_render.is_some() {
return page_to_render;
}
},
RenderedComponent::Paragraph(paragraph) => {
for component_line in paragraph {
let page_to_render = component_line.handle_left_click_at_position(x, y);
if page_to_render.is_some() {
return page_to_render;
}
}
},
_ => {},
}
}
None
}
pub fn handle_selection(&mut self) -> Option<Page> {
for rendered_component in &mut self.components_to_render {
match rendered_component {
RenderedComponent::BulletinList(bulletin_list) => {
let page_to_render = bulletin_list.handle_selection();
if page_to_render.is_some() {
return page_to_render;
}
},
_ => {},
}
}
None
}
pub fn handle_mouse_hover(&mut self, x: usize, y: usize) -> bool {
let hover_cleared = self.clear_hover(); // TODO: do the right thing if the same component was hovered from
// previous motion
for rendered_component in &mut self.components_to_render {
match rendered_component {
RenderedComponent::BulletinList(bulletin_list) => {
let should_render = bulletin_list.handle_hover_at_position(x, y);
if should_render {
self.has_hover = true;
self.menu_item_is_selected = true;
return should_render;
}
},
RenderedComponent::Paragraph(paragraph) => {
for component_line in paragraph {
let should_render = component_line.handle_hover_at_position(x, y);
if should_render {
self.has_hover = true;
self.hovering_over_link = true;
return should_render;
}
}
},
_ => {},
}
}
hover_cleared
}
fn move_selection_up(&mut self) {
match self.position_of_active_bulletin() {
Some(position_of_active_bulletin) if position_of_active_bulletin > 0 => {
self.clear_active_bulletins();
self.set_active_bulletin(position_of_active_bulletin.saturating_sub(1));
},
Some(0) => {
self.clear_active_bulletins();
},
_ => {
self.clear_active_bulletins();
self.set_last_active_bulletin();
},
}
}
fn move_selection_down(&mut self) {
match self.position_of_active_bulletin() {
Some(position_of_active_bulletin) => {
self.clear_active_bulletins();
self.set_active_bulletin(position_of_active_bulletin + 1);
},
None => {
self.set_active_bulletin(0);
},
}
}
fn position_of_active_bulletin(&self) -> Option<usize> {
self.components_to_render.iter().find_map(|c| match c {
RenderedComponent::BulletinList(bulletin_list) => {
bulletin_list.active_component_position()
},
_ => None,
})
}
fn clear_active_bulletins(&mut self) {
self.components_to_render.iter_mut().for_each(|c| {
match c {
RenderedComponent::BulletinList(bulletin_list) => {
Some(bulletin_list.clear_active_bulletins())
},
_ => None,
};
});
}
fn set_active_bulletin(&mut self, active_bulletin_position: usize) {
self.components_to_render.iter_mut().for_each(|c| {
match c {
RenderedComponent::BulletinList(bulletin_list) => {
bulletin_list.set_active_bulletin(active_bulletin_position)
},
_ => {},
};
});
}
fn set_last_active_bulletin(&mut self) {
self.components_to_render.iter_mut().for_each(|c| {
match c {
RenderedComponent::BulletinList(bulletin_list) => {
bulletin_list.set_last_active_bulletin()
},
_ => {},
};
});
}
fn clear_hover(&mut self) -> bool {
let had_hover = self.has_hover;
self.menu_item_is_selected = false;
self.hovering_over_link = false;
for rendered_component in &mut self.components_to_render {
match rendered_component {
RenderedComponent::BulletinList(bulletin_list) => {
bulletin_list.clear_hover();
},
RenderedComponent::Paragraph(paragraph) => {
for active_component in paragraph {
active_component.clear_hover();
}
},
_ => {},
}
}
self.has_hover = false;
had_hover
}
pub fn ui_column_count(&mut self) -> usize {
let mut column_count = 0;
for rendered_component in &self.components_to_render {
match rendered_component {
RenderedComponent::BulletinList(bulletin_list) => {
column_count = std::cmp::max(column_count, bulletin_list.column_count());
},
RenderedComponent::Paragraph(paragraph) => {
for active_component in paragraph {
column_count = std::cmp::max(column_count, active_component.column_count());
}
},
RenderedComponent::HelpText(_text) => {}, // we ignore help text in column
// calculation because it's always left
// justified
}
}
column_count
}
pub fn ui_row_count(&mut self) -> usize {
let mut row_count = 0;
if self.title.is_some() {
row_count += 1;
}
for rendered_component in &self.components_to_render {
match rendered_component {
RenderedComponent::BulletinList(bulletin_list) => {
row_count += bulletin_list.len();
},
RenderedComponent::Paragraph(paragraph) => {
row_count += paragraph.len();
},
RenderedComponent::HelpText(_text) => {}, // we ignore help text as it is outside
// the UI container
}
}
row_count += self.components_to_render.len();
row_count
}
pub fn render(&mut self, rows: usize, columns: usize, error: &Option<String>) {
let base_x = columns.saturating_sub(self.ui_column_count()) / 2;
let base_y = rows.saturating_sub(self.ui_row_count()) / 2;
let mut current_y = base_y;
if let Some(title) = &self.title {
print_text_with_coordinates(
title.clone(),
base_x,
current_y,
Some(columns),
Some(rows),
);
current_y += 2;
}
for rendered_component in &mut self.components_to_render {
let is_help = match rendered_component {
RenderedComponent::HelpText(_) => true,
_ => false,
};
if is_help {
if let Some(error) = error {
render_error(error, rows);
continue;
}
}
let y = if is_help { rows } else { current_y };
let columns = if is_help {
columns
} else {
columns.saturating_sub(base_x * 2)
};
let rendered_rows = rendered_component.render(
base_x,
y,
rows,
columns,
self.hovering_over_link,
self.menu_item_is_selected,
);
current_y += rendered_rows + 1; // 1 for the line space between components
}
}
}
fn render_error(error: &str, y: usize) {
print_text_with_coordinates(
Text::new(format!("ERROR: {}", error)).color_range(3, ..),
0,
y,
None,
None,
);
}
fn changelog_link_unselected(version: String) -> Text {
let full_changelog_text = format!(
"https://github.com/zellij-org/zellij/releases/tag/v{}",
version
);
Text::new(full_changelog_text)
}
fn changelog_link_selected(version: String) -> Box<dyn Fn(usize, usize) -> usize> {
Box::new(move |x, y| {
print!(
"\u{1b}[{};{}H\u{1b}[m\u{1b}[1;4mhttps://github.com/zellij-org/zellij/releases/tag/v{}",
y + 1,
x + 1,
version
);
51 + version.chars().count()
})
}
fn changelog_link_selected_len(version: String) -> Box<dyn Fn() -> usize> {
Box::new(move || 51 + version.chars().count())
}
fn sponsors_link_text_unselected() -> Text {
Text::new("https://github.com/sponsors/imsnif")
}
fn sponsors_link_text_selected(x: usize, y: usize) -> usize {
print!(
"\u{1b}[{};{}H\u{1b}[m\u{1b}[1;4mhttps://github.com/sponsors/imsnif",
y + 1,
x + 1
);
34
}
fn sponsors_link_text_selected_len() -> usize {
34
}
fn web_client_screencast_link_selected(x: usize, y: usize) -> usize {
print!(
"\u{1b}[{};{}H\u{1b}[m\u{1b}[1;4mhttps://zellij.dev/tutorials/web-client",
y + 1,
x + 1
);
39
}
fn web_client_screencast_link_selected_len() -> usize {
39
}
fn compact_bar_link_selected(x: usize, y: usize) -> usize {
print!(
"\u{1b}[{};{}H\u{1b}[m\u{1b}[1;4mhttps://zellij.dev/documentation/faq.html",
y + 1,
x + 1
);
41
}
fn compact_bar_link_selected_len() -> usize {
41
}
// Text components
fn whats_new_title() -> Text {
Text::new("What's new?")
}
fn main_screen_title(version: String, is_release_notes: bool) -> Text {
if is_release_notes {
let title_text = format!("Hi there, welcome to Zellij {}!", &version);
Text::new(title_text).color_range(2, 21..=27 + version.chars().count())
} else {
let title_text = format!("Zellij {}", &version);
Text::new(title_text).color_range(2, ..)
}
}
fn main_screen_help_text(hovering_over_link: bool, menu_item_is_selected: bool) -> Text {
if hovering_over_link {
let help_text = format!("Help: Click or Shift-Click to open in browser");
Text::new(help_text)
.color_range(3, 6..=10)
.color_range(3, 15..=25)
} else if menu_item_is_selected {
let help_text = format!("Help: <↓↑> - Navigate, <ENTER> - Learn More, <ESC> - Dismiss");
Text::new(help_text)
.color_range(1, 6..=9)
.color_range(1, 23..=29)
.color_range(1, 45..=49)
} else {
let help_text = format!("Help: <↓↑> - Navigate, <ESC> - Dismiss, <?> - Usage Tips");
Text::new(help_text)
.color_range(1, 6..=9)
.color_range(1, 23..=27)
.color_range(1, 40..=42)
}
}
fn release_notes_main_help(hovering_over_link: bool, menu_item_is_selected: bool) -> Text {
if hovering_over_link {
let help_text = format!("Help: Click or Shift-Click to open in browser");
Text::new(help_text)
.color_range(3, 6..=10)
.color_range(3, 15..=25)
} else if menu_item_is_selected {
let help_text = format!("Help: <↓↑> - Navigate, <ENTER> - Learn More, <ESC> - Dismiss");
Text::new(help_text)
.color_range(1, 6..=9)
.color_range(1, 23..=29)
.color_range(1, 45..=49)
} else {
let help_text = format!("Help: <↓↑> - Navigate, <ESC> - Dismiss");
Text::new(help_text)
.color_range(1, 6..=9)
.color_range(1, 23..=27)
}
}
fn esc_go_back_plus_link_hover(hovering_over_link: bool, _menu_item_is_selected: bool) -> Text {
if hovering_over_link {
let help_text = format!("Help: Click or Shift-Click to open in browser");
Text::new(help_text)
.color_range(3, 6..=10)
.color_range(3, 15..=25)
} else {
let help_text = format!("Help: <ESC> - Go back");
Text::new(help_text).color_range(1, 6..=10)
}
}
fn esc_to_go_back_help() -> Text {
let help_text = format!("Help: <ESC> - Go back");
Text::new(help_text).color_range(1, 6..=10)
}
fn main_menu_item(item_name: &str) -> Text {
Text::new(item_name).color_range(0, ..)
}
fn support_the_developer_text() -> Text {
let support_text = format!("Please support the Zellij developer <3: ");
Text::new(support_text).color_range(3, ..)
}
pub enum TextOrCustomRender {
Text(Text),
CustomRender(
Box<dyn Fn(usize, usize) -> usize>, // (rows, columns) -> text_len (render function)
Box<dyn Fn() -> usize>, // length of rendered component
),
}
impl TextOrCustomRender {
pub fn len(&self) -> usize {
match self {
TextOrCustomRender::Text(text) => text.len(),
TextOrCustomRender::CustomRender(_render_fn, len_fn) => len_fn(),
}
}
pub fn render(&mut self, x: usize, y: usize, rows: usize, columns: usize) -> usize {
match self {
TextOrCustomRender::Text(text) => {
print_text_with_coordinates(text.clone(), x, y, Some(columns), Some(rows));
text.len()
},
TextOrCustomRender::CustomRender(render_fn, _len_fn) => render_fn(x, y),
}
}
}
impl std::fmt::Debug for TextOrCustomRender {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TextOrCustomRender::Text(text) => write!(f, "Text {{ {:?} }}", text),
TextOrCustomRender::CustomRender(..) => write!(f, "CustomRender"),
}
}
}
enum RenderedComponent {
HelpText(Box<dyn Fn(bool, bool) -> Text>),
BulletinList(BulletinList),
Paragraph(Vec<ComponentLine>),
}
impl std::fmt::Debug for RenderedComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RenderedComponent::HelpText(_) => write!(f, "HelpText"),
RenderedComponent::BulletinList(bulletinlist) => write!(f, "{:?}", bulletinlist),
RenderedComponent::Paragraph(component_list) => write!(f, "{:?}", component_list),
}
}
}
impl RenderedComponent {
pub fn render(
&mut self,
x: usize,
y: usize,
rows: usize,
columns: usize,
hovering_over_link: bool,
menu_item_is_selected: bool,
) -> usize {
let mut rendered_rows = 0;
match self {
RenderedComponent::HelpText(text) => {
rendered_rows += 1;
print_text_with_coordinates(
text(hovering_over_link, menu_item_is_selected),
0,
y,
Some(columns),
Some(rows),
);
},
RenderedComponent::BulletinList(bulletin_list) => {
rendered_rows += bulletin_list.len();
bulletin_list.render(x, y, rows, columns);
},
RenderedComponent::Paragraph(paragraph) => {
let mut paragraph_rendered_rows = 0;
for component_line in paragraph {
component_line.render(
x,
y + paragraph_rendered_rows,
rows.saturating_sub(paragraph_rendered_rows),
columns,
);
rendered_rows += 1;
paragraph_rendered_rows += 1;
}
},
}
rendered_rows
}
}
#[derive(Debug)]
pub struct BulletinList {
title: Text,
items: Vec<ActiveComponent>,
}
impl BulletinList {
pub fn new(title: Text) -> Self {
BulletinList {
title,
items: vec![],
}
}
pub fn with_items(mut self, items: Vec<ActiveComponent>) -> Self {
self.items = items;
self
}
pub fn len(&self) -> usize {
self.items.len() + 1 // 1 for the title
}
pub fn column_count(&self) -> usize {
let mut column_count = 0;
for item in &self.items {
column_count = std::cmp::max(column_count, item.column_count());
}
column_count
}
pub fn handle_left_click_at_position(&mut self, x: usize, y: usize) -> Option<Page> {
for component in &mut self.items {
let page_to_render = component.handle_left_click_at_position(x, y);
if page_to_render.is_some() {
return page_to_render;
}
}
None
}
pub fn handle_selection(&mut self) -> Option<Page> {
for component in &mut self.items {
let page_to_render = component.handle_selection();
if page_to_render.is_some() {
return page_to_render;
}
}
None
}
pub fn handle_hover_at_position(&mut self, x: usize, y: usize) -> bool {
for component in &mut self.items {
let should_render = component.handle_hover_at_position(x, y);
if should_render {
return should_render;
}
}
false
}
pub fn clear_hover(&mut self) {
for component in &mut self.items {
component.clear_hover();
}
}
pub fn active_component_position(&self) -> Option<usize> {
self.items.iter().position(|i| i.is_active)
}
pub fn clear_active_bulletins(&mut self) {
self.items.iter_mut().for_each(|i| {
i.is_active = false;
});
}
pub fn set_active_bulletin(&mut self, new_index: usize) {
self.items.get_mut(new_index).map(|i| {
i.is_active = true;
});
}
pub fn set_last_active_bulletin(&mut self) {
self.items.last_mut().map(|i| {
i.is_active = true;
});
}
pub fn render(&mut self, x: usize, y: usize, rows: usize, columns: usize) {
print_text_with_coordinates(self.title.clone(), x, y, Some(columns), Some(rows));
let mut item_bulletin = 1;
let mut running_y = y + 1;
for item in &mut self.items {
let mut item_bulletin_text = Text::new(format!("{}. ", item_bulletin));
if item.is_active {
item_bulletin_text = item_bulletin_text.selected();
}
let item_bulletin_text_len = item_bulletin_text.len();
print_text_with_coordinates(
item_bulletin_text,
x,
running_y,
Some(item_bulletin_text_len),
Some(rows),
);
item.render(
x + item_bulletin_text_len,
running_y,
rows,
columns.saturating_sub(item_bulletin_text_len),
);
running_y += 1;
item_bulletin += 1;
}
}
}
#[derive(Debug)]
pub struct ComponentLine {
components: Vec<ActiveComponent>,
}
impl ComponentLine {
pub fn handle_left_click_at_position(&mut self, x: usize, y: usize) -> Option<Page> {
for active_component in &mut self.components {
let page_to_render = active_component.handle_left_click_at_position(x, y);
if page_to_render.is_some() {
return page_to_render;
}
}
None
}
pub fn handle_hover_at_position(&mut self, x: usize, y: usize) -> bool {
for active_component in &mut self.components {
let should_render = active_component.handle_hover_at_position(x, y);
if should_render {
return should_render;
}
}
false
}
pub fn clear_hover(&mut self) {
for active_component in &mut self.components {
active_component.clear_hover();
}
}
pub fn column_count(&self) -> usize {
let mut column_count = 0;
for active_component in &self.components {
column_count += active_component.column_count()
}
column_count
}
pub fn render(&mut self, x: usize, y: usize, rows: usize, columns: usize) {
let mut current_x = x;
let mut columns_left = columns;
for component in &mut self.components {
let component_len = component.render(current_x, y, rows, columns_left);
current_x += component_len;
columns_left = columns_left.saturating_sub(component_len);
}
}
}
impl ComponentLine {
pub fn new(components: Vec<ActiveComponent>) -> Self {
ComponentLine { components }
}
}