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, components_to_render: Vec, 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>, zellij_version: String, _base_mode: Rc>, 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>) -> 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 them")) .color_substring(3, "Alt "), )), 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>) -> 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) -> Self { self.components_to_render .push(RenderedComponent::Paragraph(paragraph)); self } pub fn with_help(mut self, help_text_fn: Box 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 { 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 { 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 { 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) { 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 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 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, - Learn More, - 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, - 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, - Learn More, - 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, - 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: - Go back"); Text::new(help_text).color_range(1, 6..=10) } } fn esc_to_go_back_help() -> Text { let help_text = format!("Help: - 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 usize>, // (rows, columns) -> text_len (render function) Box 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 Text>), BulletinList(BulletinList), Paragraph(Vec), } 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, } impl BulletinList { pub fn new(title: Text) -> Self { BulletinList { title, items: vec![], } } pub fn with_items(mut self, items: Vec) -> 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 { 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 { 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 { 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, } impl ComponentLine { pub fn handle_left_click_at_position(&mut self, x: usize, y: usize) -> Option { 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) -> Self { ComponentLine { components } } }