1use crate::ascii::process_frame;
7use crate::error::Result;
8use color_eyre::eyre::WrapErr;
9use opencv::core::Mat;
10use ratatui::{
11 layout::{Constraint, Direction, Layout, Rect},
12 style::{Color, Style, Stylize},
13 text::{Line, Span},
14 widgets::{Block, Borders, Clear, Paragraph},
15 Frame,
16};
17
18#[derive(Default)]
20pub struct App {
21 pub ascii_frame: String,
22 pub fps: f64,
23 pub show_help: bool,
24}
25
26impl App {
27 #[must_use]
39 pub fn new() -> App {
40 App {
41 ascii_frame: String::new(),
42 fps: 0.0,
43 show_help: false,
44 }
45 }
46
47 pub fn update(&mut self, frame: &Mat, width: i32, height: i32) -> Result<()> {
65 self.ascii_frame =
66 process_frame(frame, width, height).wrap_err("failed to process frame")?;
67 Ok(())
68 }
69
70 pub fn toggle_help(&mut self) {
72 self.show_help = !self.show_help;
73 }
74
75 pub fn render(&self, f: &mut Frame) {
83 let chunks = Layout::default()
84 .direction(Direction::Vertical)
85 .constraints([
86 Constraint::Length(3),
87 Constraint::Min(0),
88 Constraint::Length(1),
89 ])
90 .split(f.area());
91
92 let fps_text = format!("FPS: {:.2}", self.fps);
93 let fps_paragraph = Paragraph::new(fps_text)
94 .style(Style::default().fg(Color::Cyan))
95 .block(Block::default().borders(Borders::ALL).title("Stats"));
96
97 f.render_widget(fps_paragraph, chunks[0]);
98
99 let ascii_block = Block::default().borders(Borders::ALL).title("ASCII Webcam");
100 let ascii_paragraph = Paragraph::new(self.ascii_frame.as_str()).block(ascii_block);
101
102 f.render_widget(ascii_paragraph, chunks[1]);
103
104 let instructions = Line::from(vec![
105 "Quit".into(),
106 " <q>".blue().bold(),
107 " | Help".into(),
108 " <?>".blue().bold(),
109 ]);
110 let instructions_paragraph = Paragraph::new(instructions)
111 .style(Style::default().fg(Color::White))
112 .alignment(ratatui::layout::Alignment::Center);
113
114 f.render_widget(instructions_paragraph, chunks[2]);
115
116 if self.show_help {
117 self.render_help(f);
118 }
119 }
120
121 #[allow(clippy::unused_self)]
123 fn render_help(&self, f: &mut Frame) {
124 let area = f.area();
125 let help_area = Rect::new(
126 area.width / 4,
127 area.height / 4,
128 area.width / 2,
129 area.height / 2,
130 );
131
132 f.render_widget(Clear, help_area);
133
134 let help_text = vec![
135 Line::from("Help"),
136 Line::from(""),
137 Line::from(vec![
138 Span::raw("Press "),
139 Span::styled(
140 "q",
141 Style::default()
142 .fg(Color::Blue)
143 .add_modifier(ratatui::style::Modifier::BOLD),
144 ),
145 Span::raw(" to quit the application"),
146 ]),
147 Line::from(vec![
148 Span::raw("Press "),
149 Span::styled(
150 "?",
151 Style::default()
152 .fg(Color::Blue)
153 .add_modifier(ratatui::style::Modifier::BOLD),
154 ),
155 Span::raw(" to toggle this help menu"),
156 ]),
157 ];
158
159 let help_paragraph = Paragraph::new(help_text)
160 .block(Block::default().title("Help").borders(Borders::ALL))
161 .alignment(ratatui::layout::Alignment::Center);
162
163 f.render_widget(help_paragraph, help_area);
164 }
165}