#[allow(clippy::wildcard_imports)]
use super::*;
pub fn compute_column_rects(area: Rect, total_cols: usize) -> Vec<Rect> {
if total_cols == 0 {
return Vec::new();
}
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Ratio(1, total_cols as u32); total_cols])
.split(area)
.to_vec()
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_arguments)]
pub fn render(
board: &BoardState,
f: &mut Frame,
area: Rect,
status_msg: &Option<String>,
drag_preview_col: Option<usize>,
display: BoardDisplay,
theme: &crate::ui::color::Theme,
wip: Option<&crate::wip::WipSnapshot>,
capacity: Option<&CapacitySummaries>,
marked: Option<&[u32]>,
) -> usize {
let mut page_cards = 0usize;
if area.height < 2 || area.width < 4 {
return 0;
}
let status_height: u16 = if status_msg.is_some() { 1 } else { 0 };
let board_height = area.height.saturating_sub(status_height);
let board_area = Rect {
height: board_height,
..area
};
let status_area = Rect {
y: area.y + board_height,
height: status_height,
..area
};
let total = board.total_cols();
if total == 0 {
return 0;
}
let chunks = compute_column_rects(board_area, total);
for (i, chunk) in chunks.iter().enumerate() {
let col = if i < board.columns.len() {
&board.columns[i]
} else {
&board.mismatch
};
let group_mode = board
.filter
.as_deref()
.and_then(|q| crate::search::control_token(q, "group"));
let is_active = i == board.active_col;
let is_mismatch_col = i == board.columns.len();
let is_mismatch_highlight = is_mismatch_col && !col.cards.is_empty();
let hovered_col = board.hover_col;
let mut selected_row: Option<usize> = None;
let items: Vec<ListItem> = if let Some(mode) = group_mode.as_deref() {
if let Some(sections) = BoardState::group_sections(&col.cards, Some(mode)) {
let mut items = Vec::new();
for section in sections {
let pct = (section.done * 100).checked_div(section.total).unwrap_or(0);
let capacity_cell = (mode == "cycle")
.then(|| capacity.and_then(|cells| cells.get(§ion.name)))
.flatten();
let header = match capacity_cell {
Some(cell) => format!(
"-- {mode}: {} ({}/{} done, {pct}%) · {cell} --",
section.name, section.done, section.total
),
None => format!(
"-- {mode}: {} ({}/{} done, {pct}%) --",
section.name, section.done, section.total
),
};
items.push(ListItem::new(Line::from(Span::styled(
header,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
))));
for j in section.start..section.end {
let card = &col.cards[j];
let lines = widgets::card_lines_themed(
card,
chunk.width as usize,
display.wrap_titles,
display.title_lines,
theme,
);
let is_marked =
marked.is_some_and(|numbers| numbers.contains(&card.number));
let marked_style = Style::default()
.fg(Color::Cyan)
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD);
let style = if is_active && j == board.selected {
selected_row = Some(items.len());
if is_marked {
marked_style.add_modifier(Modifier::UNDERLINED)
} else {
Style::default()
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD)
}
} else if is_marked {
marked_style
} else if board.hover == Some((i, j)) {
if drag_preview_col == Some(i) {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Cyan)
}
} else if is_mismatch_highlight {
Style::default().fg(Color::Red)
} else {
Style::default()
};
items.push(ListItem::new(lines).style(style));
}
}
items
} else {
Vec::new()
}
} else {
col.cards
.iter()
.enumerate()
.map(|(j, card)| {
let lines = widgets::card_lines_themed(
card,
chunk.width as usize,
display.wrap_titles,
display.title_lines,
theme,
);
let is_marked = marked.is_some_and(|numbers| numbers.contains(&card.number));
let marked_style = Style::default()
.fg(Color::Cyan)
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD);
let style = if is_active && j == board.selected {
selected_row = Some(j);
if is_marked {
marked_style.add_modifier(Modifier::UNDERLINED)
} else {
Style::default()
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD)
}
} else if is_marked {
marked_style
} else if board.hover == Some((i, j)) {
if drag_preview_col == Some(i) {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Cyan)
}
} else if is_mismatch_highlight {
Style::default().fg(Color::Red)
} else {
Style::default()
};
ListItem::new(lines).style(style)
})
.collect()
};
let title = match wip
.and_then(|snapshot| snapshot.column(&col.name))
.filter(|usage| usage.limit.is_some())
{
Some(usage) => format!("{} ({})", col.name, usage.cell()),
None => format!("{} ({})", col.name, col.cards.len()),
};
let border_style = if drag_preview_col == Some(i) {
Style::default().fg(Color::Yellow)
} else if is_active {
Style::default().fg(Color::Cyan)
} else if is_mismatch_highlight {
Style::default().fg(Color::Red)
} else if hovered_col == Some(i) {
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
let block = Block::default()
.borders(Borders::ALL)
.title(title)
.border_style(border_style);
let col_chunks = if BoardState::group_footer(&col.cards, group_mode.as_deref()).is_some() {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(1)])
.split(*chunk)
} else {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(0)])
.split(*chunk)
};
let list = List::new(items).block(block);
if is_active {
page_cards = cards_per_screen(
&col.cards,
chunk.width as usize,
col_chunks[0].height.saturating_sub(2),
display,
);
let mut state = ListState::default();
if let Some(row) = selected_row {
state.select(Some(row));
}
f.render_stateful_widget(list, col_chunks[0], &mut state);
} else {
f.render_widget(list, col_chunks[0]);
}
if let Some(footer) = BoardState::group_footer(&col.cards, group_mode.as_deref()) {
let para = Paragraph::new(Line::from(Span::styled(
footer,
Style::default().fg(Color::DarkGray),
)));
f.render_widget(para, col_chunks[1]);
}
}
if let Some(msg) = status_msg {
let budget = (status_area.width as usize).saturating_sub(1);
let text = truncate_to_cells(msg, budget);
let para =
Paragraph::new(Line::from(Span::raw(text))).style(Style::default().fg(Color::Yellow));
f.render_widget(para, status_area);
}
page_cards
}
pub(super) fn cards_per_screen(
cards: &[Card],
width: usize,
rows: u16,
display: BoardDisplay,
) -> usize {
if cards.is_empty() {
return 0;
}
let mut used: u16 = 0;
let mut fits = 0usize;
for card in cards {
let height = u16::try_from(
widgets::card_lines(card, width, display.wrap_titles, display.title_lines).len(),
)
.unwrap_or(u16::MAX);
if fits > 0 && used.saturating_add(height) > rows {
break;
}
used = used.saturating_add(height);
fits += 1;
if used >= rows {
break;
}
}
fits.max(1)
}