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);
}
}