ascii_webcam/
app.rs

1//! # Application State and Rendering
2//!
3//! This module contains the `App` struct which represents the application state
4//! and provides methods for updating and rendering the application.
5
6use 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/// Represents the state of the application.
19#[derive(Default)]
20pub struct App {
21    pub ascii_frame: String,
22    pub fps: f64,
23    pub show_help: bool,
24}
25
26impl App {
27    /// Creates a new `App` instance with default values.
28    ///
29    /// # Examples
30    ///
31    /// ```
32    /// use ascii_webcam::app::App;
33    /// let app = App::new();
34    /// assert_eq!(app.ascii_frame, "");
35    /// assert_eq!(app.fps, 0.0);
36    /// assert_eq!(app.show_help, false);
37    /// ```
38    #[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    /// Updates the application state with a new video frame.
48    ///
49    /// # Arguments
50    ///
51    /// * `frame` - The video frame to process
52    /// * `width` - The width to resize the frame to
53    /// * `height` - The height to resize the frame to
54    ///
55    /// # Returns
56    ///
57    /// Returns `Ok(())` if the update was successful.
58    ///
59    /// # Errors
60    ///
61    /// This function may return an error if:
62    /// - The frame processing fails
63    /// - There are issues with resizing or converting the frame
64    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    /// Toggles the visibility of the help menu.
71    pub fn toggle_help(&mut self) {
72        self.show_help = !self.show_help;
73    }
74
75    /// Renders the application UI.
76    ///
77    /// This method is responsible for rendering:
78    /// - The FPS counter
79    /// - The ASCII video frame
80    /// - The instruction text
81    /// - The help menu (if visible)
82    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    /// Renders the help menu.
122    #[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}