use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Table},
};
use crate::actions::AppAction;
use crate::app::App;
use crate::domain::TodoStatus;
use crate::screens::layout::format_time;
const FOOTER_TOGGLE: &str = "[Space] Toggle";
const FOOTER_EDIT: &str = "[E] Edit";
const FOOTER_BACK: &str = "[Enter/Esc] Back";
const FOOTER_GAP: u16 = 3;
pub fn render_detail(app: &App, frame: &mut ratatui::Frame) {
let [header, body, footer] = detail_regions(frame.area());
if let Some(selected_id) = &app.selected_id {
if let Some(todo) = app.todos.iter().find(|t| &t.id == selected_id) {
render_detail_header(frame, header, todo.title.as_str(), todo.status);
render_detail_body(frame, body, app, todo.status);
}
} else {
frame.render_widget(
Paragraph::new("No todo selected").alignment(Alignment::Center),
body,
);
}
frame.render_widget(
Paragraph::new(format!(
"{} | {} | {}",
FOOTER_TOGGLE, FOOTER_EDIT, FOOTER_BACK
))
.alignment(Alignment::Left),
footer,
);
}
pub fn footer_click_action(
area: Rect,
row: u16,
col: u16,
selected_id: Option<&str>,
) -> Option<AppAction> {
let [_, _, footer] = detail_regions(area);
if footer.height == 0 || row != footer.y {
return None;
}
let toggle_w = FOOTER_TOGGLE.chars().count() as u16;
let edit_w = FOOTER_EDIT.chars().count() as u16;
let back_w = FOOTER_BACK.chars().count() as u16;
let total = toggle_w + FOOTER_GAP + edit_w + FOOTER_GAP + back_w;
if footer.width < total {
return None;
}
let start = footer.x;
let toggle_start = start;
let toggle_end = toggle_start + toggle_w;
let edit_start = toggle_end + FOOTER_GAP;
let edit_end = edit_start + edit_w;
let back_start = edit_end + FOOTER_GAP;
let back_end = back_start + back_w;
if col >= toggle_start && col < toggle_end {
selected_id.map(|id| AppAction::ToggleTodo(id.to_string()))
} else if col >= edit_start && col < edit_end {
selected_id.map(|id| AppAction::EditTodo(id.to_string()))
} else if col >= back_start && col < back_end {
Some(AppAction::CloseDetail)
} else {
None
}
}
fn detail_regions(area: Rect) -> [Rect; 3] {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(1),
])
.split(area);
[chunks[0], chunks[1], chunks[2]]
}
fn render_detail_header(frame: &mut ratatui::Frame, area: Rect, title: &str, status: TodoStatus) {
let is_completed = matches!(status, TodoStatus::Completed);
let title_style = if is_completed {
Style::new().fg(Color::DarkGray)
} else {
Style::new()
};
let status_icon = match status {
TodoStatus::Pending => Span::raw("○ "),
TodoStatus::Completed => Span::raw("◉ "),
};
let status_style = if is_completed {
Style::new().fg(Color::DarkGray)
} else {
Style::new().fg(Color::Green)
};
let header_text = Line::from(vec![
status_icon.style(status_style),
Span::raw(title).style(title_style),
]);
let block = Block::default()
.title(" Detail ")
.title_alignment(Alignment::Left)
.borders(Borders::ALL)
.border_style(Style::new().fg(Color::White));
let paragraph = Paragraph::new(header_text)
.block(block)
.alignment(Alignment::Left);
frame.render_widget(paragraph, area);
}
fn render_detail_body(frame: &mut ratatui::Frame, area: Rect, app: &App, status: TodoStatus) {
if let Some(selected_id) = &app.selected_id {
if let Some(todo) = app.todos.iter().find(|t| &t.id == selected_id) {
let is_completed = matches!(status, TodoStatus::Completed);
let base_style = if is_completed {
Style::new().fg(Color::DarkGray)
} else {
Style::new().fg(Color::White)
};
let priority_str = {
let p = todo.priority.to_char();
if p.is_empty() {
"—".to_string()
} else {
p.to_string()
}
};
let status_str = match status {
TodoStatus::Pending => "Pending",
TodoStatus::Completed => "Completed",
};
let created_time = format_time(&todo.created_at);
let mut rows: Vec<Vec<String>> = vec![
vec!["Status".to_string(), status_str.to_string()],
vec!["Priority".to_string(), priority_str],
vec!["Created".to_string(), created_time],
];
if is_completed {
if let Some(completed_at) = &todo.completed_at {
rows.push(vec!["Completed".to_string(), format_time(completed_at)]);
}
}
let table_rows: Vec<_> = rows
.iter()
.map(|row| {
ratatui::widgets::Row::new(vec![
ratatui::widgets::Cell::from(row[0].as_str()).style(base_style),
ratatui::widgets::Cell::from(row[1].as_str()).style(base_style),
])
})
.collect();
let table = Table::new(table_rows, &[Constraint::Length(12), Constraint::Fill(1)])
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(Color::White))
.title_alignment(Alignment::Left)
.title(" Info "),
)
.column_spacing(1);
let body_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(7),
Constraint::Length(1),
Constraint::Fill(1),
])
.split(area);
let info_area = body_chunks[0];
let note_area = body_chunks[2];
frame.render_widget(table, info_area);
if !todo.note.is_empty() {
let note_paragraph = Paragraph::new(todo.note.as_str())
.block(
Block::default()
.title(" Note ")
.title_alignment(Alignment::Left)
.borders(Borders::ALL)
.border_style(Style::new().fg(Color::White)),
)
.style(base_style);
frame.render_widget(note_paragraph, note_area);
} else {
let note_paragraph = Paragraph::new("(empty)")
.block(
Block::default()
.title(" Note ")
.title_alignment(Alignment::Left)
.borders(Borders::ALL)
.border_style(Style::new().fg(Color::DarkGray))
.title_style(Style::new().fg(Color::DarkGray)),
)
.style(Style::new().fg(Color::DarkGray));
frame.render_widget(note_paragraph, note_area);
}
}
}
}