use std::fmt::Display; use altar::*; #[tokio::main] async fn main() { let mut app = TourApp::default(); app.run(true).await; } #[derive(Clone, Copy, PartialEq, Hash)] enum Tab { ZStack, Frame, List, } impl Display for Tab { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { Tab::Frame => "Frame", Tab::List => "List", Tab::ZStack => "ZStack", }; write!(f, "{}", s) } } impl Tab { const ALL: [Tab; 3] = [Tab::Frame, Tab::List, Tab::ZStack]; fn next(&self) -> Self { let index = Self::ALL.iter().position(|x| x == self).unwrap(); Self::ALL[(index + 1) % Self::ALL.len()] } fn prev(&self) -> Self { let index = Self::ALL.iter().position(|x| x == self).unwrap(); Self::ALL[(index + Self::ALL.len() - 1) % Self::ALL.len()] } } struct FrameTab { alignment: Alignment, } struct ZStackTab { alignment: Alignment, } struct TourApp { tab: Tab, frame_tab: FrameTab, zstack_tab: ZStackTab, list_tab: ListTab, tick: usize, } impl Default for TourApp { fn default() -> Self { Self { tab: Tab::List, frame_tab: FrameTab { alignment: Alignment::CENTER, }, zstack_tab: ZStackTab { alignment: Alignment::TOP, }, list_tab: ListTab { selected_index: 0, items: vec![ "Buy Milk".to_string(), "Buy Bread".to_string(), "Buy Cheese".to_string(), "Buy Butter".to_string(), "Buy Eggs".to_string(), "Buy Flour".to_string(), "Buy Sugar".to_string(), "Buy Salt".to_string(), "Buy Water".to_string(), ], }, tick: 0, } } } enum Message { Tick, } impl AsyncTerminalApp for TourApp { type Message = Message; fn render(&self) -> impl View { let main_view = match self.tab { Tab::Frame => frame_tab_view(&self.frame_tab).as_any(), Tab::List => list_tab_view(&self.list_tab).as_any(), Tab::ZStack => stack_tab_view(self.tick, &self.zstack_tab).as_any(), }; vstack((main_view, tab_bar_view(self.tab))) } fn update( &mut self, event: Event, _sender: &tokio::sync::mpsc::UnboundedSender, ) -> bool { match event { Event::Key(key_event) => { if key_event.code == KeyCode::Char('q') { return false; } if key_event.code == KeyCode::Tab { self.tab = self.tab.next(); } else if key_event.code == KeyCode::BackTab { self.tab = self.tab.prev(); } match self.tab { Tab::Frame => handle_key_frame_tab(&mut self.frame_tab, key_event), Tab::List => handle_key_list_tab(&mut self.list_tab, key_event), Tab::ZStack => handle_key_zstack_tab(&mut self.zstack_tab, key_event), } } Event::Message(Message::Tick) => { self.tick = self.tick.wrapping_add(1); } } true } fn init(&mut self, sender: &tokio::sync::mpsc::UnboundedSender) { let sender_clone = sender.clone(); tokio::spawn(async move { let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(17)); loop { interval.tick().await; sender_clone.send(Message::Tick).unwrap(); } }); } } /// Renders the tabs for the main view. The active tab is green and underlined. fn tab_bar_view(tab: Tab) -> impl View { let tabs = Tab::ALL .iter() .map(|t| { let is_active = *t == tab; let color = if is_active { Color::Green } else { Color::Reset }; t.to_string() .to_uppercase() .underline_when(is_active) .color(color) .id(t) }) .collect::>(); hstack(("PRESS TAB".dim(), hstack(tabs))) .fill_horizontally() .border() .border_style(BorderStyle::Rounded) .title(" TABS ") } /// FRAME TAB fn handle_key_frame_tab(tab: &mut FrameTab, event: KeyEvent) { tab.alignment = modify_alignment_with_key(tab.alignment, event.code); } fn frame_tab_view(frame_tab: &FrameTab) -> impl View { vstack(( "FRAME", format!("Alignment: {}", frame_tab.alignment).green(), )) .alignment(frame_tab.alignment.horizontal) .frame( None, None, Some(u16::MAX), Some(u16::MAX), frame_tab.alignment, ) .border() .border_style(BorderStyle::Rounded) .title(" FRAME ") } /// LIST TAB pub struct ListTab { selected_index: usize, items: Vec, } pub fn handle_key_list_tab(tab: &mut ListTab, event: KeyEvent) { match event.code { KeyCode::Down => { tab.selected_index = tab .selected_index .saturating_add(1) .clamp(0, tab.items.len() - 1) } KeyCode::Up => { tab.selected_index = tab .selected_index .saturating_sub(1) .clamp(0, tab.items.len() - 1) } _ => (), } } pub fn list_tab_view(list_tab: &ListTab) -> impl View { let items = list_tab .items .iter() .enumerate() .map(|(i, x)| { let is_active = i == list_tab.selected_index; let color = if is_active { Color::Green } else { Color::Reset }; let message = if is_active { ">" } else { " " }; hstack(( message.color(color), format!("ITEM {}", i).color(color), text(x), )) .id(i) }) .collect::>(); let selected_item = list_tab.items[list_tab.selected_index].clone(); let selected_item_info_view = vstack(( hstack(("SELECTED ITEM", selected_item.clone())), format!("THE CURRENT ITEM IS {}", selected_item).green(), )) .fill_vertically() .fill_horizontally(); hstack(( scroll_view(items, list_tab.selected_index) .fill_vertically() .border() .border_style(BorderStyle::Rounded) .title(" LIST "), // selected_item_info_view .border() .border_style(BorderStyle::Rounded) .title(" INFO "), )) } // TODO: Improve this and pull it into the library. pub fn scroll_view( views: Vec>, selected_index: usize, ) -> impl View { with_size(move |available_size| { let sizes = views .iter() .map(|view| view.value.size(available_size)) .collect::>(); let mut start_offset = 0; let mut end_offset = 0; let mut includes_selected = false; while !includes_selected { let mut total_height = 0; for (i, size) in sizes.iter().enumerate().skip(start_offset) { total_height += size.height; if total_height > available_size.height { break; } end_offset = i; } if (start_offset..=end_offset).contains(&selected_index) { includes_selected = true; } else { start_offset += 1; } } vstack(views[start_offset..=end_offset].to_vec()) }) } /// ZSTACK TAB fn handle_key_zstack_tab(tab: &mut ZStackTab, event: KeyEvent) { tab.alignment = modify_alignment_with_key(tab.alignment, event.code); } fn stack_tab_view(tick: usize, tab: &ZStackTab) -> impl View { let alignment_string = tab.alignment.to_string().to_uppercase(); let color_for_alignment = match tab.alignment.vertical { VerticalAlignment::Top => Color::Green, VerticalAlignment::Center => Color::Blue, VerticalAlignment::Bottom => Color::Red, }; let logo = vstack(( text("████████ ███████ ██████ ███ ███ ██ ███ ██ ██ ██ ███████"), text(" ██ ██ ██ ██ ████ ████ ██ ████ ██ ██ ██ ██ "), text(" ██ █████ ██████ ██ ████ ██ ██ ██ ██ ██ ██ ██ ███████"), text(" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██"), text(" ██ ███████ ██ ██ ██ ██ ██ ██ ████ ██████ ███████"), )) .bold(); let overlay = hstack(("I AM ON ", alignment_string.underline(), " OF THE WORLD!")) .spacing(0) .bold(); let background = with_size(move |size| { fn make_line(line_number: u16, width: u16, tick: usize) -> impl View { let base_string = format!("{} ", (line_number as usize) + tick); let repeat_count = (width as usize + base_string.len() - 1) / base_string.len(); let repeated_string: String = base_string.repeat(repeat_count); repeated_string[..width as usize].to_string() } vstack( (1..=size.height) .map(|line| make_line(line, size.width, tick).id(line)) .collect::>(), ) }); zstack(( background.dim().red(), vstack((logo, overlay)) .alignment(tab.alignment.horizontal) .color(color_for_alignment), )) .alignment(tab.alignment) .border() .border_style(BorderStyle::Rounded) .title(" ZSTACK ") } fn modify_alignment_with_key(alignment: Alignment, code: KeyCode) -> Alignment { match (code, alignment) { (KeyCode::Right, Alignment::CENTER) => Alignment::RIGHT, (KeyCode::Left, Alignment::CENTER) => Alignment::LEFT, (KeyCode::Down, Alignment::CENTER) => Alignment::BOTTOM, (KeyCode::Up, Alignment::CENTER) => Alignment::TOP, (KeyCode::Left, Alignment::RIGHT) => Alignment::CENTER, (KeyCode::Up, Alignment::RIGHT) => Alignment::TOP_RIGHT, (KeyCode::Down, Alignment::RIGHT) => Alignment::BOTTOM_RIGHT, (KeyCode::Right, Alignment::LEFT) => Alignment::CENTER, (KeyCode::Down, Alignment::LEFT) => Alignment::BOTTOM_LEFT, (KeyCode::Up, Alignment::LEFT) => Alignment::TOP_LEFT, (KeyCode::Down, Alignment::TOP) => Alignment::CENTER, (KeyCode::Left, Alignment::TOP) => Alignment::TOP_LEFT, (KeyCode::Right, Alignment::TOP) => Alignment::TOP_RIGHT, (KeyCode::Up, Alignment::BOTTOM) => Alignment::CENTER, (KeyCode::Left, Alignment::BOTTOM) => Alignment::BOTTOM_LEFT, (KeyCode::Right, Alignment::BOTTOM) => Alignment::BOTTOM_RIGHT, (KeyCode::Right, Alignment::TOP_LEFT) => Alignment::TOP, (KeyCode::Down, Alignment::TOP_LEFT) => Alignment::LEFT, (KeyCode::Left, Alignment::TOP_RIGHT) => Alignment::TOP, (KeyCode::Down, Alignment::TOP_RIGHT) => Alignment::RIGHT, (KeyCode::Right, Alignment::BOTTOM_LEFT) => Alignment::BOTTOM, (KeyCode::Up, Alignment::BOTTOM_LEFT) => Alignment::LEFT, (KeyCode::Left, Alignment::BOTTOM_RIGHT) => Alignment::BOTTOM, (KeyCode::Up, Alignment::BOTTOM_RIGHT) => Alignment::RIGHT, _ => alignment, } }