use crate::app::App; use miette::ErrReport; use ratatui::{ Frame, layout::{Constraint, Direction, Layout, Rect}, style::{Style, Stylize}, symbols::border, text::{Line, Span, Text}, widgets::{Block, Clear, Paragraph, Widget}, }; pub fn draw(frame: &mut Frame, app: &App) { let rect = draw_layout(app); frame.render_widget(rect, frame.area()); if let Some(error) = app.get_error() { let popup = error_popup(error); let area = centered_rect(40, 25, frame.area()); frame.render_widget(Clear, area); frame.render_widget(popup, area); } } fn draw_layout(app: &App) -> impl Widget { let title = Line::from(" Counter App Tutorial ".bold()); let instructions = Line::from(vec![ " Decrement ".into(), "".blue().bold(), " Increment ".into(), "".blue().bold(), " Quit ".into(), " ".blue().bold(), ]); let block = Block::bordered() .title(title.centered()) .title_bottom(instructions.centered()) .border_set(border::THICK); let counter_text = Text::from(vec![Line::from(vec![ "Value: ".into(), app.display_counter().yellow(), ])]); Paragraph::new(counter_text).centered().block(block) } /// helper function to create a centered rect using up certain percentage of the available rect `r` fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { // Cut the given rectangle into three vertical pieces let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage(percent_y), Constraint::Percentage((100 - percent_y) / 2), ]) .split(r); // Then cut the middle vertical piece into three width-wise pieces Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage((100 - percent_x) / 2), Constraint::Percentage(percent_x), Constraint::Percentage((100 - percent_x) / 2), ]) .split(popup_layout[1])[1] // Return the middle chunk } fn error_popup(error: &ErrReport) -> impl Widget { let title = Line::from("Error").bold(); let popup = Block::bordered() .title(title.centered()) .border_set(border::DOUBLE); let error_text = error_text(error); Paragraph::new(error_text).block(popup) } fn error_text(error: &ErrReport) -> Text<'_> { let errname_spans = vec![ Span::raw("Error "), Span::styled("x", Style::new().red()), Span::raw(" "), Span::raw(error.to_string()), ]; let line1 = Line::default().spans(errname_spans); let help_spans = vec![ Span::styled(" help: ", Style::new().cyan()), Span::raw(error.help().map_or("".into(), |x| x.to_string())), ]; let line2 = Line::default().spans(help_spans); Text::from(vec![line1, line2]) } #[cfg(test)] mod tests { use super::*; use ratatui::{buffer::Buffer, layout::Rect, style::Style}; #[test] fn render_success() { let mut app = App::default(); assert!(app.init_context().is_ok()); let rect = Rect::new(0, 0, 50, 4); let mut buf = Buffer::empty(rect); let res = draw_layout(&app); res.render(rect, &mut buf); let mut expected = Buffer::with_lines(vec![ "┏━━━━━━━━━━━━━ Counter App Tutorial ━━━━━━━━━━━━━┓", "┃ Value: 0 ┃", "┃ ┃", "┗━ Decrement Increment Quit ━━┛", ]); let title_style = Style::new().bold(); let counter_style = Style::new().yellow(); let key_style = Style::new().blue().bold(); expected.set_style(Rect::new(14, 0, 22, 1), title_style); expected.set_style(Rect::new(28, 1, 1, 1), counter_style); expected.set_style(Rect::new(13, 3, 6, 1), key_style); expected.set_style(Rect::new(30, 3, 7, 1), key_style); expected.set_style(Rect::new(43, 3, 4, 1), key_style); assert_eq!(buf, expected); } }