use super::*;
pub(crate) fn draw_popup(f: &mut Frame, app: &App, popup: &crate::app::Popup) {
let theme = &app.theme;
let icons = app.icons;
let area = f.area();
let popup_area = centered_rect(68, 78, area);
f.render_widget(Clear, popup_area);
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent))
.title(Span::styled(
match popup {
crate::app::Popup::AddTask => format!(" {} Add Task ", icons.plus),
crate::app::Popup::EditTask(_) => format!(" {} Edit Task ", icons.edit),
crate::app::Popup::ConfirmDelete(_) => format!(" {} Confirm Delete ", icons.delete),
crate::app::Popup::EmptyQueueChoice => format!(" {} All Tasks Done ", icons.check),
},
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
));
f.render_widget(block, popup_area);
let inner = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Min(8), Constraint::Length(2)])
.split(popup_area);
match popup {
crate::app::Popup::AddTask | crate::app::Popup::EditTask(_) => {
let (left_area, right_area) = if matches!(app.input_field, InputField::DueDate) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(inner[0]);
(chunks[0], Some(chunks[1]))
} else {
(inner[0], None)
};
let cursor = |active: bool, text: &str| -> String {
if active {
if text.is_empty() {
"|".to_string()
} else {
format!("{}|", text)
}
} else if text.is_empty() {
"—".to_string()
} else {
text.to_string()
}
};
let due_display = if matches!(app.input_field, InputField::DueDate) {
cursor(true, &app.input_due_date)
} else if app.input_due_date.is_empty() {
"—".to_string()
} else {
app.input_due_date.clone()
};
let tags_display = cursor(matches!(app.input_field, InputField::Tags), &app.input_tags);
let p = Paragraph::new(vec![
popup_field_line(
theme,
"Title",
cursor(
matches!(app.input_field, InputField::Title),
&app.input_buffer,
),
matches!(app.input_field, InputField::Title),
),
popup_field_line(
theme,
"Notes",
cursor(
matches!(app.input_field, InputField::Notes),
&app.input_buffer2,
),
matches!(app.input_field, InputField::Notes),
),
popup_field_line(
theme,
"Estimate (min)",
if matches!(app.input_field, InputField::Estimate) {
format!("{}|", app.input_number)
} else {
app.input_number.to_string()
},
matches!(app.input_field, InputField::Estimate),
),
popup_field_line(
theme,
"Priority",
app.input_priority.label().to_string(),
matches!(app.input_field, InputField::Priority),
),
popup_field_line(
theme,
"Due (YYYY-MM-DD)",
due_display,
matches!(app.input_field, InputField::DueDate),
),
popup_field_line(
theme,
"Tags (comma-sep)",
tags_display,
matches!(app.input_field, InputField::Tags),
),
]);
f.render_widget(p, left_area);
if let Some(r) = right_area {
let d = app.calendar_date;
if let Ok(time_date) = time::Date::from_calendar_date(
d.year(),
time::Month::try_from(d.month() as u8).unwrap_or(time::Month::January),
d.day() as u8,
) {
let mut store = ratatui::widgets::calendar::CalendarEventStore::default();
store.add(
time_date,
Style::default()
.bg(theme.accent)
.fg(theme.on_accent)
.add_modifier(Modifier::BOLD),
);
let monthly = ratatui::widgets::calendar::Monthly::new(time_date, store)
.show_month_header(
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)
.show_weekdays_header(Style::default().fg(theme.dim));
f.render_widget(monthly, r);
}
}
}
crate::app::Popup::ConfirmDelete(id) => {
let title = app
.data
.tasks
.iter()
.find(|t| t.id == *id)
.map(|t| t.title.as_str())
.unwrap_or("Unknown task");
let p = Paragraph::new(vec![
Line::from(Span::styled(
format!("Delete \"{}\"?", title),
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(Span::styled(
"Press y or Enter to confirm, n or Esc to cancel.",
Style::default().fg(theme.dim),
)),
Line::from(Span::styled(
"This cannot be undone.",
Style::default().fg(theme.error),
)),
]);
f.render_widget(p, inner[0]);
}
crate::app::Popup::EmptyQueueChoice => {
let p = Paragraph::new(vec![
Line::from(Span::styled(
"You've completed every task in your queue.",
Style::default().fg(theme.text),
)),
Line::from(""),
Line::from(Span::styled(
"[Enter] Continue free focus (log general sessions)",
Style::default().fg(theme.success),
)),
Line::from(Span::styled(
"[p] Pause the timer",
Style::default().fg(theme.warning),
)),
Line::from(Span::styled(
"[a] Add another task",
Style::default().fg(theme.accent),
)),
Line::from(Span::styled(
"[Esc] Dismiss",
Style::default().fg(theme.dim),
)),
]);
f.render_widget(p, inner[0]);
}
}
if matches!(
popup,
crate::app::Popup::AddTask | crate::app::Popup::EditTask(_)
) {
let hint = if matches!(app.input_field, InputField::DueDate) {
"←→: day · ↑↓: week · Tab: field · Enter: save · Esc: cancel"
} else {
"Tab/Shift+Tab: field · Enter: save · Esc: cancel"
};
f.render_widget(
Paragraph::new(Span::styled(hint, Style::default().fg(theme.dim))),
inner[1],
);
}
}
pub(crate) fn popup_field_line(
theme: &crate::app::Theme,
label: &str,
value: String,
active: bool,
) -> Line<'static> {
let label_style = if active {
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.dim)
};
let value_style = if active {
Style::default().fg(theme.text).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.text)
};
Line::from(vec![
Span::styled(format!("{:<20} ", label), label_style),
Span::styled(value, value_style),
])
}
pub(crate) fn draw_input(f: &mut Frame, app: &App, area: Rect) {
let theme = &app.theme;
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Length(3), Constraint::Min(1)])
.split(area);
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent))
.title(Span::styled(" Input ", Style::default().fg(theme.accent)));
let p = Paragraph::new(format!("{}|", app.input_buffer))
.style(Style::default().fg(theme.text))
.block(block);
f.render_widget(p, chunks[0]);
}