use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::Frame;
use crate::app::{App, FocusTab};
use crate::model::TimerState;
use crate::storage;
use crate::ui::IconSet;
use super::widgets::{active_task_spans, chip, format_minutes};
pub fn draw_header(f: &mut Frame, app: &App, area: Rect) {
let theme = &app.theme;
let icons = app.icons;
let version = env!("CARGO_PKG_VERSION");
let (timer_icon, timer_color) = match app.timer.state {
TimerState::Running => (icons.play, theme.accent),
TimerState::Paused => (icons.pause, theme.warning),
TimerState::Finished => (icons.check, theme.success),
TimerState::Idle => (icons.idle, theme.dim),
};
let timer_text = if app.timer.state == TimerState::Idle {
"idle".to_string()
} else {
app.timer.format_remaining()
};
let today = storage::today_focus_minutes(&app.data);
let goal = app.data.daily_goal_minutes;
let goal_met = app.daily_goal_met();
let mut chips: Vec<Span> = vec![
chip(timer_icon, timer_text, timer_color, theme.panel_border),
Span::raw(" "),
chip(
icons.fire,
format!("{}d", app.data.streak_days),
theme.success,
theme.panel_border,
),
Span::raw(" "),
chip(
icons.target,
format!("{}/{}", format_minutes(today), format_minutes(goal)),
if goal_met { theme.success } else { theme.text },
theme.panel_border,
),
];
if app.timer.state != TimerState::Idle {
chips.extend([
Span::raw(" "),
chip(
icons.cycle,
app.timer.cycle_label(),
theme.info,
theme.panel_border,
),
]);
}
if app.queue_empty() && !app.data.tasks.is_empty() {
chips.extend([
Span::raw(" "),
chip(
icons.check,
"queue clear".into(),
theme.success,
theme.panel_border,
),
]);
}
chips.push(Span::raw(" "));
chips.push(Span::styled(
format!("{} {}", icons.timer, app.data.total_sessions),
Style::default().fg(theme.dim),
));
let title = Line::from(vec![
Span::styled(
format!(" {} Void ", icons.logo),
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
),
Span::styled(format!("v{version}"), Style::default().fg(theme.dim)),
]);
let right = Line::from(chips);
let block = Block::default()
.borders(Borders::BOTTOM)
.border_style(Style::default().fg(theme.panel_border))
.title(title)
.title_alignment(Alignment::Left)
.title(right.alignment(Alignment::Right));
f.render_widget(block, area);
}
pub fn draw_tabs(f: &mut Frame, app: &App, area: Rect) {
let theme = &app.theme;
let pending = app.pending_task_count();
let tabs: Vec<Span> = FocusTab::all()
.iter()
.enumerate()
.map(|(i, t)| {
let num = i + 1;
let icon = tab_icon(app.icons, *t);
let badge = if *t == FocusTab::Tasks && pending > 0 {
format!(" {pending}")
} else {
String::new()
};
let label = format!(" {icon} {num}ยท{}{badge} ", t.label());
if *t == app.tab {
Span::styled(
label,
Style::default()
.fg(theme.on_accent)
.bg(theme.accent)
.add_modifier(Modifier::BOLD),
)
} else {
Span::styled(label, Style::default().fg(theme.dim))
}
})
.collect();
let block = Block::default()
.borders(Borders::BOTTOM)
.border_style(Style::default().fg(theme.panel_border));
f.render_widget(Paragraph::new(Line::from(tabs)).block(block), area);
}
pub fn draw_footer(f: &mut Frame, app: &App, area: Rect) {
let theme = &app.theme;
let msg = if let Some(ref m) = app.status {
m.clone()
} else if app.should_quit {
format!("{} Goodbye!", app.icons.check)
} else {
app.hint()
};
let left_style = if app.status_error {
Style::default().fg(theme.error)
} else {
Style::default().fg(theme.dim)
};
let mut right_width: u16 = 0;
let right_line = if let Some(spans) = active_task_spans(app, theme) {
right_width = spans.iter().map(|s| s.width() as u16).sum::<u16>() + 1;
Some(Line::from(spans))
} else {
None
};
let footer_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(1), Constraint::Length(right_width)])
.split(area);
let top_border = Style::default().fg(theme.panel_border);
let status = Line::from(Span::styled(format!(" {msg}"), left_style));
f.render_widget(
Paragraph::new(status).block(
Block::default()
.borders(Borders::TOP)
.border_style(top_border),
),
footer_layout[0],
);
if let Some(line) = right_line {
f.render_widget(
Paragraph::new(line)
.block(
Block::default()
.borders(Borders::TOP)
.border_style(top_border),
)
.alignment(Alignment::Right),
footer_layout[1],
);
}
}
fn tab_icon(icons: IconSet, tab: FocusTab) -> &'static str {
match tab {
FocusTab::Dashboard => icons.dashboard,
FocusTab::Tasks => icons.tasks,
FocusTab::Stats => icons.stats,
FocusTab::Settings => icons.settings,
FocusTab::Help => icons.help,
}
}