use crate::app::App;
use crate::models::Project;
use ratatui::{
Frame,
layout::{Constraint, Direction, Flex, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem},
};
fn tag_color(tag: &str) -> Color {
let mut hash: u32 = 0;
for byte in tag.bytes() {
hash = hash.wrapping_mul(31).wrapping_add(byte as u32);
}
let colors = [
Color::Rgb(100, 149, 237), Color::Rgb(144, 238, 144), Color::Rgb(255, 182, 193), Color::Rgb(255, 218, 185), Color::Rgb(221, 160, 221), Color::Rgb(173, 216, 230), Color::Rgb(144, 238, 144), Color::Rgb(240, 230, 140), ];
colors[(hash as usize) % colors.len()]
}
pub fn render(f: &mut Frame, area: Rect, project: &Project, is_focused: bool, app: &mut App) {
let border_style = if is_focused {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::DarkGray)
};
let total_count = project.tasks.len();
let done_count = project
.tasks
.iter()
.filter(|t| {
if let Some(last_status) = project.statuses.last() {
t.status == last_status.name
} else {
false
}
})
.count();
let project_type_label = match project.project_type {
crate::models::ProjectType::Global => "[G]",
crate::models::ProjectType::Local => "[L]",
};
let title = format!(
" {} {} ({}/{}) ",
project_type_label, project.name, done_count, total_count
);
let block = Block::default()
.title(title)
.title_alignment(ratatui::layout::Alignment::Center)
.borders(Borders::ALL)
.border_style(border_style)
.border_type(ratatui::widgets::BorderType::Rounded);
let inner = block.inner(area);
f.render_widget(block, area);
let num_columns = project.statuses.len();
if num_columns == 0 {
return;
}
let constraints: Vec<Constraint> =
if let Some(widths) = app.config.column_widths.get(&project.name) {
if widths.len() == num_columns {
widths.iter().map(|&w| Constraint::Percentage(w)).collect()
} else {
vec![Constraint::Fill(1); num_columns]
}
} else if let Some(Some(max_col)) = app.config.maximized_column.get(&project.name) {
(0..num_columns)
.map(|i| {
if i == *max_col {
Constraint::Percentage(90)
} else {
let remaining = if num_columns > 1 {
10 / (num_columns - 1) as u16
} else {
0
};
Constraint::Percentage(remaining)
}
})
.collect()
} else {
vec![Constraint::Fill(1); num_columns]
};
let columns = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.flex(Flex::Start)
.split(inner);
for (col_idx, status) in project.statuses.iter().enumerate() {
let tasks: Vec<_> = project
.tasks
.iter()
.filter(|t| t.status == status.name)
.collect();
render_column(
f,
columns[col_idx],
&status.display,
&tasks,
col_idx,
app,
is_focused,
project,
);
}
}
fn render_column(
f: &mut Frame,
area: Rect,
title: &str,
tasks: &[&crate::models::Task],
column_idx: usize,
app: &mut App,
is_pane_focused: bool,
project: &Project,
) {
let current_column = app
.selected_column
.get(&app.focused_pane)
.copied()
.unwrap_or(0);
let is_column_focused = is_pane_focused && current_column == column_idx;
let (border_color, title_style) = if is_column_focused {
(
Color::White,
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)
} else {
(Color::DarkGray, Style::default().fg(Color::Gray))
};
let items: Vec<ListItem> = tasks
.iter()
.enumerate()
.map(|(i, task)| {
let selected_idx = app
.selected_task_index
.get(&app.focused_pane)
.copied()
.unwrap_or(0);
let is_selected = is_column_focused && i == selected_idx;
let style = if is_selected {
Style::default()
.bg(Color::Rgb(41, 98, 218))
.fg(Color::White)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
let priority_indicator = match task.priority.as_deref() {
Some("high") => Span::styled("● ", Style::default().fg(Color::Red)),
Some("medium") => Span::styled("● ", Style::default().fg(Color::Yellow)),
Some("low") => Span::styled("● ", Style::default().fg(Color::Green)),
_ => Span::raw(" "),
};
let selection_indicator = if is_selected {
Span::styled("▶ ", Style::default().fg(Color::White))
} else {
Span::raw(" ")
};
let mut spans = vec![
Span::raw(" "),
selection_indicator,
priority_indicator,
Span::raw(&task.title),
];
for tag in &task.tags {
spans.push(Span::raw(" "));
spans.push(Span::styled(
format!("[{}]", tag),
Style::default()
.fg(tag_color(tag))
.add_modifier(Modifier::BOLD),
));
}
spans.push(Span::raw(" "));
ListItem::new(Line::from(spans)).style(style)
})
.collect();
let show_percentage = app
.last_column_resize_time
.map(|t| t.elapsed().as_secs() < 2)
.unwrap_or(false);
let title_with_count = if show_percentage {
if let Some(widths) = app.config.column_widths.get(&project.name) {
if column_idx < widths.len() {
format!(" {} ({}) [{}%] ", title, tasks.len(), widths[column_idx])
} else {
format!(" {} ({}) ", title, tasks.len())
}
} else if let Some(Some(max_col)) = app.config.maximized_column.get(&project.name) {
if column_idx == *max_col {
format!(" {} ({}) [MAX] ", title, tasks.len())
} else {
format!(" {} ({}) ", title, tasks.len())
}
} else {
format!(" {} ({}) ", title, tasks.len())
}
} else {
format!(" {} ({}) ", title, tasks.len())
};
let list = List::new(items)
.block(
Block::default()
.title(title_with_count)
.title_alignment(ratatui::layout::Alignment::Center)
.title_style(title_style)
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.border_type(ratatui::widgets::BorderType::Rounded),
)
.highlight_style(
Style::default()
.bg(Color::Rgb(41, 98, 218))
.fg(Color::White)
.add_modifier(Modifier::BOLD),
);
let list_state = app.list_states.entry(app.focused_pane).or_insert_with(|| {
ratatui::widgets::ListState::default()
});
if is_column_focused {
let selected_idx = app
.selected_task_index
.get(&app.focused_pane)
.copied()
.unwrap_or(0);
list_state.select(Some(selected_idx));
} else {
list_state.select(None);
}
f.render_stateful_widget(list, area, list_state);
}