use ratatui::{ buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, widgets::{Block, Borders, Widget}, }; use crate::model::Model; use crate::view::Axis; use crate::ui::app::AppMode; pub struct CategoryPanel<'a> { pub model: &'a Model, pub mode: &'a AppMode, pub cursor: usize, } impl<'a> CategoryPanel<'a> { pub fn new(model: &'a Model, mode: &'a AppMode, cursor: usize) -> Self { Self { model, mode, cursor } } } impl<'a> Widget for CategoryPanel<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let is_item_add = matches!(self.mode, AppMode::ItemAdd { .. }); let is_cat_add = matches!(self.mode, AppMode::CategoryAdd { .. }); let is_active = matches!(self.mode, AppMode::CategoryPanel) || is_item_add || is_cat_add; let (border_color, title) = if is_cat_add { (Color::Yellow, " Categories — New category (Enter:add Esc:done) ") } else if is_item_add { (Color::Green, " Categories — Adding items (Enter:add Esc:done) ") } else if is_active { (Color::Cyan, " Categories n:new a:add-items Space:axis ") } else { (Color::DarkGray, " Categories ") }; let block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(border_color)) .title(title); let inner = block.inner(area); block.render(area, buf); let view = match self.model.active_view() { Some(v) => v, None => return, }; let cat_names: Vec<&str> = self.model.category_names(); if cat_names.is_empty() { buf.set_string(inner.x, inner.y, "(no categories — use :add-cat )", Style::default().fg(Color::DarkGray)); return; } // How many rows for the list vs the prompt at bottom let prompt_rows = if is_item_add { 2u16 } else { 0 }; let list_height = inner.height.saturating_sub(prompt_rows); for (i, cat_name) in cat_names.iter().enumerate() { if i as u16 >= list_height { break; } let y = inner.y + i as u16; let axis = view.axis_of(cat_name); let axis_str = match axis { Axis::Row => "Row ↕", Axis::Column => "Col ↔", Axis::Page => "Page ☰", Axis::Unassigned => "none", }; let axis_color = match axis { Axis::Row => Color::Green, Axis::Column => Color::Blue, Axis::Page => Color::Magenta, Axis::Unassigned => Color::DarkGray, }; let item_count = self.model.category(cat_name).map(|c| c.items.len()).unwrap_or(0); // Highlight the selected category both in CategoryPanel and ItemAdd modes let is_selected_cat = if is_item_add { if let AppMode::ItemAdd { category, .. } = self.mode { *cat_name == category.as_str() } else { false } } else { i == self.cursor && is_active }; let base_style = if is_selected_cat { Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD) } else { Style::default() }; if is_selected_cat { let fill = " ".repeat(inner.width as usize); buf.set_string(inner.x, y, &fill, base_style); } let name_part = format!(" {cat_name} ({item_count})"); let axis_part = format!(" [{axis_str}]"); buf.set_string(inner.x, y, &name_part, base_style); if name_part.len() + axis_part.len() < inner.width as usize { buf.set_string(inner.x + name_part.len() as u16, y, &axis_part, if is_selected_cat { base_style } else { Style::default().fg(axis_color) }); } } // Inline prompt at the bottom for CategoryAdd or ItemAdd let (prompt_color, prompt_text) = match self.mode { AppMode::CategoryAdd { buffer } => { (Color::Yellow, format!(" + category: {buffer}▌")) } AppMode::ItemAdd { buffer, .. } => { (Color::Green, format!(" + item: {buffer}▌")) } _ => return, }; let sep_y = inner.y + list_height; let prompt_y = sep_y + 1; if sep_y < inner.y + inner.height { let sep = "─".repeat(inner.width as usize); buf.set_string(inner.x, sep_y, &sep, Style::default().fg(prompt_color)); } if prompt_y < inner.y + inner.height { buf.set_string(inner.x, prompt_y, &prompt_text, Style::default().fg(prompt_color).add_modifier(Modifier::BOLD)); } } }