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:
@ -119,29 +119,29 @@ mod tests {
|
||||
#[test]
|
||||
fn drill_into_formula_cell_returns_data_records() {
|
||||
use crate::formula::parse_formula;
|
||||
use crate::model::Model;
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
use crate::workbook::Workbook;
|
||||
|
||||
let mut m = Model::new("Test");
|
||||
let mut m = Workbook::new("Test");
|
||||
m.add_category("Region").unwrap();
|
||||
m.category_mut("Region").unwrap().add_item("East");
|
||||
m.category_mut("_Measure").unwrap().add_item("Revenue");
|
||||
m.category_mut("_Measure").unwrap().add_item("Cost");
|
||||
m.set_cell(
|
||||
m.model.category_mut("Region").unwrap().add_item("East");
|
||||
m.model.category_mut("_Measure").unwrap().add_item("Revenue");
|
||||
m.model.category_mut("_Measure").unwrap().add_item("Cost");
|
||||
m.model.set_cell(
|
||||
CellKey::new(vec![
|
||||
("_Measure".into(), "Revenue".into()),
|
||||
("Region".into(), "East".into()),
|
||||
]),
|
||||
CellValue::Number(1000.0),
|
||||
);
|
||||
m.set_cell(
|
||||
m.model.set_cell(
|
||||
CellKey::new(vec![
|
||||
("_Measure".into(), "Cost".into()),
|
||||
("Region".into(), "East".into()),
|
||||
]),
|
||||
CellValue::Number(600.0),
|
||||
);
|
||||
m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
|
||||
m.model.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
|
||||
|
||||
let layout = make_layout(&m);
|
||||
let reg = make_registry();
|
||||
@ -376,7 +376,7 @@ impl Cmd for TogglePruneEmpty {
|
||||
"toggle-prune-empty"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let currently_on = ctx.model.active_view().prune_empty;
|
||||
let currently_on = ctx.view.prune_empty;
|
||||
vec![
|
||||
Box::new(effect::TogglePruneEmpty),
|
||||
effect::set_status(if currently_on {
|
||||
@ -454,7 +454,7 @@ impl Cmd for AddRecordRow {
|
||||
)];
|
||||
}
|
||||
// Build a CellKey from the current page filters
|
||||
let view = ctx.model.active_view();
|
||||
let view = ctx.view;
|
||||
let page_cats: Vec<String> = view
|
||||
.categories_on(crate::view::Axis::Page)
|
||||
.into_iter()
|
||||
|
||||
Reference in New Issue
Block a user