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( "Stacked Resize", ))) .with_hover(TextOrCustomRender::Text( main_menu_item("Stacked Resize").selected(), )) .with_left_click_action(ClickAction::new_change_page({ let link_executable = link_executable.clone(); move || Page::new_stacked_resize(link_executable.clone()) })), ActiveComponent::new(TextOrCustomRender::Text(main_menu_item( "Pinned Floating Panes", ))) .with_hover(TextOrCustomRender::Text( main_menu_item("Pinned Floating Panes").selected(), )) .with_left_click_action(ClickAction::new_change_page(move || { Page::new_pinned_panes(base_mode.clone()) })), ActiveComponent::new(TextOrCustomRender::Text(main_menu_item( "New Theme Definition Spec", ))) .with_hover(TextOrCustomRender::Text( main_menu_item("New Theme Definition Spec").selected(), )) .with_left_click_action(ClickAction::new_change_page({ let link_executable = link_executable.clone(); move || Page::new_theme_definition_spec(link_executable.clone()) })), ActiveComponent::new(TextOrCustomRender::Text(main_menu_item( "New Plugin APIs", ))) .with_hover(TextOrCustomRender::Text( main_menu_item("New Plugin APIs").selected(), )) .with_left_click_action(ClickAction::new_change_page(move || { Page::new_plugin_apis() })), ActiveComponent::new(TextOrCustomRender::Text(main_menu_item( "Mouse Any-Event Handling", ))) .with_hover(TextOrCustomRender::Text( main_menu_item("Mouse Any-Event Handling").selected(), )) .with_left_click_action(ClickAction::new_change_page({ move || Page::new_mouse_any_event() })), ])) .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_stacked_resize(link_executable: Rc>) -> Page { Page::new() .with_title(Text::new("Stacked Resize").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"))), ]), ComponentLine::new(vec![ ActiveComponent::new(TextOrCustomRender::Text(Text::new("into stacks."))), ]), ]) .with_bulletin_list(BulletinList::new(Text::new("To try it out:").color_range(2, ..)) .with_items(vec![ ActiveComponent::new(TextOrCustomRender::Text( Text::new("Hide this pane with Alt f (you can bring it back with Alt f again)") .color_range(3, 20..=24) .color_range(3, 54..=58) )), ActiveComponent::new(TextOrCustomRender::Text( Text::new("Open 4-5 panes with Alt n") .color_range(3, 20..=24) )), ActiveComponent::new(TextOrCustomRender::Text( Text::new("Press Alt + until you reach full screen") .color_range(3, 6..=10) )), ActiveComponent::new(TextOrCustomRender::Text( Text::new("Press Alt - until you are back at the original state") .color_range(3, 6..=10) )), ActiveComponent::new(TextOrCustomRender::Text( Text::new("You can always snap back to the built-in swap layouts with Alt <[]>") .color_range(3, 59..=61) .color_range(3, 64..=65) )), ]) ) .with_paragraph(vec![ ComponentLine::new(vec![ ActiveComponent::new(TextOrCustomRender::Text( Text::new("To disable, add stacked_resize false to the Zellij Configuration") .color_range(3, 16..=35) )), ]) ]) .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/screencasts/stacked-resize"))) .with_hover(TextOrCustomRender::CustomRender(Box::new(stacked_resize_screencast_link_selected), Box::new(stacked_resize_screencast_link_selected_len))) .with_left_click_action(ClickAction::new_open_link("https://zellij.dev/screencasts/stacked-resize".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_pinned_panes(base_mode: Rc>) -> Page { Page::new() .with_title(Text::new("Pinned Floating Panes").color_range(0, ..)) .with_paragraph(vec![ ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text( Text::new( "This version adds the ability to \"pin\" a floating pane so that it", ), ))]), ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text( Text::new("will always be visible even if floating panes are hidden."), ))]), ]) .with_bulletin_list( BulletinList::new( Text::new(format!("Floating panes can be \"pinned\": ")).color_range(2, ..), ) .with_items(vec![ ActiveComponent::new(TextOrCustomRender::Text( Text::new(format!("With a mouse click on their top right corner")) .color_range(3, 7..=17), )), ActiveComponent::new(TextOrCustomRender::Text(match *base_mode.borrow() { InputMode::Locked => Text::new(format!("With Ctrl g + p + i")) .color_range(3, 5..=10) .color_range(3, 14..15) .color_range(3, 18..19), _ => Text::new("With Ctrl p + i") .color_range(3, 5..=10) .color_range(3, 14..15), })), ]), ) .with_paragraph(vec![ ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text( Text::new("A great use case for these is to tail log files or to show"), ))]), ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text( Text::new(format!( "real-time compiler output while working in other panes." )), ))]), ]) .with_help(Box::new(|_hovering_over_link, _menu_item_is_selected| { esc_to_go_back_help() })) } fn new_theme_definition_spec(link_executable: Rc>) -> Page { Page::new() .with_title(Text::new("New Theme Definition Spec").color_range(0, ..)) .with_paragraph(vec![ ComponentLine::new(vec![ ActiveComponent::new(TextOrCustomRender::Text( Text::new("Starting this version, themes can be defined by UI components") .color_range(3, 37..=60) )) ]), ComponentLine::new(vec![ ActiveComponent::new(TextOrCustomRender::Text( Text::new("instead of the previously obscure color-to-color definitions.") )) ]), ]) .with_paragraph(vec![ ComponentLine::new(vec![ ActiveComponent::new(TextOrCustomRender::Text( Text::new("This both improves the convenience of theme creation and allows greater freedom") )) ]), ComponentLine::new(vec![ ActiveComponent::new(TextOrCustomRender::Text( Text::new("for theme authors.") )) ]), ]) .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/themes"))) .with_hover(TextOrCustomRender::CustomRender(Box::new(theme_link_selected), Box::new(theme_link_selected_len))) .with_left_click_action(ClickAction::new_open_link("https://zellij.dev/documentation/themes".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_plugin_apis() -> Page { Page::new() .with_title(Text::new("New Plugin APIs").color_range(0, ..)) .with_paragraph(vec![ ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text( Text::new("New APIs were added in this version affording plugins"), ))]), ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text( Text::new("finer control over the workspace."), ))]), ]) .with_bulletin_list( BulletinList::new(Text::new("Some examples:").color_range(2, ..)).with_items(vec![ ActiveComponent::new(TextOrCustomRender::Text( Text::new("Change floating panes' coordinates and size") .color_range(3, 23..=33) .color_range(3, 39..=42), )), ActiveComponent::new(TextOrCustomRender::Text( Text::new("Stack arbitrary panes").color_range(3, ..=4), )), ActiveComponent::new(TextOrCustomRender::Text( Text::new("Change /host folder").color_range(3, 7..=11), )), ActiveComponent::new(TextOrCustomRender::Text( Text::new("Discover the user's $SHELL and $EDITOR") .color_range(3, 20..=25) .color_range(3, 31..=37), )), ]), ) .with_help(Box::new(|_hovering_over_link, _menu_item_is_selected| { esc_to_go_back_help() })) } fn new_mouse_any_event() -> Page { Page::new() .with_title(Text::new("Mosue Any-Event Tracking").color_range(0, ..)) .with_paragraph(vec![ ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text( Text::new( "This version adds the capability to track mouse motions more accurately", ), ))]), ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text( Text::new("both in Zellij, in terminal panes and in plugin panes."), ))]), ]) .with_paragraph(vec![ComponentLine::new(vec![ActiveComponent::new( TextOrCustomRender::Text(Text::new( "Future versions will also build on this capability to improve the Zellij UI", )), )])]) .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 stacked_resize_screencast_link_selected(x: usize, y: usize) -> usize { print!( "\u{1b}[{};{}H\u{1b}[m\u{1b}[1;4mhttps://zellij.dev/screencasts/stacked-resize", y + 1, x + 1 ); 45 } fn stacked_resize_screencast_link_selected_len() -> usize { 45 } fn theme_link_selected(x: usize, y: usize) -> usize { print!( "\u{1b}[{};{}H\u{1b}[m\u{1b}[1;4mhttps://zellij.dev/documentation/themes", y + 1, x + 1 ); 39 } fn theme_link_selected_len() -> usize { 39 } // 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 } } }