use std::time::{Duration, Instant}; use managarr_tree_widget::{Tree, TreeItem, TreeState}; use ratatui::backend::{Backend, CrosstermBackend}; use ratatui::crossterm::event::{Event, KeyCode, KeyModifiers, MouseEventKind}; use ratatui::layout::{Position, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::Span; use ratatui::widgets::{Block, Scrollbar, ScrollbarOrientation}; use ratatui::{crossterm, Frame, Terminal}; #[must_use] struct App { state: TreeState, items: Vec>, } impl App { fn new() -> Self { Self { state: TreeState::default(), items: vec![ TreeItem::new_leaf("Alfa"), TreeItem::new( "Bravo", vec![ TreeItem::new_leaf("Charlie"), TreeItem::new( "Delta", vec![TreeItem::new_leaf("Echo"), TreeItem::new_leaf("Foxtrot")], ) .expect("all item identifiers are unique"), TreeItem::new_leaf("Golf"), ], ) .expect("all item identifiers are unique"), TreeItem::new_leaf("Hotel"), TreeItem::new( "India", vec![ TreeItem::new_leaf("Juliet"), TreeItem::new_leaf("Kilo"), TreeItem::new_leaf("Lima"), TreeItem::new_leaf("Mike"), TreeItem::new_leaf("November"), ], ) .expect("all item identifiers are unique"), TreeItem::new_leaf("Oscar"), TreeItem::new( "Papa", vec![ TreeItem::new_leaf("Quebec"), TreeItem::new_leaf("Romeo"), TreeItem::new_leaf("Sierra"), TreeItem::new_leaf("Tango"), TreeItem::new_leaf("Uniform"), TreeItem::new( "Victor", vec![ TreeItem::new_leaf("Whiskey"), TreeItem::new_leaf("Xray"), TreeItem::new_leaf("Yankee"), ], ) .expect("all item identifiers are unique"), ], ) .expect("all item identifiers are unique"), TreeItem::new_leaf("Zulu"), ], } } fn draw(&mut self, frame: &mut Frame) { let area = frame.area(); let widget = Tree::new(&self.items) .expect("all item identifiers are unique") .block( Block::bordered() .title("Tree Widget") .title_bottom(format!("{:?}", self.state)), ) .experimental_scrollbar(Some( Scrollbar::new(ScrollbarOrientation::VerticalRight) .begin_symbol(None) .track_symbol(None) .end_symbol(None), )) .highlight_style( Style::new() .fg(Color::Black) .bg(Color::LightGreen) .add_modifier(Modifier::BOLD), ) .highlight_symbol(">> "); frame.render_stateful_widget(widget, area, &mut self.state); } } fn main() -> std::io::Result<()> { // Terminal initialization crossterm::terminal::enable_raw_mode()?; let mut stdout = std::io::stdout(); crossterm::execute!( stdout, crossterm::terminal::EnterAlternateScreen, crossterm::event::EnableMouseCapture )?; let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?; // App let app = App::new(); let res = run_app(&mut terminal, app); // restore terminal crossterm::terminal::disable_raw_mode()?; crossterm::execute!( terminal.backend_mut(), crossterm::terminal::LeaveAlternateScreen, crossterm::event::DisableMouseCapture )?; terminal.show_cursor()?; if let Err(err) = res { println!("{err:?}"); } Ok(()) } fn run_app(terminal: &mut Terminal, mut app: App) -> std::io::Result<()> { const DEBOUNCE: Duration = Duration::from_millis(20); // 50 FPS let before = Instant::now(); terminal.draw(|frame| app.draw(frame))?; let mut last_render_took = before.elapsed(); let mut debounce: Option = None; loop { let timeout = debounce.map_or(DEBOUNCE, |start| DEBOUNCE.saturating_sub(start.elapsed())); if crossterm::event::poll(timeout)? { let update = match crossterm::event::read()? { Event::Key(key) => match key.code { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { return Ok(()) } KeyCode::Char('q') => return Ok(()), KeyCode::Char('\n' | ' ') => app.state.toggle_selected(), KeyCode::Left => app.state.key_left(), KeyCode::Right => app.state.key_right(), KeyCode::Down => app.state.key_down(), KeyCode::Up => app.state.key_up(), KeyCode::Esc => app.state.select(Vec::new()), KeyCode::Home => app.state.select_first(), KeyCode::End => app.state.select_last(), KeyCode::PageDown => app.state.scroll_down(3), KeyCode::PageUp => app.state.scroll_up(3), _ => false, }, Event::Mouse(mouse) => match mouse.kind { MouseEventKind::ScrollDown => app.state.scroll_down(1), MouseEventKind::ScrollUp => app.state.scroll_up(1), MouseEventKind::Down(_button) => { app.state.click_at(Position::new(mouse.column, mouse.row)) } _ => false, }, Event::Resize(_, _) => true, _ => false, }; if update { debounce.get_or_insert_with(Instant::now); } } if debounce.is_some_and(|debounce| debounce.elapsed() > DEBOUNCE) { let before = Instant::now(); terminal.draw(|frame| { app.draw(frame); // Performance info in top right corner { let text = format!( " {} {last_render_took:?} {:.1} FPS", frame.count(), 1.0 / last_render_took.as_secs_f64() ); #[allow(clippy::cast_possible_truncation)] let area = Rect { y: 0, height: 1, x: frame.area().width.saturating_sub(text.len() as u16), width: text.len() as u16, }; frame.render_widget( Span::styled(text, Style::new().fg(Color::Black).bg(Color::Gray)), area, ); } })?; last_render_took = before.elapsed(); debounce = None; } } }