Model is now pure data (categories, cells, formulas, measure_agg) with no references to view/. The Workbook struct owns the Model together with views and the active view name, and is responsible for cross-slice operations (add/remove category → notify views, view management). - New: src/workbook.rs with Workbook wrapper and cross-slice helpers (add_category, add_label_category, remove_category, create_view, switch_view, delete_view, normalize_view_state). - Model: strip view state and view-touching methods. recompute_formulas remains on Model as a primitive; the view-derived none_cats list is gathered at each call site (App::rebuild_layout, persistence::load) so the view dependency is explicit, not hidden behind a wrapper. - View: add View::none_cats() helper. - CmdContext: add workbook and view fields so commands can reach both slices without threading Model + View through every call. - App: rename `model` field to `workbook`. - Persistence (save/load/format_md/parse_md/export_csv): take/return Workbook so the on-disk format carries model + views together. - Widgets (GridWidget, TileBar, CategoryContent, ViewContent): take explicit &Model + &View instead of routing through Model. Tests updated throughout to reflect the new shape. View-management tests that previously lived on Model continue to cover the same behaviour via a build_workbook() helper in model/types.rs. All 573 tests pass; clippy is clean. This is Phase A of improvise-36h. Phase B will mechanically extract crates/improvise-core/ containing model/, view/, format.rs, workbook.rs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
112 lines
3.1 KiB
Rust
112 lines
3.1 KiB
Rust
use ratatui::{
|
|
buffer::Buffer,
|
|
layout::Rect,
|
|
style::{Color, Modifier, Style},
|
|
};
|
|
|
|
use crate::model::Model;
|
|
use crate::ui::app::AppMode;
|
|
use crate::ui::cat_tree::{CatTreeEntry, build_cat_tree};
|
|
use crate::ui::panel::PanelContent;
|
|
use crate::view::{Axis, View};
|
|
|
|
fn axis_display(axis: Axis) -> (&'static str, Color) {
|
|
match axis {
|
|
Axis::Row => ("Row ↕", Color::Green),
|
|
Axis::Column => ("Col ↔", Color::Blue),
|
|
Axis::Page => ("Page ☰", Color::Magenta),
|
|
Axis::None => ("None ∅", Color::DarkGray),
|
|
}
|
|
}
|
|
|
|
pub struct CategoryContent<'a> {
|
|
view: &'a View,
|
|
tree: Vec<CatTreeEntry>,
|
|
}
|
|
|
|
impl<'a> CategoryContent<'a> {
|
|
pub fn new(
|
|
model: &'a Model,
|
|
view: &'a View,
|
|
expanded: &'a std::collections::HashSet<String>,
|
|
) -> Self {
|
|
let tree = build_cat_tree(model, expanded);
|
|
Self { view, tree }
|
|
}
|
|
}
|
|
|
|
impl PanelContent for CategoryContent<'_> {
|
|
fn is_active(&self, mode: &AppMode) -> bool {
|
|
matches!(
|
|
mode,
|
|
AppMode::CategoryPanel | AppMode::ItemAdd { .. } | AppMode::CategoryAdd { .. }
|
|
)
|
|
}
|
|
|
|
fn active_color(&self) -> Color {
|
|
Color::Cyan
|
|
}
|
|
|
|
fn title(&self) -> &str {
|
|
" Categories "
|
|
}
|
|
|
|
fn item_count(&self) -> usize {
|
|
self.tree.len()
|
|
}
|
|
|
|
fn empty_message(&self) -> &str {
|
|
"(no categories — use :add-cat <name>)"
|
|
}
|
|
|
|
fn render_item(&self, index: usize, is_selected: bool, inner: Rect, buf: &mut Buffer) {
|
|
let y = inner.y + index as u16;
|
|
let view = self.view;
|
|
|
|
let base_style = if is_selected {
|
|
Style::default()
|
|
.fg(Color::Black)
|
|
.bg(Color::Cyan)
|
|
.add_modifier(Modifier::BOLD)
|
|
} else {
|
|
Style::default()
|
|
};
|
|
|
|
if is_selected {
|
|
let fill = " ".repeat(inner.width as usize);
|
|
buf.set_string(inner.x, y, &fill, base_style);
|
|
}
|
|
|
|
match &self.tree[index] {
|
|
CatTreeEntry::Category {
|
|
name,
|
|
item_count,
|
|
expanded,
|
|
} => {
|
|
let indicator = if *expanded { "▼" } else { "▶" };
|
|
let (axis_str, axis_color) = axis_display(view.axis_of(name));
|
|
let name_part = format!("{indicator} {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 {
|
|
base_style
|
|
} else {
|
|
Style::default().fg(axis_color)
|
|
},
|
|
);
|
|
}
|
|
}
|
|
CatTreeEntry::Item { item_name, .. } => {
|
|
let label = format!(" · {item_name}");
|
|
buf.set_string(inner.x, y, &label, base_style);
|
|
}
|
|
}
|
|
}
|
|
}
|