diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..875cb80 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,166 @@ +use figlet_rs::Toilet; +use ratatui::{ + Frame, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + widgets::{Block, Borders, Clear, Paragraph}, +}; + +use crate::app::{Termato, TimerMode}; +use crate::audio::AudioVisualizer; + +// use unicode blocks for rendering +// Empty -> " " +// 1/8th height -> "▂" +// 2/8th height -> "▃" +// 3/8th height -> "▄" +// 4/8th height -> "▅" +// 5/8th height -> "▆" +// 6/8th height -> "▇" +// Full Height -> "█" +pub fn render_visualizer(f: &mut Frame, area: Rect, bar_values: &[u64]) { + const BLOCKS: [&str; 8] = ["▂", "▃", "▄", "▅", "▆", "▇", "█", "█"]; + + let max_bars = area.width as usize; + let line: String = bar_values + .iter() + .take(max_bars) + .map(|&val| { + if val == 0 { + " " + } else { + let idx = ((val as f32 / 100.0) * (BLOCKS.len() - 1) as f32) + .clamp(0.0, (BLOCKS.len() - 1) as f32) as usize; + + BLOCKS[idx] + } + }) + .collect(); + + let viz_paragraph = Paragraph::new(line) + .style(Style::default().fg(Color::Rgb(200, 184, 224))) + .alignment(Alignment::Center); + + f.render_widget(viz_paragraph, area); +} + +pub fn render_app( + app: &Termato, + f: &mut Frame, + font: &Toilet, + visualizer: Option<&AudioVisualizer>, +) { + let size = f.area(); + + let minutes = app.time_remaining_in_sec / 60; + let seconds = app.time_remaining_in_sec % 60; + let time_str = format!("{:02}:{:02}", minutes, seconds); + + let time_color = if !app.is_running { + Color::Rgb(212, 200, 122) + } else { + match app.mode { + TimerMode::Work => Color::Rgb(212, 115, 115), + TimerMode::Break => Color::Rgb(126, 200, 192), + } + }; + + let raw_time_text = if let Some(fig) = font.convert(&time_str) { + fig.to_string() + } else { + time_str + }; + + let trimmed_time_text = raw_time_text.trim_matches('\n'); + let text_lines: Vec<&str> = trimmed_time_text.lines().collect(); + let text_height = text_lines.len() as u16; + + let viz_height = if visualizer.is_some() { 1 } else { 0 }; + let viz_width = 64; + + let content_height = text_height + viz_height; + + let outer_vertical = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Min(0), + Constraint::Length(content_height), + Constraint::Min(0), + ]) + .split(size); + + let inner_vertical = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(text_height), + Constraint::Length(viz_height), + ]) + .split(outer_vertical[1]); + + let viz_horizontal = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Min(0), + Constraint::Length(viz_width.min(size.width)), + Constraint::Min(0), + ]) + .split(inner_vertical[1]); + + let timer_paragraph = Paragraph::new(trimmed_time_text) + .style(Style::default().fg(time_color)) + .alignment(Alignment::Center); + + f.render_widget(timer_paragraph, inner_vertical[0]); + + if let Some(viz) = visualizer + && let Ok(bar_data) = viz.bar_data.lock() + { + render_visualizer(f, viz_horizontal[1], &bar_data); + } + + if app.show_help { + let popup_width = 44; + let popup_height = 7; + + if size.width >= popup_width && size.height >= popup_height { + let popup_vertical = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length((size.height.saturating_sub(popup_height)) / 2), + Constraint::Length(popup_height), + Constraint::Min(1), + ]) + .split(size); + + let popup_horizontal = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length((size.width.saturating_sub(popup_width)) / 2), + Constraint::Length(popup_width), + Constraint::Min(1), + ]) + .split(popup_vertical[1]); + + let popup_area = popup_horizontal[1]; + + let help_block = Block::default() + .title(" Controls ") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Rgb(58, 62, 70))); + + let help_content = Paragraph::new( + "[Space] Pause / Resume\n\ + [S] Switch Mode\n\ + [R] Reset Timer\n\ + [?] Toggle Help\n\ + [Q] Quit", + ) + .block(help_block) + .style(Style::default().fg(Color::Rgb(224, 228, 232))) + .alignment(Alignment::Left); + + f.render_widget(Clear, popup_area); + f.render_widget(help_content, popup_area); + } + } +}