use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem},
Frame,
};
use crate::{
app::{App, TaskColumn},
types::TaskStatus,
ui::components::{
focused_border_style, render_header, render_hints, render_status_bar, selected_style,
unfocused_border_style,
},
};
pub fn render(frame: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2), Constraint::Min(10), Constraint::Length(2), Constraint::Length(2), ])
.split(frame.area());
let title = if let Some(ref project) = app.selected_project {
format!("Tasks - {}", project.name)
} else {
"Tasks".to_string()
};
render_header(frame, chunks[0], &title);
let board_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(chunks[1]);
render_column(frame, board_chunks[0], app, TaskColumn::Todo);
render_column(frame, board_chunks[1], app, TaskColumn::InProgress);
render_column(frame, board_chunks[2], app, TaskColumn::InReview);
render_column(frame, board_chunks[3], app, TaskColumn::Done);
render_hints(
frame,
chunks[2],
&[
("←/→", "Column"),
("↑/↓", "Task"),
("Enter", "View"),
("n", "New Task"),
("m", "Move"),
("Esc", "Back"),
],
);
render_status_bar(frame, chunks[3], app);
}
fn render_column(frame: &mut Frame, area: Rect, app: &App, column: TaskColumn) {
let is_focused = app.selected_column == column;
let column_index = match column {
TaskColumn::Todo => 0,
TaskColumn::InProgress => 1,
TaskColumn::InReview => 2,
TaskColumn::Done => 3,
};
let selected_index = app.selected_task_indices[column_index];
let tasks = app.tasks_for_column(column);
let items: Vec<ListItem> = tasks
.iter()
.enumerate()
.map(|(i, task)| {
let is_selected = is_focused && i == selected_index;
let style = if is_selected {
selected_style()
} else {
Style::default()
};
let marker = if is_selected { "▸ " } else { " " };
let status_indicator = if task.has_in_progress_attempt {
Span::styled("● ", Style::default().fg(Color::Green))
} else if task.last_attempt_failed {
Span::styled("✗ ", Style::default().fg(Color::Red))
} else {
Span::raw(" ")
};
let max_len = area.width.saturating_sub(8) as usize;
let title = if task.task.title.len() > max_len {
format!("{}...", &task.task.title[..max_len.saturating_sub(3)])
} else {
task.task.title.clone()
};
ListItem::new(Line::from(vec![
Span::styled(marker, style),
status_indicator,
Span::styled(title, style),
]))
})
.collect();
let border_style = if is_focused {
focused_border_style()
} else {
unfocused_border_style()
};
let title_style = if is_focused {
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Gray)
};
let title = format!(" {} ({}) ", column.title(), tasks.len());
let list = List::new(items).block(
Block::default()
.title(Span::styled(title, title_style))
.borders(Borders::ALL)
.border_style(border_style),
);
frame.render_widget(list, area);
}
#[allow(dead_code)]
fn status_color(status: TaskStatus) -> Color {
match status {
TaskStatus::Todo => Color::Gray,
TaskStatus::Inprogress => Color::Yellow,
TaskStatus::Inreview => Color::Magenta,
TaskStatus::Done => Color::Green,
TaskStatus::Cancelled => Color::Red,
}
}