1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//! # Application State and Rendering
//!
//! This module contains the `App` struct which represents the application state
//! and provides methods for updating and rendering the application.

use crate::ascii::process_frame;
use crate::error::Result;
use color_eyre::eyre::WrapErr;
use opencv::core::Mat;
use ratatui::{
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Style, Stylize},
    text::{Line, Span},
    widgets::{Block, Borders, Clear, Paragraph},
    Frame,
};

/// Represents the state of the application.
#[derive(Default)]
pub struct App {
    pub ascii_frame: String,
    pub fps: f64,
    pub show_help: bool,
}

impl App {
    /// Creates a new `App` instance with default values.
    ///
    /// # Examples
    ///
    /// ```
    /// use ascii_webcam::app::App;
    /// let app = App::new();
    /// assert_eq!(app.ascii_frame, "");
    /// assert_eq!(app.fps, 0.0);
    /// assert_eq!(app.show_help, false);
    /// ```
    #[must_use]
    pub fn new() -> App {
        App {
            ascii_frame: String::new(),
            fps: 0.0,
            show_help: false,
        }
    }

    /// Updates the application state with a new video frame.
    ///
    /// # Arguments
    ///
    /// * `frame` - The video frame to process
    /// * `width` - The width to resize the frame to
    /// * `height` - The height to resize the frame to
    ///
    /// # Returns
    ///
    /// Returns `Ok(())` if the update was successful.
    ///
    /// # Errors
    ///
    /// This function may return an error if:
    /// - The frame processing fails
    /// - There are issues with resizing or converting the frame
    pub fn update(&mut self, frame: &Mat, width: i32, height: i32) -> Result<()> {
        self.ascii_frame =
            process_frame(frame, width, height).wrap_err("failed to process frame")?;
        Ok(())
    }

    /// Toggles the visibility of the help menu.
    pub fn toggle_help(&mut self) {
        self.show_help = !self.show_help;
    }

    /// Renders the application UI.
    ///
    /// This method is responsible for rendering:
    /// - The FPS counter
    /// - The ASCII video frame
    /// - The instruction text
    /// - The help menu (if visible)
    pub fn render(&self, f: &mut Frame) {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(3),
                Constraint::Min(0),
                Constraint::Length(1),
            ])
            .split(f.area());

        let fps_text = format!("FPS: {:.2}", self.fps);
        let fps_paragraph = Paragraph::new(fps_text)
            .style(Style::default().fg(Color::Cyan))
            .block(Block::default().borders(Borders::ALL).title("Stats"));

        f.render_widget(fps_paragraph, chunks[0]);

        let ascii_block = Block::default().borders(Borders::ALL).title("ASCII Webcam");
        let ascii_paragraph = Paragraph::new(self.ascii_frame.as_str()).block(ascii_block);

        f.render_widget(ascii_paragraph, chunks[1]);

        let instructions = Line::from(vec![
            "Quit".into(),
            " <q>".blue().bold(),
            " | Help".into(),
            " <?>".blue().bold(),
        ]);
        let instructions_paragraph = Paragraph::new(instructions)
            .style(Style::default().fg(Color::White))
            .alignment(ratatui::layout::Alignment::Center);

        f.render_widget(instructions_paragraph, chunks[2]);

        if self.show_help {
            self.render_help(f);
        }
    }

    /// Renders the help menu.
    #[allow(clippy::unused_self)]
    fn render_help(&self, f: &mut Frame) {
        let area = f.area();
        let help_area = Rect::new(
            area.width / 4,
            area.height / 4,
            area.width / 2,
            area.height / 2,
        );

        f.render_widget(Clear, help_area);

        let help_text = vec![
            Line::from("Help"),
            Line::from(""),
            Line::from(vec![
                Span::raw("Press "),
                Span::styled(
                    "q",
                    Style::default()
                        .fg(Color::Blue)
                        .add_modifier(ratatui::style::Modifier::BOLD),
                ),
                Span::raw(" to quit the application"),
            ]),
            Line::from(vec![
                Span::raw("Press "),
                Span::styled(
                    "?",
                    Style::default()
                        .fg(Color::Blue)
                        .add_modifier(ratatui::style::Modifier::BOLD),
                ),
                Span::raw(" to toggle this help menu"),
            ]),
        ];

        let help_paragraph = Paragraph::new(help_text)
            .block(Block::default().title("Help").borders(Borders::ALL))
            .alignment(ratatui::layout::Alignment::Center);

        f.render_widget(help_paragraph, help_area);
    }
}