#[path = "common/lib.rs"] mod common; use common::{Colors, Result, Terminal}; use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use ratatui::{ buffer::Buffer, layout::Rect, prelude::*, style::{Color, Style, Styled}, widgets::{Block, BorderType, Borders, Paragraph, Widget}, }; use tui_widget_list::{ListBuilder, ListState, ListView, ScrollAxis}; fn main() -> Result<()> { let mut terminal = Terminal::init()?; App::default().run(&mut terminal).unwrap(); Terminal::reset()?; terminal.show_cursor()?; Ok(()) } #[derive(Default)] struct App {} #[derive(Default)] struct AppState { pub text_list_state: ListState, pub color_list_state: ListState, } impl App { pub fn run(&self, terminal: &mut Terminal) -> Result<()> { let mut state = AppState::default(); loop { terminal.draw_app(self, &mut state)?; if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { match key.code { KeyCode::Char('q') => return Ok(()), KeyCode::Up | KeyCode::Char('k') => state.text_list_state.previous(), KeyCode::Down | KeyCode::Char('j') => state.text_list_state.next(), KeyCode::Left | KeyCode::Char('h') => state.color_list_state.previous(), KeyCode::Right | KeyCode::Char('l') => state.color_list_state.next(), _ => {} } } } } } } impl StatefulWidget for &App { type State = AppState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) where Self: Sized, { use Constraint::{Min, Percentage}; let [top, bottom] = Layout::vertical([Percentage(75), Min(0)]).areas(area); // Text list let colors = demo_colors(); let selected_color = match state.color_list_state.selected { Some(index) => colors[index], None => colors[1], }; let text_list = TextContainer::demo(selected_color); text_list.render(top, buf, &mut state.text_list_state); // Color list let color_list = ColoredContainer::demo(); color_list.render(bottom, buf, &mut state.color_list_state); } } #[derive(Debug, Clone)] pub struct TextContainer { title: String, content: Vec, style: Style, expand: bool, } impl Styled for TextContainer { type Item = Self; fn style(&self) -> Style { self.style } fn set_style>(mut self, style: S) -> Self::Item { self.style = style.into(); self } } impl TextContainer { pub fn new(title: &str, content: Vec) -> Self { Self { title: title.to_string(), content, style: Style::default(), expand: false, } } fn demo(selected_color: Color) -> ListView<'static, TextContainer> { let monday: Vec = vec![ String::from("1. Exercise for 30 minutes"), String::from("2. Work on the project for 2 hours"), String::from("3. Read a book for 1 hour"), String::from("4. Cook dinner"), ]; let tuesday: Vec = vec![ String::from("1. Attend a team meeting at 10 AM"), String::from("2. Reply to emails"), String::from("3. Prepare lunch"), ]; let wednesday: Vec = vec![ String::from("1. Update work tasks"), String::from("2. Conduct code review"), String::from("3. Attend a training"), ]; let thursday: Vec = vec![ String::from("1. Brainstorm for an upcoming project"), String::from("2. Document ideas and refine tasks"), ]; let friday: Vec = vec![ String::from("1. Have a recap meeting"), String::from("2. Attent conference talk"), String::from("3. Go running for 1 hour"), ]; let saturday: Vec = vec![ String::from("1. Work on coding project"), String::from("2. Read a chapter from a book"), String::from("3. Go for a short walk"), ]; let sunday: Vec = vec![ String::from("1. Plan upcoming trip"), String::from("2. Read in the park"), String::from("3. Go to dinner with friends"), ]; let containers = vec![ TextContainer::new("Monday", monday), TextContainer::new("Tuesday", tuesday), TextContainer::new("Wednesday", wednesday), TextContainer::new("Thursday", thursday), TextContainer::new("Friday", friday), TextContainer::new("Saturday", saturday), TextContainer::new("Sunday", sunday), ]; let builder = ListBuilder::new(move |context| { let mut main_axis_size = 2; let mut container = containers[context.index].clone(); if context.index % 2 == 0 { container.style = Style::default().bg(Colors::CHARCOAL); } else { container.style = Style::default().bg(Colors::BLACK); } if context.is_selected { container.style = Style::default().bg(selected_color).fg(Colors::CHARCOAL); container.expand = true; main_axis_size = 3 + container.content.len() as u16; } (container, main_axis_size) }); ListView::new(builder, 7) } } impl Widget for TextContainer { fn render(self, area: Rect, buf: &mut Buffer) { let mut lines = vec![Line::styled(self.title, self.style)]; if self.expand { lines.push(Line::from(String::new())); lines.extend(self.content.into_iter().map(|x| Line::from(x))); lines.push(Line::from(String::new())); } Paragraph::new(lines) .alignment(Alignment::Center) .style(self.style) .render(area, buf); } } struct ColoredContainer { color: Color, border_style: Style, border_type: BorderType, } impl ColoredContainer { fn new(color: Color) -> Self { Self { color, border_style: Style::default(), border_type: BorderType::Plain, } } fn demo() -> ListView<'static, ColoredContainer> { let colors = demo_colors(); let builder = ListBuilder::new(move |context| { let color = demo_colors()[context.index]; let mut widget = ColoredContainer::new(color); if context.is_selected { widget.border_style = Style::default().fg(Color::Black); widget.border_type = BorderType::Thick; }; (widget, 15) }); ListView::new(builder, colors.len()).scroll_axis(ScrollAxis::Horizontal) } } impl Widget for ColoredContainer { fn render(self, area: Rect, buf: &mut Buffer) { Block::default() .borders(Borders::ALL) .border_style(self.border_style) .border_type(self.border_type) .bg(self.color) .render(area, buf); } } fn demo_colors() -> Vec { vec![ Color::Rgb(255, 102, 102), // Neon Red Color::Rgb(255, 153, 0), // Neon Orange Color::Rgb(255, 204, 0), // Neon Yellow Color::Rgb(0, 204, 102), // Neon Green Color::Rgb(0, 204, 255), // Neon Blue Color::Rgb(102, 51, 255), // Neon Purple Color::Rgb(255, 51, 204), // Neon Magenta Color::Rgb(51, 255, 255), // Neon Cyan Color::Rgb(255, 102, 255), // Neon Pink Color::Rgb(102, 255, 255), // Neon Aqua ] }