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:
+23
-10
@@ -16,7 +16,6 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Clear, Paragraph},
|
||||
};
|
||||
|
||||
use crate::model::Model;
|
||||
use crate::ui::app::{App, AppMode};
|
||||
use crate::ui::category_panel::CategoryContent;
|
||||
use crate::ui::formula_panel::FormulaContent;
|
||||
@@ -51,13 +50,13 @@ impl<'a> Drop for TuiContext<'a> {
|
||||
}
|
||||
|
||||
pub fn run_tui(
|
||||
model: Model,
|
||||
workbook: crate::workbook::Workbook,
|
||||
file_path: Option<PathBuf>,
|
||||
import_value: Option<serde_json::Value>,
|
||||
) -> Result<()> {
|
||||
let mut stdout = io::stdout();
|
||||
let mut tui_context = TuiContext::enter(&mut stdout)?;
|
||||
let mut app = App::new(model, file_path);
|
||||
let mut app = App::new(workbook, file_path);
|
||||
|
||||
if let Some(json) = import_value {
|
||||
app.start_import_wizard(json);
|
||||
@@ -193,7 +192,10 @@ fn draw_title(f: &mut Frame, area: Rect, app: &App) {
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|n| format!(" ({n})"))
|
||||
.unwrap_or_default();
|
||||
let title = format!(" improvise · {}{}{} ", app.model.name, file, dirty);
|
||||
let title = format!(
|
||||
" improvise · {}{}{} ",
|
||||
app.workbook.model.name, file, dirty
|
||||
);
|
||||
let right = " ?:help :q quit ";
|
||||
let line = fill_line(title, right, area.width);
|
||||
f.render_widget(
|
||||
@@ -234,19 +236,20 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
if app.formula_panel_open {
|
||||
let a = Rect::new(side.x, y, side.width, ph);
|
||||
let content = FormulaContent::new(&app.model, &app.mode);
|
||||
let content = FormulaContent::new(&app.workbook.model, &app.mode);
|
||||
f.render_widget(Panel::new(content, &app.mode, app.formula_cursor), a);
|
||||
y += ph;
|
||||
}
|
||||
if app.category_panel_open {
|
||||
let a = Rect::new(side.x, y, side.width, ph);
|
||||
let content = CategoryContent::new(&app.model, &app.expanded_cats);
|
||||
let content =
|
||||
CategoryContent::new(&app.workbook.model, app.workbook.active_view(), &app.expanded_cats);
|
||||
f.render_widget(Panel::new(content, &app.mode, app.cat_panel_cursor), a);
|
||||
y += ph;
|
||||
}
|
||||
if app.view_panel_open {
|
||||
let a = Rect::new(side.x, y, side.width, ph);
|
||||
let content = ViewContent::new(&app.model);
|
||||
let content = ViewContent::new(&app.workbook);
|
||||
f.render_widget(Panel::new(content, &app.mode, app.view_panel_cursor), a);
|
||||
}
|
||||
} else {
|
||||
@@ -255,7 +258,9 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
f.render_widget(
|
||||
GridWidget::new(
|
||||
&app.model,
|
||||
&app.workbook.model,
|
||||
app.workbook.active_view(),
|
||||
&app.workbook.active_view,
|
||||
&app.layout,
|
||||
&app.mode,
|
||||
&app.search_query,
|
||||
@@ -267,7 +272,15 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
||||
}
|
||||
|
||||
fn draw_tile_bar(f: &mut Frame, area: Rect, app: &App) {
|
||||
f.render_widget(TileBar::new(&app.model, &app.mode, app.tile_cat_idx), area);
|
||||
f.render_widget(
|
||||
TileBar::new(
|
||||
&app.workbook.model,
|
||||
app.workbook.active_view(),
|
||||
&app.mode,
|
||||
app.tile_cat_idx,
|
||||
),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
|
||||
@@ -306,7 +319,7 @@ fn draw_status(f: &mut Frame, area: Rect, app: &App) {
|
||||
};
|
||||
|
||||
let yank_indicator = if app.yanked.is_some() { " [yank]" } else { "" };
|
||||
let view_badge = format!(" {}{} ", app.model.active_view, yank_indicator);
|
||||
let view_badge = format!(" {}{} ", app.workbook.active_view, yank_indicator);
|
||||
|
||||
let left = format!(" {}{search_part} {msg}", mode_name(&app.mode));
|
||||
let line = fill_line(left, &view_badge, area.width);
|
||||
|
||||
Reference in New Issue
Block a user