refactor: break Model↔View cycle, introduce Workbook wrapper

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>
This commit is contained in:
Edward Langley
2026-04-15 21:08:11 -07:00
parent f02d905aac
commit 3fbf56ec8b
26 changed files with 1271 additions and 972 deletions

View File

@ -8,7 +8,7 @@ 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;
use crate::view::{Axis, View};
fn axis_display(axis: Axis) -> (&'static str, Color) {
match axis {
@ -20,14 +20,18 @@ fn axis_display(axis: Axis) -> (&'static str, Color) {
}
pub struct CategoryContent<'a> {
model: &'a Model,
view: &'a View,
tree: Vec<CatTreeEntry>,
}
impl<'a> CategoryContent<'a> {
pub fn new(model: &'a Model, expanded: &'a std::collections::HashSet<String>) -> Self {
pub fn new(
model: &'a Model,
view: &'a View,
expanded: &'a std::collections::HashSet<String>,
) -> Self {
let tree = build_cat_tree(model, expanded);
Self { model, tree }
Self { view, tree }
}
}
@ -57,7 +61,7 @@ impl PanelContent for CategoryContent<'_> {
fn render_item(&self, index: usize, is_selected: bool, inner: Rect, buf: &mut Buffer) {
let y = inner.y + index as u16;
let view = self.model.active_view();
let view = self.view;
let base_style = if is_selected {
Style::default()