N (from anywhere) or n (in Category panel) opens an inline prompt to add categories one after another without typing :add-cat each time. - Yellow border + prompt distinguishes it from item-add (green) - Enter / Tab adds the category and clears the buffer, staying open - Esc returns to the category list - Cursor automatically moves to the newly added category Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
137 lines
4.8 KiB
Rust
137 lines
4.8 KiB
Rust
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 <name>)",
|
|
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));
|
|
}
|
|
}
|
|
}
|