refactor(ui): move workbook/file_path/dirty into ModelState (improvise-x2c)
Step 2 of vb4. Populates ModelState with the document slice and routes every read/write site (effects, draw, main, app methods, tests) through app.model_state.X. App no longer owns workbook, file_path, or dirty directly. Effect::apply signatures still take &mut App; narrowing happens in step 5 (improvise-drg). A structural test (app_model_state_owns_workbook_file_path_and_dirty) locks in the field layout. ModelState now has a manual Default impl that creates an "Untitled" Workbook so the existing constructibility test keeps working. 623 tests pass workspace-wide (+1 new). cargo clippy --workspace --tests clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-12
@@ -188,8 +188,9 @@ fn draw(f: &mut Frame, app: &App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_title(f: &mut Frame, area: Rect, app: &App) {
|
fn draw_title(f: &mut Frame, area: Rect, app: &App) {
|
||||||
let dirty = if app.dirty { " [+]" } else { "" };
|
let dirty = if app.model_state.dirty { " [+]" } else { "" };
|
||||||
let file = app
|
let file = app
|
||||||
|
.model_state
|
||||||
.file_path
|
.file_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|p| p.file_name())
|
.and_then(|p| p.file_name())
|
||||||
@@ -198,7 +199,7 @@ fn draw_title(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let title = format!(
|
let title = format!(
|
||||||
" improvise · {}{}{} ",
|
" improvise · {}{}{} ",
|
||||||
app.workbook.model.name, file, dirty
|
app.model_state.workbook.model.name, file, dirty
|
||||||
);
|
);
|
||||||
let right = " ?:help :q quit ";
|
let right = " ?:help :q quit ";
|
||||||
let line = fill_line(title, right, area.width);
|
let line = fill_line(title, right, area.width);
|
||||||
@@ -240,15 +241,15 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
|
|
||||||
if app.formula_panel_open {
|
if app.formula_panel_open {
|
||||||
let a = Rect::new(side.x, y, side.width, ph);
|
let a = Rect::new(side.x, y, side.width, ph);
|
||||||
let content = FormulaContent::new(&app.workbook.model, &app.mode);
|
let content = FormulaContent::new(&app.model_state.workbook.model, &app.mode);
|
||||||
f.render_widget(Panel::new(content, &app.mode, app.formula_cursor), a);
|
f.render_widget(Panel::new(content, &app.mode, app.formula_cursor), a);
|
||||||
y += ph;
|
y += ph;
|
||||||
}
|
}
|
||||||
if app.category_panel_open {
|
if app.category_panel_open {
|
||||||
let a = Rect::new(side.x, y, side.width, ph);
|
let a = Rect::new(side.x, y, side.width, ph);
|
||||||
let content = CategoryContent::new(
|
let content = CategoryContent::new(
|
||||||
&app.workbook.model,
|
&app.model_state.workbook.model,
|
||||||
app.workbook.active_view(),
|
app.model_state.workbook.active_view(),
|
||||||
&app.expanded_cats,
|
&app.expanded_cats,
|
||||||
);
|
);
|
||||||
f.render_widget(Panel::new(content, &app.mode, app.cat_panel_cursor), a);
|
f.render_widget(Panel::new(content, &app.mode, app.cat_panel_cursor), a);
|
||||||
@@ -256,7 +257,7 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
}
|
}
|
||||||
if app.view_panel_open {
|
if app.view_panel_open {
|
||||||
let a = Rect::new(side.x, y, side.width, ph);
|
let a = Rect::new(side.x, y, side.width, ph);
|
||||||
let content = ViewContent::new(&app.workbook);
|
let content = ViewContent::new(&app.model_state.workbook);
|
||||||
f.render_widget(Panel::new(content, &app.mode, app.view_panel_cursor), a);
|
f.render_widget(Panel::new(content, &app.mode, app.view_panel_cursor), a);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -265,9 +266,9 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
|
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
GridWidget::new(
|
GridWidget::new(
|
||||||
&app.workbook.model,
|
&app.model_state.workbook.model,
|
||||||
app.workbook.active_view(),
|
app.model_state.workbook.active_view(),
|
||||||
&app.workbook.active_view,
|
&app.model_state.workbook.active_view,
|
||||||
&app.layout,
|
&app.layout,
|
||||||
&app.mode,
|
&app.mode,
|
||||||
&app.search_query,
|
&app.search_query,
|
||||||
@@ -281,8 +282,8 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
fn draw_tile_bar(f: &mut Frame, area: Rect, app: &App) {
|
fn draw_tile_bar(f: &mut Frame, area: Rect, app: &App) {
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
TileBar::new(
|
TileBar::new(
|
||||||
&app.workbook.model,
|
&app.model_state.workbook.model,
|
||||||
app.workbook.active_view(),
|
app.model_state.workbook.active_view(),
|
||||||
&app.mode,
|
&app.mode,
|
||||||
app.tile_cat_idx,
|
app.tile_cat_idx,
|
||||||
),
|
),
|
||||||
@@ -326,7 +327,7 @@ fn draw_status(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let yank_indicator = if app.yanked.is_some() { " [yank]" } else { "" };
|
let yank_indicator = if app.yanked.is_some() { " [yank]" } else { "" };
|
||||||
let view_badge = format!(" {}{} ", app.workbook.active_view, yank_indicator);
|
let view_badge = format!(" {}{} ", app.model_state.workbook.active_view, yank_indicator);
|
||||||
|
|
||||||
let left = format!(" {}{search_part} {msg}", mode_name(&app.mode));
|
let left = format!(" {}{search_part} {msg}", mode_name(&app.mode));
|
||||||
let line = fill_line(left, &view_badge, area.width);
|
let line = fill_line(left, &view_badge, area.width);
|
||||||
|
|||||||
+1
-1
@@ -353,7 +353,7 @@ fn run_headless_commands(cmds: &[String], file: &Option<PathBuf>) -> Result<()>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(path) = file {
|
if let Some(path) = file {
|
||||||
persistence::save(&app.workbook, path)?;
|
persistence::save(&app.model_state.workbook, path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::process::exit(exit_code);
|
std::process::exit(exit_code);
|
||||||
|
|||||||
+92
-69
@@ -187,10 +187,23 @@ impl AppMode {
|
|||||||
|
|
||||||
/// Document state slice: the workbook and its IO bookkeeping. Distinct from
|
/// Document state slice: the workbook and its IO bookkeeping. Distinct from
|
||||||
/// `Workbook` itself (which is pure document semantics in `improvise-core`)
|
/// `Workbook` itself (which is pure document semantics in `improvise-core`)
|
||||||
/// because `file_path` and `dirty` are persistence-layer concerns. Filled in
|
/// because `file_path` and `dirty` are persistence-layer concerns.
|
||||||
/// by improvise-x2c (vb4 step 2).
|
#[derive(Debug)]
|
||||||
#[derive(Debug, Default)]
|
pub struct ModelState {
|
||||||
pub struct ModelState {}
|
pub workbook: Workbook,
|
||||||
|
pub file_path: Option<PathBuf>,
|
||||||
|
pub dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ModelState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
workbook: Workbook::new("Untitled"),
|
||||||
|
file_path: None,
|
||||||
|
dirty: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// UI session-state slice: mode, cursors, panels, buffers, navigation stacks,
|
/// UI session-state slice: mode, cursors, panels, buffers, navigation stacks,
|
||||||
/// and other per-session state that does not persist to disk. Filled in by
|
/// and other per-session state that does not persist to disk. Filled in by
|
||||||
@@ -199,8 +212,7 @@ pub struct ModelState {}
|
|||||||
pub struct ViewState {}
|
pub struct ViewState {}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub workbook: Workbook,
|
pub model_state: ModelState,
|
||||||
pub file_path: Option<PathBuf>,
|
|
||||||
pub mode: AppMode,
|
pub mode: AppMode,
|
||||||
pub status_msg: String,
|
pub status_msg: String,
|
||||||
pub wizard: Option<ImportWizard>,
|
pub wizard: Option<ImportWizard>,
|
||||||
@@ -213,7 +225,6 @@ pub struct App {
|
|||||||
pub cat_panel_cursor: usize,
|
pub cat_panel_cursor: usize,
|
||||||
pub view_panel_cursor: usize,
|
pub view_panel_cursor: usize,
|
||||||
pub formula_cursor: usize,
|
pub formula_cursor: usize,
|
||||||
pub dirty: bool,
|
|
||||||
/// Yanked cell value for `p` paste
|
/// Yanked cell value for `p` paste
|
||||||
pub yanked: Option<CellValue>,
|
pub yanked: Option<CellValue>,
|
||||||
/// Tile select cursor (which category index is highlighted)
|
/// Tile select cursor (which category index is highlighted)
|
||||||
@@ -264,8 +275,11 @@ impl App {
|
|||||||
GridLayout::with_frozen_records(&workbook.model, view, None)
|
GridLayout::with_frozen_records(&workbook.model, view, None)
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
workbook,
|
model_state: ModelState {
|
||||||
file_path,
|
workbook,
|
||||||
|
file_path,
|
||||||
|
dirty: false,
|
||||||
|
},
|
||||||
mode: AppMode::Normal,
|
mode: AppMode::Normal,
|
||||||
status_msg: String::new(),
|
status_msg: String::new(),
|
||||||
wizard: None,
|
wizard: None,
|
||||||
@@ -278,7 +292,6 @@ impl App {
|
|||||||
cat_panel_cursor: 0,
|
cat_panel_cursor: 0,
|
||||||
view_panel_cursor: 0,
|
view_panel_cursor: 0,
|
||||||
formula_cursor: 0,
|
formula_cursor: 0,
|
||||||
dirty: false,
|
|
||||||
yanked: None,
|
yanked: None,
|
||||||
tile_cat_idx: 0,
|
tile_cat_idx: 0,
|
||||||
view_back_stack: Vec::new(),
|
view_back_stack: Vec::new(),
|
||||||
@@ -299,20 +312,20 @@ impl App {
|
|||||||
/// Rebuild the grid layout from current workbook, active view, and drill
|
/// Rebuild the grid layout from current workbook, active view, and drill
|
||||||
/// state. Note: `with_frozen_records` already handles pruning internally.
|
/// state. Note: `with_frozen_records` already handles pruning internally.
|
||||||
pub fn rebuild_layout(&mut self) {
|
pub fn rebuild_layout(&mut self) {
|
||||||
let none_cats = self.workbook.active_view().none_cats();
|
let none_cats = self.model_state.workbook.active_view().none_cats();
|
||||||
self.workbook.model.recompute_formulas(&none_cats);
|
self.model_state.workbook.model.recompute_formulas(&none_cats);
|
||||||
let view = self.workbook.active_view();
|
let view = self.model_state.workbook.active_view();
|
||||||
let frozen = self.drill_state.as_ref().map(|s| Rc::clone(&s.records));
|
let frozen = self.drill_state.as_ref().map(|s| Rc::clone(&s.records));
|
||||||
self.layout = GridLayout::with_frozen_records(&self.workbook.model, view, frozen);
|
self.layout = GridLayout::with_frozen_records(&self.model_state.workbook.model, view, frozen);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cmd_context(&self, key: KeyCode, _mods: KeyModifiers) -> CmdContext<'_> {
|
pub fn cmd_context(&self, key: KeyCode, _mods: KeyModifiers) -> CmdContext<'_> {
|
||||||
let view = self.workbook.active_view();
|
let view = self.model_state.workbook.active_view();
|
||||||
let layout = &self.layout;
|
let layout = &self.layout;
|
||||||
let (sel_row, sel_col) = view.selected;
|
let (sel_row, sel_col) = view.selected;
|
||||||
CmdContext {
|
CmdContext {
|
||||||
model: &self.workbook.model,
|
model: &self.model_state.workbook.model,
|
||||||
workbook: &self.workbook,
|
workbook: &self.model_state.workbook,
|
||||||
view,
|
view,
|
||||||
layout,
|
layout,
|
||||||
registry: self.keymap_set.registry(),
|
registry: self.keymap_set.registry(),
|
||||||
@@ -322,7 +335,7 @@ impl App {
|
|||||||
col_offset: view.col_offset,
|
col_offset: view.col_offset,
|
||||||
search_query: &self.search_query,
|
search_query: &self.search_query,
|
||||||
yanked: &self.yanked,
|
yanked: &self.yanked,
|
||||||
dirty: self.dirty,
|
dirty: self.model_state.dirty,
|
||||||
search_mode: self.search_mode,
|
search_mode: self.search_mode,
|
||||||
formula_panel_open: self.formula_panel_open,
|
formula_panel_open: self.formula_panel_open,
|
||||||
category_panel_open: self.category_panel_open,
|
category_panel_open: self.category_panel_open,
|
||||||
@@ -345,7 +358,7 @@ impl App {
|
|||||||
.or_else(|| layout.resolve_display(k))
|
.or_else(|| layout.resolve_display(k))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
self.workbook
|
self.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.get_cell(k)
|
.get_cell(k)
|
||||||
.map(|v| v.to_string())
|
.map(|v| v.to_string())
|
||||||
@@ -359,7 +372,7 @@ impl App {
|
|||||||
visible_cols: {
|
visible_cols: {
|
||||||
let (fmt_comma, fmt_decimals) = parse_number_format(&view.number_format);
|
let (fmt_comma, fmt_decimals) = parse_number_format(&view.number_format);
|
||||||
let col_widths =
|
let col_widths =
|
||||||
compute_col_widths(&self.workbook.model, layout, fmt_comma, fmt_decimals);
|
compute_col_widths(&self.model_state.workbook.model, layout, fmt_comma, fmt_decimals);
|
||||||
let row_header_width = compute_row_header_width(layout);
|
let row_header_width = compute_row_header_width(layout);
|
||||||
compute_visible_cols(
|
compute_visible_cols(
|
||||||
&col_widths,
|
&col_widths,
|
||||||
@@ -392,7 +405,7 @@ impl App {
|
|||||||
/// Virtual categories (_Index, _Dim, _Measure) are always present and don't count.
|
/// Virtual categories (_Index, _Dim, _Measure) are always present and don't count.
|
||||||
pub fn is_empty_model(&self) -> bool {
|
pub fn is_empty_model(&self) -> bool {
|
||||||
use crate::model::category::CategoryKind;
|
use crate::model::category::CategoryKind;
|
||||||
self.workbook.model.categories.values().all(|c| {
|
self.model_state.workbook.model.categories.values().all(|c| {
|
||||||
matches!(
|
matches!(
|
||||||
c.kind,
|
c.kind,
|
||||||
CategoryKind::VirtualIndex
|
CategoryKind::VirtualIndex
|
||||||
@@ -431,12 +444,12 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn autosave_if_needed(&mut self) {
|
pub fn autosave_if_needed(&mut self) {
|
||||||
if self.dirty
|
if self.model_state.dirty
|
||||||
&& self.last_autosave.elapsed() > Duration::from_secs(30)
|
&& self.last_autosave.elapsed() > Duration::from_secs(30)
|
||||||
&& let Some(path) = &self.file_path.clone()
|
&& let Some(path) = &self.model_state.file_path.clone()
|
||||||
{
|
{
|
||||||
let ap = persistence::autosave_path(path);
|
let ap = persistence::autosave_path(path);
|
||||||
let _ = persistence::save(&self.workbook, &ap);
|
let _ = persistence::save(&self.model_state.workbook, &ap);
|
||||||
self.last_autosave = Instant::now();
|
self.last_autosave = Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -494,6 +507,16 @@ mod tests {
|
|||||||
let _: ViewState = ViewState::default();
|
let _: ViewState = ViewState::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// improvise-x2c: ModelState owns the document slice — workbook,
|
||||||
|
/// file_path, and dirty. App accesses them through model_state.
|
||||||
|
#[test]
|
||||||
|
fn app_model_state_owns_workbook_file_path_and_dirty() {
|
||||||
|
let app = App::new(Workbook::new("T"), Some(PathBuf::from("/tmp/x")));
|
||||||
|
let _: &Workbook = &app.model_state.workbook;
|
||||||
|
let _: &Option<PathBuf> = &app.model_state.file_path;
|
||||||
|
let _: bool = app.model_state.dirty;
|
||||||
|
}
|
||||||
|
|
||||||
fn two_col_model() -> App {
|
fn two_col_model() -> App {
|
||||||
let mut wb = Workbook::new("T");
|
let mut wb = Workbook::new("T");
|
||||||
wb.add_category("Row").unwrap(); // → Row axis
|
wb.add_category("Row").unwrap(); // → Row axis
|
||||||
@@ -515,7 +538,7 @@ mod tests {
|
|||||||
|
|
||||||
fn enter_advance_cmd(app: &App) -> crate::command::cmd::navigation::EnterAdvance {
|
fn enter_advance_cmd(app: &App) -> crate::command::cmd::navigation::EnterAdvance {
|
||||||
use crate::command::cmd::navigation::CursorState;
|
use crate::command::cmd::navigation::CursorState;
|
||||||
let view = app.workbook.active_view();
|
let view = app.model_state.workbook.active_view();
|
||||||
let cursor = CursorState {
|
let cursor = CursorState {
|
||||||
row: view.selected.0,
|
row: view.selected.0,
|
||||||
col: view.selected.1,
|
col: view.selected.1,
|
||||||
@@ -532,29 +555,29 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn enter_advance_moves_down_within_column() {
|
fn enter_advance_moves_down_within_column() {
|
||||||
let mut app = two_col_model();
|
let mut app = two_col_model();
|
||||||
app.workbook.active_view_mut().selected = (0, 0);
|
app.model_state.workbook.active_view_mut().selected = (0, 0);
|
||||||
let cmd = enter_advance_cmd(&app);
|
let cmd = enter_advance_cmd(&app);
|
||||||
run_cmd(&mut app, &cmd);
|
run_cmd(&mut app, &cmd);
|
||||||
assert_eq!(app.workbook.active_view().selected, (1, 0));
|
assert_eq!(app.model_state.workbook.active_view().selected, (1, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn enter_advance_wraps_to_top_of_next_column() {
|
fn enter_advance_wraps_to_top_of_next_column() {
|
||||||
let mut app = two_col_model();
|
let mut app = two_col_model();
|
||||||
// row_max = 2 (A,B,C), col 0 → should wrap to (0, 1)
|
// row_max = 2 (A,B,C), col 0 → should wrap to (0, 1)
|
||||||
app.workbook.active_view_mut().selected = (2, 0);
|
app.model_state.workbook.active_view_mut().selected = (2, 0);
|
||||||
let cmd = enter_advance_cmd(&app);
|
let cmd = enter_advance_cmd(&app);
|
||||||
run_cmd(&mut app, &cmd);
|
run_cmd(&mut app, &cmd);
|
||||||
assert_eq!(app.workbook.active_view().selected, (0, 1));
|
assert_eq!(app.model_state.workbook.active_view().selected, (0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn enter_advance_stays_at_bottom_right() {
|
fn enter_advance_stays_at_bottom_right() {
|
||||||
let mut app = two_col_model();
|
let mut app = two_col_model();
|
||||||
app.workbook.active_view_mut().selected = (2, 1);
|
app.model_state.workbook.active_view_mut().selected = (2, 1);
|
||||||
let cmd = enter_advance_cmd(&app);
|
let cmd = enter_advance_cmd(&app);
|
||||||
run_cmd(&mut app, &cmd);
|
run_cmd(&mut app, &cmd);
|
||||||
assert_eq!(app.workbook.active_view().selected, (2, 1));
|
assert_eq!(app.model_state.workbook.active_view().selected, (2, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -633,34 +656,34 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.workbook.active_view().selected.1,
|
app.model_state.workbook.active_view().selected.1,
|
||||||
3,
|
3,
|
||||||
"cursor should be at column 3"
|
"cursor should be at column 3"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
app.workbook.active_view().col_offset > 0,
|
app.model_state.workbook.active_view().col_offset > 0,
|
||||||
"col_offset should scroll when cursor moves past visible area (only ~2 cols fit \
|
"col_offset should scroll when cursor moves past visible area (only ~2 cols fit \
|
||||||
in 80-char terminal with 26-char-wide columns), but col_offset is {}",
|
in 80-char terminal with 26-char-wide columns), but col_offset is {}",
|
||||||
app.workbook.active_view().col_offset
|
app.model_state.workbook.active_view().col_offset
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn home_jumps_to_first_col() {
|
fn home_jumps_to_first_col() {
|
||||||
let mut app = two_col_model();
|
let mut app = two_col_model();
|
||||||
app.workbook.active_view_mut().selected = (1, 1);
|
app.model_state.workbook.active_view_mut().selected = (1, 1);
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(app.workbook.active_view().selected, (1, 0));
|
assert_eq!(app.model_state.workbook.active_view().selected, (1, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn end_jumps_to_last_col() {
|
fn end_jumps_to_last_col() {
|
||||||
let mut app = two_col_model();
|
let mut app = two_col_model();
|
||||||
app.workbook.active_view_mut().selected = (1, 0);
|
app.model_state.workbook.active_view_mut().selected = (1, 0);
|
||||||
app.handle_key(KeyEvent::new(KeyCode::End, KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::End, KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(app.workbook.active_view().selected, (1, 1));
|
assert_eq!(app.model_state.workbook.active_view().selected, (1, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -668,40 +691,40 @@ mod tests {
|
|||||||
let mut app = two_col_model();
|
let mut app = two_col_model();
|
||||||
// Add enough rows
|
// Add enough rows
|
||||||
for i in 0..30 {
|
for i in 0..30 {
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.category_mut("Row")
|
.category_mut("Row")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.add_item(format!("R{i}"));
|
.add_item(format!("R{i}"));
|
||||||
}
|
}
|
||||||
app.term_height = 28; // ~20 visible rows → delta = 15
|
app.term_height = 28; // ~20 visible rows → delta = 15
|
||||||
app.workbook.active_view_mut().selected = (0, 0);
|
app.model_state.workbook.active_view_mut().selected = (0, 0);
|
||||||
app.handle_key(KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(app.workbook.active_view().selected.1, 0, "column preserved");
|
assert_eq!(app.model_state.workbook.active_view().selected.1, 0, "column preserved");
|
||||||
assert!(
|
assert!(
|
||||||
app.workbook.active_view().selected.0 > 0,
|
app.model_state.workbook.active_view().selected.0 > 0,
|
||||||
"row should advance on PageDown"
|
"row should advance on PageDown"
|
||||||
);
|
);
|
||||||
// 3/4 of ~20 = 15
|
// 3/4 of ~20 = 15
|
||||||
assert_eq!(app.workbook.active_view().selected.0, 15);
|
assert_eq!(app.model_state.workbook.active_view().selected.0, 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn page_up_scrolls_backward() {
|
fn page_up_scrolls_backward() {
|
||||||
let mut app = two_col_model();
|
let mut app = two_col_model();
|
||||||
for i in 0..30 {
|
for i in 0..30 {
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.category_mut("Row")
|
.category_mut("Row")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.add_item(format!("R{i}"));
|
.add_item(format!("R{i}"));
|
||||||
}
|
}
|
||||||
app.term_height = 28;
|
app.term_height = 28;
|
||||||
app.workbook.active_view_mut().selected = (20, 0);
|
app.model_state.workbook.active_view_mut().selected = (20, 0);
|
||||||
app.handle_key(KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(app.workbook.active_view().selected.0, 5);
|
assert_eq!(app.model_state.workbook.active_view().selected.0, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -709,22 +732,22 @@ mod tests {
|
|||||||
let mut app = two_col_model();
|
let mut app = two_col_model();
|
||||||
// Total rows: A, B, C + R0..R9 = 13 rows. Last row = 12.
|
// Total rows: A, B, C + R0..R9 = 13 rows. Last row = 12.
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.category_mut("Row")
|
.category_mut("Row")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.add_item(format!("R{i}"));
|
.add_item(format!("R{i}"));
|
||||||
}
|
}
|
||||||
app.term_height = 13; // ~5 visible rows
|
app.term_height = 13; // ~5 visible rows
|
||||||
app.workbook.active_view_mut().selected = (0, 0);
|
app.model_state.workbook.active_view_mut().selected = (0, 0);
|
||||||
// G jumps to last row (row 12)
|
// G jumps to last row (row 12)
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('G'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('G'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let last = app.workbook.active_view().selected.0;
|
let last = app.model_state.workbook.active_view().selected.0;
|
||||||
assert_eq!(last, 12, "should be at last row");
|
assert_eq!(last, 12, "should be at last row");
|
||||||
// With only ~5 visible rows and 13 rows, offset should scroll.
|
// With only ~5 visible rows and 13 rows, offset should scroll.
|
||||||
// Bug: hardcoded 20 means `12 >= 0 + 20` is false → no scroll.
|
// Bug: hardcoded 20 means `12 >= 0 + 20` is false → no scroll.
|
||||||
let offset = app.workbook.active_view().row_offset;
|
let offset = app.model_state.workbook.active_view().row_offset;
|
||||||
assert!(
|
assert!(
|
||||||
offset > 0,
|
offset > 0,
|
||||||
"row_offset should scroll when last row is beyond visible area, but is {offset}"
|
"row_offset should scroll when last row is beyond visible area, but is {offset}"
|
||||||
@@ -735,34 +758,34 @@ mod tests {
|
|||||||
fn ctrl_d_scrolls_viewport_with_small_terminal() {
|
fn ctrl_d_scrolls_viewport_with_small_terminal() {
|
||||||
let mut app = two_col_model();
|
let mut app = two_col_model();
|
||||||
for i in 0..30 {
|
for i in 0..30 {
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.category_mut("Row")
|
.category_mut("Row")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.add_item(format!("R{i}"));
|
.add_item(format!("R{i}"));
|
||||||
}
|
}
|
||||||
app.term_height = 13; // ~5 visible rows
|
app.term_height = 13; // ~5 visible rows
|
||||||
app.workbook.active_view_mut().selected = (0, 0);
|
app.model_state.workbook.active_view_mut().selected = (0, 0);
|
||||||
// Ctrl+d scrolls by 5 rows
|
// Ctrl+d scrolls by 5 rows
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL))
|
app.handle_key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(app.workbook.active_view().selected.0, 5);
|
assert_eq!(app.model_state.workbook.active_view().selected.0, 5);
|
||||||
// Press Ctrl+d again — now at row 10 with only 5 visible rows,
|
// Press Ctrl+d again — now at row 10 with only 5 visible rows,
|
||||||
// row_offset should have scrolled (not stay at 0 due to hardcoded 20)
|
// row_offset should have scrolled (not stay at 0 due to hardcoded 20)
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL))
|
app.handle_key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(app.workbook.active_view().selected.0, 10);
|
assert_eq!(app.model_state.workbook.active_view().selected.0, 10);
|
||||||
assert!(
|
assert!(
|
||||||
app.workbook.active_view().row_offset > 0,
|
app.model_state.workbook.active_view().row_offset > 0,
|
||||||
"row_offset should scroll with small terminal, but is {}",
|
"row_offset should scroll with small terminal, but is {}",
|
||||||
app.workbook.active_view().row_offset
|
app.model_state.workbook.active_view().row_offset
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tab_in_edit_mode_commits_and_moves_right() {
|
fn tab_in_edit_mode_commits_and_moves_right() {
|
||||||
let mut app = two_col_model();
|
let mut app = two_col_model();
|
||||||
app.workbook.active_view_mut().selected = (0, 0);
|
app.model_state.workbook.active_view_mut().selected = (0, 0);
|
||||||
// Enter edit mode
|
// Enter edit mode
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -780,7 +803,7 @@ mod tests {
|
|||||||
app.mode
|
app.mode
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.workbook.active_view().selected.1,
|
app.model_state.workbook.active_view().selected.1,
|
||||||
1,
|
1,
|
||||||
"should have moved to column 1"
|
"should have moved to column 1"
|
||||||
);
|
);
|
||||||
@@ -885,7 +908,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.workbook.model.get_cell(&CellKey::new(vec![(
|
app.model_state.workbook.model.get_cell(&CellKey::new(vec![(
|
||||||
"_Measure".to_string(),
|
"_Measure".to_string(),
|
||||||
"Rev".to_string(),
|
"Rev".to_string(),
|
||||||
)])),
|
)])),
|
||||||
@@ -957,11 +980,11 @@ mod tests {
|
|||||||
use crate::model::cell::{CellKey, CellValue};
|
use crate::model::cell::{CellKey, CellValue};
|
||||||
let mut app = records_model_with_two_rows();
|
let mut app = records_model_with_two_rows();
|
||||||
// Simulate Tab-at-bottom-right having produced an empty-key cell.
|
// Simulate Tab-at-bottom-right having produced an empty-key cell.
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.set_cell(CellKey::new(vec![]), CellValue::Number(0.0));
|
.set_cell(CellKey::new(vec![]), CellValue::Number(0.0));
|
||||||
assert!(
|
assert!(
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.data
|
.data
|
||||||
.iter_cells()
|
.iter_cells()
|
||||||
@@ -976,7 +999,7 @@ mod tests {
|
|||||||
"setup: should have left records mode"
|
"setup: should have left records mode"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!app.workbook
|
!app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.data
|
.data
|
||||||
.iter_cells()
|
.iter_cells()
|
||||||
@@ -994,7 +1017,7 @@ mod tests {
|
|||||||
let mut app = records_model_with_two_rows();
|
let mut app = records_model_with_two_rows();
|
||||||
let last_row = app.layout.row_count() - 1;
|
let last_row = app.layout.row_count() - 1;
|
||||||
let last_col = app.layout.col_count() - 1;
|
let last_col = app.layout.col_count() - 1;
|
||||||
app.workbook.active_view_mut().selected = (last_row, last_col);
|
app.model_state.workbook.active_view_mut().selected = (last_row, last_col);
|
||||||
|
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -1025,7 +1048,7 @@ mod tests {
|
|||||||
|
|
||||||
let last_row = initial_rows - 1;
|
let last_row = initial_rows - 1;
|
||||||
let last_col = app.layout.col_count() - 1;
|
let last_col = app.layout.col_count() - 1;
|
||||||
app.workbook.active_view_mut().selected = (last_row, last_col);
|
app.model_state.workbook.active_view_mut().selected = (last_row, last_col);
|
||||||
|
|
||||||
// Enter edit mode on the bottom-right cell
|
// Enter edit mode on the bottom-right cell
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||||
@@ -1042,7 +1065,7 @@ mod tests {
|
|||||||
"TAB on bottom-right should insert a record below"
|
"TAB on bottom-right should insert a record below"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.workbook.active_view().selected,
|
app.model_state.workbook.active_view().selected,
|
||||||
(initial_rows, 0),
|
(initial_rows, 0),
|
||||||
"TAB should move to first cell of the new row"
|
"TAB should move to first cell of the new row"
|
||||||
);
|
);
|
||||||
@@ -1078,7 +1101,7 @@ mod tests {
|
|||||||
let value_col = (0..app.layout.col_count())
|
let value_col = (0..app.layout.col_count())
|
||||||
.find(|&col| app.layout.col_label(col) == "Value")
|
.find(|&col| app.layout.col_label(col) == "Value")
|
||||||
.expect("drill view should include a Value column");
|
.expect("drill view should include a Value column");
|
||||||
app.workbook.active_view_mut().selected = (0, value_col);
|
app.model_state.workbook.active_view_mut().selected = (0, value_col);
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE))
|
||||||
@@ -1089,7 +1112,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.workbook.model.get_cell(&record_key),
|
app.model_state.workbook.model.get_cell(&record_key),
|
||||||
Some(&CellValue::Number(1.0)),
|
Some(&CellValue::Number(1.0)),
|
||||||
"drill edit should remain staged until leaving the drill view"
|
"drill edit should remain staged until leaving the drill view"
|
||||||
);
|
);
|
||||||
@@ -1107,7 +1130,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.workbook.model.get_cell(&record_key),
|
app.model_state.workbook.model.get_cell(&record_key),
|
||||||
Some(&CellValue::Number(9.0)),
|
Some(&CellValue::Number(9.0)),
|
||||||
"leaving drill view should apply the staged edit"
|
"leaving drill view should apply the staged edit"
|
||||||
);
|
);
|
||||||
@@ -1140,7 +1163,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
!app.workbook
|
!app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.category("Region")
|
.category("Region")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
+122
-104
@@ -24,7 +24,7 @@ pub trait Effect: Debug {
|
|||||||
pub struct AddCategory(pub String);
|
pub struct AddCategory(pub String);
|
||||||
impl Effect for AddCategory {
|
impl Effect for AddCategory {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
let _ = app.workbook.add_category(&self.0);
|
let _ = app.model_state.workbook.add_category(&self.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ pub struct AddItem {
|
|||||||
}
|
}
|
||||||
impl Effect for AddItem {
|
impl Effect for AddItem {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
if let Some(cat) = app.workbook.model.category_mut(&self.category) {
|
if let Some(cat) = app.model_state.workbook.model.category_mut(&self.category) {
|
||||||
cat.add_item(&self.item);
|
cat.add_item(&self.item);
|
||||||
} else {
|
} else {
|
||||||
app.status_msg = format!("Unknown category '{}'", self.category);
|
app.status_msg = format!("Unknown category '{}'", self.category);
|
||||||
@@ -51,7 +51,7 @@ pub struct AddItemInGroup {
|
|||||||
}
|
}
|
||||||
impl Effect for AddItemInGroup {
|
impl Effect for AddItemInGroup {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
if let Some(cat) = app.workbook.model.category_mut(&self.category) {
|
if let Some(cat) = app.model_state.workbook.model.category_mut(&self.category) {
|
||||||
cat.add_item_in_group(&self.item, &self.group);
|
cat.add_item_in_group(&self.item, &self.group);
|
||||||
} else {
|
} else {
|
||||||
app.status_msg = format!("Unknown category '{}'", self.category);
|
app.status_msg = format!("Unknown category '{}'", self.category);
|
||||||
@@ -63,7 +63,7 @@ impl Effect for AddItemInGroup {
|
|||||||
pub struct SortData;
|
pub struct SortData;
|
||||||
impl Effect for SortData {
|
impl Effect for SortData {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.model.data.sort_by_key();
|
app.model_state.workbook.model.data.sort_by_key();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ impl Effect for SortData {
|
|||||||
pub struct SetCell(pub CellKey, pub CellValue);
|
pub struct SetCell(pub CellKey, pub CellValue);
|
||||||
impl Effect for SetCell {
|
impl Effect for SetCell {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.model.set_cell(self.0.clone(), self.1.clone());
|
app.model_state.workbook.model.set_cell(self.0.clone(), self.1.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ impl Effect for SetCell {
|
|||||||
pub struct ClearCell(pub CellKey);
|
pub struct ClearCell(pub CellKey);
|
||||||
impl Effect for ClearCell {
|
impl Effect for ClearCell {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.model.clear_cell(&self.0);
|
app.model_state.workbook.model.clear_cell(&self.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,11 +96,11 @@ impl Effect for AddFormula {
|
|||||||
// appears in the grid. _Measure targets are dynamically included
|
// appears in the grid. _Measure targets are dynamically included
|
||||||
// via Model::measure_item_names().
|
// via Model::measure_item_names().
|
||||||
if formula.target_category != "_Measure"
|
if formula.target_category != "_Measure"
|
||||||
&& let Some(cat) = app.workbook.model.category_mut(&formula.target_category)
|
&& let Some(cat) = app.model_state.workbook.model.category_mut(&formula.target_category)
|
||||||
{
|
{
|
||||||
cat.add_item(&formula.target);
|
cat.add_item(&formula.target);
|
||||||
}
|
}
|
||||||
app.workbook.model.add_formula(formula);
|
app.model_state.workbook.model.add_formula(formula);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("Formula error: {e}");
|
app.status_msg = format!("Formula error: {e}");
|
||||||
@@ -116,7 +116,7 @@ pub struct RemoveFormula {
|
|||||||
}
|
}
|
||||||
impl Effect for RemoveFormula {
|
impl Effect for RemoveFormula {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.remove_formula(&self.target, &self.target_category);
|
.remove_formula(&self.target, &self.target_category);
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ impl Effect for EnterEditAtCursor {
|
|||||||
pub struct TogglePruneEmpty;
|
pub struct TogglePruneEmpty;
|
||||||
impl Effect for TogglePruneEmpty {
|
impl Effect for TogglePruneEmpty {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
let v = app.workbook.active_view_mut();
|
let v = app.model_state.workbook.active_view_mut();
|
||||||
v.prune_empty = !v.prune_empty;
|
v.prune_empty = !v.prune_empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ pub struct RemoveItem {
|
|||||||
}
|
}
|
||||||
impl Effect for RemoveItem {
|
impl Effect for RemoveItem {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.model.remove_item(&self.category, &self.item);
|
app.model_state.workbook.model.remove_item(&self.category, &self.item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ impl Effect for RemoveItem {
|
|||||||
pub struct RemoveCategory(pub String);
|
pub struct RemoveCategory(pub String);
|
||||||
impl Effect for RemoveCategory {
|
impl Effect for RemoveCategory {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.remove_category(&self.0);
|
app.model_state.workbook.remove_category(&self.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ impl Effect for RemoveCategory {
|
|||||||
pub struct CreateView(pub String);
|
pub struct CreateView(pub String);
|
||||||
impl Effect for CreateView {
|
impl Effect for CreateView {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.create_view(&self.0);
|
app.model_state.workbook.create_view(&self.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ impl Effect for CreateView {
|
|||||||
pub struct DeleteView(pub String);
|
pub struct DeleteView(pub String);
|
||||||
impl Effect for DeleteView {
|
impl Effect for DeleteView {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
let _ = app.workbook.delete_view(&self.0);
|
let _ = app.model_state.workbook.delete_view(&self.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ impl Effect for DeleteView {
|
|||||||
pub struct SwitchView(pub String);
|
pub struct SwitchView(pub String);
|
||||||
impl Effect for SwitchView {
|
impl Effect for SwitchView {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
let current = app.workbook.active_view.clone();
|
let current = app.model_state.workbook.active_view.clone();
|
||||||
if current != self.0 {
|
if current != self.0 {
|
||||||
app.view_back_stack.push(ViewFrame {
|
app.view_back_stack.push(ViewFrame {
|
||||||
view_name: current,
|
view_name: current,
|
||||||
@@ -217,7 +217,7 @@ impl Effect for SwitchView {
|
|||||||
});
|
});
|
||||||
app.view_forward_stack.clear();
|
app.view_forward_stack.clear();
|
||||||
}
|
}
|
||||||
let _ = app.workbook.switch_view(&self.0);
|
let _ = app.model_state.workbook.switch_view(&self.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,12 +227,12 @@ pub struct ViewBack;
|
|||||||
impl Effect for ViewBack {
|
impl Effect for ViewBack {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
if let Some(frame) = app.view_back_stack.pop() {
|
if let Some(frame) = app.view_back_stack.pop() {
|
||||||
let current = app.workbook.active_view.clone();
|
let current = app.model_state.workbook.active_view.clone();
|
||||||
app.view_forward_stack.push(ViewFrame {
|
app.view_forward_stack.push(ViewFrame {
|
||||||
view_name: current,
|
view_name: current,
|
||||||
mode: app.mode.clone(),
|
mode: app.mode.clone(),
|
||||||
});
|
});
|
||||||
let _ = app.workbook.switch_view(&frame.view_name);
|
let _ = app.model_state.workbook.switch_view(&frame.view_name);
|
||||||
app.mode = frame.mode;
|
app.mode = frame.mode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,12 +244,12 @@ pub struct ViewForward;
|
|||||||
impl Effect for ViewForward {
|
impl Effect for ViewForward {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
if let Some(frame) = app.view_forward_stack.pop() {
|
if let Some(frame) = app.view_forward_stack.pop() {
|
||||||
let current = app.workbook.active_view.clone();
|
let current = app.model_state.workbook.active_view.clone();
|
||||||
app.view_back_stack.push(ViewFrame {
|
app.view_back_stack.push(ViewFrame {
|
||||||
view_name: current,
|
view_name: current,
|
||||||
mode: app.mode.clone(),
|
mode: app.mode.clone(),
|
||||||
});
|
});
|
||||||
let _ = app.workbook.switch_view(&frame.view_name);
|
let _ = app.model_state.workbook.switch_view(&frame.view_name);
|
||||||
app.mode = frame.mode;
|
app.mode = frame.mode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +262,7 @@ pub struct SetAxis {
|
|||||||
}
|
}
|
||||||
impl Effect for SetAxis {
|
impl Effect for SetAxis {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.active_view_mut()
|
.active_view_mut()
|
||||||
.set_axis(&self.category, self.axis);
|
.set_axis(&self.category, self.axis);
|
||||||
}
|
}
|
||||||
@@ -275,7 +275,7 @@ pub struct SetPageSelection {
|
|||||||
}
|
}
|
||||||
impl Effect for SetPageSelection {
|
impl Effect for SetPageSelection {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.active_view_mut()
|
.active_view_mut()
|
||||||
.set_page_selection(&self.category, &self.item);
|
.set_page_selection(&self.category, &self.item);
|
||||||
}
|
}
|
||||||
@@ -288,7 +288,7 @@ pub struct ToggleGroup {
|
|||||||
}
|
}
|
||||||
impl Effect for ToggleGroup {
|
impl Effect for ToggleGroup {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.active_view_mut()
|
.active_view_mut()
|
||||||
.toggle_group_collapse(&self.category, &self.group);
|
.toggle_group_collapse(&self.category, &self.group);
|
||||||
}
|
}
|
||||||
@@ -301,7 +301,7 @@ pub struct HideItem {
|
|||||||
}
|
}
|
||||||
impl Effect for HideItem {
|
impl Effect for HideItem {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.active_view_mut()
|
.active_view_mut()
|
||||||
.hide_item(&self.category, &self.item);
|
.hide_item(&self.category, &self.item);
|
||||||
}
|
}
|
||||||
@@ -314,7 +314,7 @@ pub struct ShowItem {
|
|||||||
}
|
}
|
||||||
impl Effect for ShowItem {
|
impl Effect for ShowItem {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.active_view_mut()
|
.active_view_mut()
|
||||||
.show_item(&self.category, &self.item);
|
.show_item(&self.category, &self.item);
|
||||||
}
|
}
|
||||||
@@ -324,7 +324,7 @@ impl Effect for ShowItem {
|
|||||||
pub struct TransposeAxes;
|
pub struct TransposeAxes;
|
||||||
impl Effect for TransposeAxes {
|
impl Effect for TransposeAxes {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.active_view_mut().transpose_axes();
|
app.model_state.workbook.active_view_mut().transpose_axes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ impl Effect for TransposeAxes {
|
|||||||
pub struct CycleAxis(pub String);
|
pub struct CycleAxis(pub String);
|
||||||
impl Effect for CycleAxis {
|
impl Effect for CycleAxis {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.active_view_mut().cycle_axis(&self.0);
|
app.model_state.workbook.active_view_mut().cycle_axis(&self.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +340,7 @@ impl Effect for CycleAxis {
|
|||||||
pub struct SetNumberFormat(pub String);
|
pub struct SetNumberFormat(pub String);
|
||||||
impl Effect for SetNumberFormat {
|
impl Effect for SetNumberFormat {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.active_view_mut().number_format = self.0.clone();
|
app.model_state.workbook.active_view_mut().number_format = self.0.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@ impl Effect for SetNumberFormat {
|
|||||||
pub struct SetSelected(pub usize, pub usize);
|
pub struct SetSelected(pub usize, pub usize);
|
||||||
impl Effect for SetSelected {
|
impl Effect for SetSelected {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.active_view_mut().selected = (self.0, self.1);
|
app.model_state.workbook.active_view_mut().selected = (self.0, self.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +358,7 @@ impl Effect for SetSelected {
|
|||||||
pub struct SetRowOffset(pub usize);
|
pub struct SetRowOffset(pub usize);
|
||||||
impl Effect for SetRowOffset {
|
impl Effect for SetRowOffset {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.active_view_mut().row_offset = self.0;
|
app.model_state.workbook.active_view_mut().row_offset = self.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ impl Effect for SetRowOffset {
|
|||||||
pub struct SetColOffset(pub usize);
|
pub struct SetColOffset(pub usize);
|
||||||
impl Effect for SetColOffset {
|
impl Effect for SetColOffset {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.workbook.active_view_mut().col_offset = self.0;
|
app.model_state.workbook.active_view_mut().col_offset = self.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +395,7 @@ impl Effect for SetStatus {
|
|||||||
pub struct MarkDirty;
|
pub struct MarkDirty;
|
||||||
impl Effect for MarkDirty {
|
impl Effect for MarkDirty {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.dirty = true;
|
app.model_state.dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,25 +480,25 @@ impl Effect for ApplyAndClearDrill {
|
|||||||
if col_name == "Value" {
|
if col_name == "Value" {
|
||||||
// Update the cell's value
|
// Update the cell's value
|
||||||
let value = if new_value.is_empty() {
|
let value = if new_value.is_empty() {
|
||||||
app.workbook.model.clear_cell(orig_key);
|
app.model_state.workbook.model.clear_cell(orig_key);
|
||||||
continue;
|
continue;
|
||||||
} else if let Ok(n) = new_value.parse::<f64>() {
|
} else if let Ok(n) = new_value.parse::<f64>() {
|
||||||
CellValue::Number(n)
|
CellValue::Number(n)
|
||||||
} else {
|
} else {
|
||||||
CellValue::Text(new_value.clone())
|
CellValue::Text(new_value.clone())
|
||||||
};
|
};
|
||||||
app.workbook.model.set_cell(orig_key.clone(), value);
|
app.model_state.workbook.model.set_cell(orig_key.clone(), value);
|
||||||
} else {
|
} else {
|
||||||
if new_value.is_empty() {
|
if new_value.is_empty() {
|
||||||
app.status_msg = RECORD_COORDS_CANNOT_BE_EMPTY.to_string();
|
app.status_msg = RECORD_COORDS_CANNOT_BE_EMPTY.to_string();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Rename a coordinate: remove old cell, insert new with updated coord
|
// Rename a coordinate: remove old cell, insert new with updated coord
|
||||||
let value = match app.workbook.model.get_cell(orig_key) {
|
let value = match app.model_state.workbook.model.get_cell(orig_key) {
|
||||||
Some(v) => v.clone(),
|
Some(v) => v.clone(),
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
app.workbook.model.clear_cell(orig_key);
|
app.model_state.workbook.model.clear_cell(orig_key);
|
||||||
// Build new key by replacing the coord
|
// Build new key by replacing the coord
|
||||||
let new_coords: Vec<(String, String)> = orig_key
|
let new_coords: Vec<(String, String)> = orig_key
|
||||||
.0
|
.0
|
||||||
@@ -513,13 +513,13 @@ impl Effect for ApplyAndClearDrill {
|
|||||||
.collect();
|
.collect();
|
||||||
let new_key = CellKey::new(new_coords);
|
let new_key = CellKey::new(new_coords);
|
||||||
// Ensure the new item exists in that category
|
// Ensure the new item exists in that category
|
||||||
if let Some(cat) = app.workbook.model.category_mut(col_name) {
|
if let Some(cat) = app.model_state.workbook.model.category_mut(col_name) {
|
||||||
cat.add_item(new_value.clone());
|
cat.add_item(new_value.clone());
|
||||||
}
|
}
|
||||||
app.workbook.model.set_cell(new_key, value);
|
app.model_state.workbook.model.set_cell(new_key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.dirty = true;
|
app.model_state.dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,10 +547,10 @@ impl Effect for SetDrillPendingEdit {
|
|||||||
pub struct Save;
|
pub struct Save;
|
||||||
impl Effect for Save {
|
impl Effect for Save {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
if let Some(ref path) = app.file_path {
|
if let Some(ref path) = app.model_state.file_path {
|
||||||
match crate::persistence::save(&app.workbook, path) {
|
match crate::persistence::save(&app.model_state.workbook, path) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
app.dirty = false;
|
app.model_state.dirty = false;
|
||||||
app.status_msg = format!("Saved to {}", path.display());
|
app.status_msg = format!("Saved to {}", path.display());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -567,10 +567,10 @@ impl Effect for Save {
|
|||||||
pub struct SaveAs(pub PathBuf);
|
pub struct SaveAs(pub PathBuf);
|
||||||
impl Effect for SaveAs {
|
impl Effect for SaveAs {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
match crate::persistence::save(&app.workbook, &self.0) {
|
match crate::persistence::save(&app.model_state.workbook, &self.0) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
app.file_path = Some(self.0.clone());
|
app.model_state.file_path = Some(self.0.clone());
|
||||||
app.dirty = false;
|
app.model_state.dirty = false;
|
||||||
app.status_msg = format!("Saved to {}", self.0.display());
|
app.status_msg = format!("Saved to {}", self.0.display());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -685,9 +685,9 @@ impl Effect for WizardKey {
|
|||||||
crossterm::event::KeyCode::Enter => match wizard.build_model() {
|
crossterm::event::KeyCode::Enter => match wizard.build_model() {
|
||||||
Ok(mut workbook) => {
|
Ok(mut workbook) => {
|
||||||
workbook.normalize_view_state();
|
workbook.normalize_view_state();
|
||||||
app.workbook = workbook;
|
app.model_state.workbook = workbook;
|
||||||
app.formula_cursor = 0;
|
app.formula_cursor = 0;
|
||||||
app.dirty = true;
|
app.model_state.dirty = true;
|
||||||
app.status_msg = "Import successful! Press :w <path> to save.".to_string();
|
app.status_msg = "Import successful! Press :w <path> to save.".to_string();
|
||||||
app.mode = AppMode::Normal;
|
app.mode = AppMode::Normal;
|
||||||
app.wizard = None;
|
app.wizard = None;
|
||||||
@@ -738,8 +738,8 @@ impl Effect for StartImportWizard {
|
|||||||
pub struct ExportCsv(pub PathBuf);
|
pub struct ExportCsv(pub PathBuf);
|
||||||
impl Effect for ExportCsv {
|
impl Effect for ExportCsv {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
let view_name = app.workbook.active_view.clone();
|
let view_name = app.model_state.workbook.active_view.clone();
|
||||||
match crate::persistence::export_csv(&app.workbook, &view_name, &self.0) {
|
match crate::persistence::export_csv(&app.model_state.workbook, &view_name, &self.0) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
app.status_msg = format!("Exported to {}", self.0.display());
|
app.status_msg = format!("Exported to {}", self.0.display());
|
||||||
}
|
}
|
||||||
@@ -758,7 +758,7 @@ impl Effect for LoadModel {
|
|||||||
match crate::persistence::load(&self.0) {
|
match crate::persistence::load(&self.0) {
|
||||||
Ok(mut loaded) => {
|
Ok(mut loaded) => {
|
||||||
loaded.normalize_view_state();
|
loaded.normalize_view_state();
|
||||||
app.workbook = loaded;
|
app.model_state.workbook = loaded;
|
||||||
app.status_msg = format!("Loaded from {}", self.0.display());
|
app.status_msg = format!("Loaded from {}", self.0.display());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -869,7 +869,7 @@ impl Effect for ImportJsonHeadless {
|
|||||||
|
|
||||||
match pipeline.build_model() {
|
match pipeline.build_model() {
|
||||||
Ok(new_workbook) => {
|
Ok(new_workbook) => {
|
||||||
app.workbook = new_workbook;
|
app.model_state.workbook = new_workbook;
|
||||||
app.status_msg = "Imported successfully".to_string();
|
app.status_msg = "Imported successfully".to_string();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -953,6 +953,8 @@ pub struct CleanEmptyRecords;
|
|||||||
impl Effect for CleanEmptyRecords {
|
impl Effect for CleanEmptyRecords {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
let empties: Vec<CellKey> = app
|
let empties: Vec<CellKey> = app
|
||||||
|
.model_state
|
||||||
|
|
||||||
.workbook
|
.workbook
|
||||||
.model
|
.model
|
||||||
.data
|
.data
|
||||||
@@ -960,7 +962,7 @@ impl Effect for CleanEmptyRecords {
|
|||||||
.filter_map(|(k, _)| if k.0.is_empty() { Some(k) } else { None })
|
.filter_map(|(k, _)| if k.0.is_empty() { Some(k) } else { None })
|
||||||
.collect();
|
.collect();
|
||||||
for key in empties {
|
for key in empties {
|
||||||
app.workbook.model.clear_cell(&key);
|
app.model_state.workbook.model.clear_cell(&key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1045,7 +1047,7 @@ mod tests {
|
|||||||
fn add_category_effect() {
|
fn add_category_effect() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
AddCategory("Region".to_string()).apply(&mut app);
|
AddCategory("Region".to_string()).apply(&mut app);
|
||||||
assert!(app.workbook.model.category("Region").is_some());
|
assert!(app.model_state.workbook.model.category("Region").is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1057,6 +1059,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
let items: Vec<&str> = app
|
let items: Vec<&str> = app
|
||||||
|
.model_state
|
||||||
|
|
||||||
.workbook
|
.workbook
|
||||||
.model
|
.model
|
||||||
.category("Type")
|
.category("Type")
|
||||||
@@ -1087,12 +1091,12 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
SetCell(key.clone(), CellValue::Number(42.0)).apply(&mut app);
|
SetCell(key.clone(), CellValue::Number(42.0)).apply(&mut app);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.workbook.model.get_cell(&key),
|
app.model_state.workbook.model.get_cell(&key),
|
||||||
Some(&CellValue::Number(42.0))
|
Some(&CellValue::Number(42.0))
|
||||||
);
|
);
|
||||||
|
|
||||||
ClearCell(key.clone()).apply(&mut app);
|
ClearCell(key.clone()).apply(&mut app);
|
||||||
assert_eq!(app.workbook.model.get_cell(&key), None);
|
assert_eq!(app.model_state.workbook.model.get_cell(&key), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1103,7 +1107,7 @@ mod tests {
|
|||||||
target_category: "Type".to_string(),
|
target_category: "Type".to_string(),
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(!app.workbook.model.formulas().is_empty());
|
assert!(!app.model_state.workbook.model.formulas().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Regression: AddFormula must add the target item to the target category
|
/// Regression: AddFormula must add the target item to the target category
|
||||||
@@ -1114,7 +1118,7 @@ mod tests {
|
|||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
// "Margin" does not exist as an item in "Type" before adding the formula
|
// "Margin" does not exist as an item in "Type" before adding the formula
|
||||||
assert!(
|
assert!(
|
||||||
!app.workbook
|
!app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.category("Type")
|
.category("Type")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1127,6 +1131,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
let items: Vec<&str> = app
|
let items: Vec<&str> = app
|
||||||
|
.model_state
|
||||||
|
|
||||||
.workbook
|
.workbook
|
||||||
.model
|
.model
|
||||||
.category("Type")
|
.category("Type")
|
||||||
@@ -1152,7 +1158,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
// Should appear in effective_item_names (used by layout)
|
// Should appear in effective_item_names (used by layout)
|
||||||
let effective = app.workbook.model.effective_item_names("_Measure");
|
let effective = app.model_state.workbook.model.effective_item_names("_Measure");
|
||||||
assert!(
|
assert!(
|
||||||
effective.contains(&"Margin".to_string()),
|
effective.contains(&"Margin".to_string()),
|
||||||
"formula target 'Margin' should appear in effective _Measure items, got: {:?}",
|
"formula target 'Margin' should appear in effective _Measure items, got: {:?}",
|
||||||
@@ -1160,7 +1166,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
// Should NOT be in the category's own items
|
// Should NOT be in the category's own items
|
||||||
assert!(
|
assert!(
|
||||||
!app.workbook
|
!app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.category("_Measure")
|
.category("_Measure")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1189,13 +1195,13 @@ mod tests {
|
|||||||
target_category: "Type".to_string(),
|
target_category: "Type".to_string(),
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(!app.workbook.model.formulas().is_empty());
|
assert!(!app.model_state.workbook.model.formulas().is_empty());
|
||||||
RemoveFormula {
|
RemoveFormula {
|
||||||
target: "Clothing".to_string(),
|
target: "Clothing".to_string(),
|
||||||
target_category: "Type".to_string(),
|
target_category: "Type".to_string(),
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(app.workbook.model.formulas().is_empty());
|
assert!(app.model_state.workbook.model.formulas().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── View effects ────────────────────────────────────────────────────
|
// ── View effects ────────────────────────────────────────────────────
|
||||||
@@ -1203,11 +1209,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn switch_view_pushes_to_back_stack() {
|
fn switch_view_pushes_to_back_stack() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
app.workbook.create_view("View 2");
|
app.model_state.workbook.create_view("View 2");
|
||||||
assert!(app.view_back_stack.is_empty());
|
assert!(app.view_back_stack.is_empty());
|
||||||
|
|
||||||
SwitchView("View 2".to_string()).apply(&mut app);
|
SwitchView("View 2".to_string()).apply(&mut app);
|
||||||
assert_eq!(app.workbook.active_view.as_str(), "View 2");
|
assert_eq!(app.model_state.workbook.active_view.as_str(), "View 2");
|
||||||
assert_eq!(app.view_back_stack.len(), 1);
|
assert_eq!(app.view_back_stack.len(), 1);
|
||||||
assert_eq!(app.view_back_stack[0].view_name, "Default");
|
assert_eq!(app.view_back_stack[0].view_name, "Default");
|
||||||
// Forward stack should be cleared
|
// Forward stack should be cleared
|
||||||
@@ -1224,20 +1230,20 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn view_back_and_forward() {
|
fn view_back_and_forward() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
app.workbook.create_view("View 2");
|
app.model_state.workbook.create_view("View 2");
|
||||||
SwitchView("View 2".to_string()).apply(&mut app);
|
SwitchView("View 2".to_string()).apply(&mut app);
|
||||||
assert_eq!(app.workbook.active_view.as_str(), "View 2");
|
assert_eq!(app.model_state.workbook.active_view.as_str(), "View 2");
|
||||||
|
|
||||||
// Go back
|
// Go back
|
||||||
ViewBack.apply(&mut app);
|
ViewBack.apply(&mut app);
|
||||||
assert_eq!(app.workbook.active_view.as_str(), "Default");
|
assert_eq!(app.model_state.workbook.active_view.as_str(), "Default");
|
||||||
assert_eq!(app.view_forward_stack.len(), 1);
|
assert_eq!(app.view_forward_stack.len(), 1);
|
||||||
assert_eq!(app.view_forward_stack[0].view_name, "View 2");
|
assert_eq!(app.view_forward_stack[0].view_name, "View 2");
|
||||||
assert!(app.view_back_stack.is_empty());
|
assert!(app.view_back_stack.is_empty());
|
||||||
|
|
||||||
// Go forward
|
// Go forward
|
||||||
ViewForward.apply(&mut app);
|
ViewForward.apply(&mut app);
|
||||||
assert_eq!(app.workbook.active_view.as_str(), "View 2");
|
assert_eq!(app.model_state.workbook.active_view.as_str(), "View 2");
|
||||||
assert_eq!(app.view_back_stack.len(), 1);
|
assert_eq!(app.view_back_stack.len(), 1);
|
||||||
assert_eq!(app.view_back_stack[0].view_name, "Default");
|
assert_eq!(app.view_back_stack[0].view_name, "Default");
|
||||||
assert!(app.view_forward_stack.is_empty());
|
assert!(app.view_forward_stack.is_empty());
|
||||||
@@ -1246,19 +1252,19 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn view_back_with_empty_stack_is_noop() {
|
fn view_back_with_empty_stack_is_noop() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
let before = app.workbook.active_view.clone();
|
let before = app.model_state.workbook.active_view.clone();
|
||||||
ViewBack.apply(&mut app);
|
ViewBack.apply(&mut app);
|
||||||
assert_eq!(app.workbook.active_view, before);
|
assert_eq!(app.model_state.workbook.active_view, before);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_and_delete_view() {
|
fn create_and_delete_view() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
CreateView("View 2".to_string()).apply(&mut app);
|
CreateView("View 2".to_string()).apply(&mut app);
|
||||||
assert!(app.workbook.views.contains_key("View 2"));
|
assert!(app.model_state.workbook.views.contains_key("View 2"));
|
||||||
|
|
||||||
DeleteView("View 2".to_string()).apply(&mut app);
|
DeleteView("View 2".to_string()).apply(&mut app);
|
||||||
assert!(!app.workbook.views.contains_key("View 2"));
|
assert!(!app.model_state.workbook.views.contains_key("View 2"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1269,13 +1275,15 @@ mod tests {
|
|||||||
axis: Axis::Page,
|
axis: Axis::Page,
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert_eq!(app.workbook.active_view().axis_of("Type"), Axis::Page);
|
assert_eq!(app.model_state.workbook.active_view().axis_of("Type"), Axis::Page);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transpose_axes_effect() {
|
fn transpose_axes_effect() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
let row_before: Vec<String> = app
|
let row_before: Vec<String> = app
|
||||||
|
.model_state
|
||||||
|
|
||||||
.workbook
|
.workbook
|
||||||
.active_view()
|
.active_view()
|
||||||
.categories_on(Axis::Row)
|
.categories_on(Axis::Row)
|
||||||
@@ -1283,6 +1291,8 @@ mod tests {
|
|||||||
.map(String::from)
|
.map(String::from)
|
||||||
.collect();
|
.collect();
|
||||||
let col_before: Vec<String> = app
|
let col_before: Vec<String> = app
|
||||||
|
.model_state
|
||||||
|
|
||||||
.workbook
|
.workbook
|
||||||
.active_view()
|
.active_view()
|
||||||
.categories_on(Axis::Column)
|
.categories_on(Axis::Column)
|
||||||
@@ -1291,6 +1301,8 @@ mod tests {
|
|||||||
.collect();
|
.collect();
|
||||||
TransposeAxes.apply(&mut app);
|
TransposeAxes.apply(&mut app);
|
||||||
let row_after: Vec<String> = app
|
let row_after: Vec<String> = app
|
||||||
|
.model_state
|
||||||
|
|
||||||
.workbook
|
.workbook
|
||||||
.active_view()
|
.active_view()
|
||||||
.categories_on(Axis::Row)
|
.categories_on(Axis::Row)
|
||||||
@@ -1298,6 +1310,8 @@ mod tests {
|
|||||||
.map(String::from)
|
.map(String::from)
|
||||||
.collect();
|
.collect();
|
||||||
let col_after: Vec<String> = app
|
let col_after: Vec<String> = app
|
||||||
|
.model_state
|
||||||
|
|
||||||
.workbook
|
.workbook
|
||||||
.active_view()
|
.active_view()
|
||||||
.categories_on(Axis::Column)
|
.categories_on(Axis::Column)
|
||||||
@@ -1314,7 +1328,7 @@ mod tests {
|
|||||||
fn set_selected_effect() {
|
fn set_selected_effect() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
SetSelected(3, 5).apply(&mut app);
|
SetSelected(3, 5).apply(&mut app);
|
||||||
assert_eq!(app.workbook.active_view().selected, (3, 5));
|
assert_eq!(app.model_state.workbook.active_view().selected, (3, 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1322,8 +1336,8 @@ mod tests {
|
|||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
SetRowOffset(10).apply(&mut app);
|
SetRowOffset(10).apply(&mut app);
|
||||||
SetColOffset(5).apply(&mut app);
|
SetColOffset(5).apply(&mut app);
|
||||||
assert_eq!(app.workbook.active_view().row_offset, 10);
|
assert_eq!(app.model_state.workbook.active_view().row_offset, 10);
|
||||||
assert_eq!(app.workbook.active_view().col_offset, 5);
|
assert_eq!(app.model_state.workbook.active_view().col_offset, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── App state effects ───────────────────────────────────────────────
|
// ── App state effects ───────────────────────────────────────────────
|
||||||
@@ -1369,7 +1383,7 @@ mod tests {
|
|||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
// An empty-key cell (the bug: produced by AddRecordRow when no page
|
// An empty-key cell (the bug: produced by AddRecordRow when no page
|
||||||
// filters are set).
|
// filters are set).
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.set_cell(CellKey::new(vec![]), CellValue::Number(0.0));
|
.set_cell(CellKey::new(vec![]), CellValue::Number(0.0));
|
||||||
// Plus a well-formed cell that must survive.
|
// Plus a well-formed cell that must survive.
|
||||||
@@ -1377,15 +1391,15 @@ mod tests {
|
|||||||
("Type".to_string(), "Food".to_string()),
|
("Type".to_string(), "Food".to_string()),
|
||||||
("Month".to_string(), "Jan".to_string()),
|
("Month".to_string(), "Jan".to_string()),
|
||||||
]);
|
]);
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.set_cell(valid.clone(), CellValue::Number(42.0));
|
.set_cell(valid.clone(), CellValue::Number(42.0));
|
||||||
assert_eq!(app.workbook.model.data.iter_cells().count(), 2);
|
assert_eq!(app.model_state.workbook.model.data.iter_cells().count(), 2);
|
||||||
|
|
||||||
CleanEmptyRecords.apply(&mut app);
|
CleanEmptyRecords.apply(&mut app);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
!app.workbook
|
!app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.data
|
.data
|
||||||
.iter_cells()
|
.iter_cells()
|
||||||
@@ -1393,7 +1407,7 @@ mod tests {
|
|||||||
"empty-key cell should be gone"
|
"empty-key cell should be gone"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.workbook.model.get_cell(&valid),
|
app.model_state.workbook.model.get_cell(&valid),
|
||||||
Some(&CellValue::Number(42.0)),
|
Some(&CellValue::Number(42.0)),
|
||||||
"valid cell must survive"
|
"valid cell must survive"
|
||||||
);
|
);
|
||||||
@@ -1459,9 +1473,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn mark_dirty_effect() {
|
fn mark_dirty_effect() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
assert!(!app.dirty);
|
assert!(!app.model_state.dirty);
|
||||||
MarkDirty.apply(&mut app);
|
MarkDirty.apply(&mut app);
|
||||||
assert!(app.dirty);
|
assert!(app.model_state.dirty);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1587,7 +1601,7 @@ mod tests {
|
|||||||
// Apply with no pending edits — should just clear state
|
// Apply with no pending edits — should just clear state
|
||||||
ApplyAndClearDrill.apply(&mut app);
|
ApplyAndClearDrill.apply(&mut app);
|
||||||
assert!(app.drill_state.is_none());
|
assert!(app.drill_state.is_none());
|
||||||
assert!(!app.dirty); // no edits → not dirty
|
assert!(!app.model_state.dirty); // no edits → not dirty
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1598,7 +1612,7 @@ mod tests {
|
|||||||
("Month".into(), "Jan".into()),
|
("Month".into(), "Jan".into()),
|
||||||
]);
|
]);
|
||||||
// Set original cell
|
// Set original cell
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.set_cell(key.clone(), CellValue::Number(42.0));
|
.set_cell(key.clone(), CellValue::Number(42.0));
|
||||||
|
|
||||||
@@ -1615,9 +1629,9 @@ mod tests {
|
|||||||
|
|
||||||
ApplyAndClearDrill.apply(&mut app);
|
ApplyAndClearDrill.apply(&mut app);
|
||||||
assert!(app.drill_state.is_none());
|
assert!(app.drill_state.is_none());
|
||||||
assert!(app.dirty);
|
assert!(app.model_state.dirty);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.workbook.model.get_cell(&key),
|
app.model_state.workbook.model.get_cell(&key),
|
||||||
Some(&CellValue::Number(99.0))
|
Some(&CellValue::Number(99.0))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1629,7 +1643,7 @@ mod tests {
|
|||||||
("Type".into(), "Food".into()),
|
("Type".into(), "Food".into()),
|
||||||
("Month".into(), "Jan".into()),
|
("Month".into(), "Jan".into()),
|
||||||
]);
|
]);
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.set_cell(key.clone(), CellValue::Number(42.0));
|
.set_cell(key.clone(), CellValue::Number(42.0));
|
||||||
|
|
||||||
@@ -1645,20 +1659,22 @@ mod tests {
|
|||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
|
|
||||||
ApplyAndClearDrill.apply(&mut app);
|
ApplyAndClearDrill.apply(&mut app);
|
||||||
assert!(app.dirty);
|
assert!(app.model_state.dirty);
|
||||||
// Old cell should be gone
|
// Old cell should be gone
|
||||||
assert_eq!(app.workbook.model.get_cell(&key), None);
|
assert_eq!(app.model_state.workbook.model.get_cell(&key), None);
|
||||||
// New cell should exist
|
// New cell should exist
|
||||||
let new_key = CellKey::new(vec![
|
let new_key = CellKey::new(vec![
|
||||||
("Type".into(), "Drink".into()),
|
("Type".into(), "Drink".into()),
|
||||||
("Month".into(), "Jan".into()),
|
("Month".into(), "Jan".into()),
|
||||||
]);
|
]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.workbook.model.get_cell(&new_key),
|
app.model_state.workbook.model.get_cell(&new_key),
|
||||||
Some(&CellValue::Number(42.0))
|
Some(&CellValue::Number(42.0))
|
||||||
);
|
);
|
||||||
// "Drink" should have been added as an item
|
// "Drink" should have been added as an item
|
||||||
let items: Vec<&str> = app
|
let items: Vec<&str> = app
|
||||||
|
.model_state
|
||||||
|
|
||||||
.workbook
|
.workbook
|
||||||
.model
|
.model
|
||||||
.category("Type")
|
.category("Type")
|
||||||
@@ -1676,7 +1692,7 @@ mod tests {
|
|||||||
("Type".into(), "Food".into()),
|
("Type".into(), "Food".into()),
|
||||||
("Month".into(), "Jan".into()),
|
("Month".into(), "Jan".into()),
|
||||||
]);
|
]);
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.model
|
.model
|
||||||
.set_cell(key.clone(), CellValue::Number(42.0));
|
.set_cell(key.clone(), CellValue::Number(42.0));
|
||||||
|
|
||||||
@@ -1692,7 +1708,7 @@ mod tests {
|
|||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
|
|
||||||
ApplyAndClearDrill.apply(&mut app);
|
ApplyAndClearDrill.apply(&mut app);
|
||||||
assert_eq!(app.workbook.model.get_cell(&key), None);
|
assert_eq!(app.model_state.workbook.model.get_cell(&key), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Toggle effects ──────────────────────────────────────────────────
|
// ── Toggle effects ──────────────────────────────────────────────────
|
||||||
@@ -1700,11 +1716,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn toggle_prune_empty_effect() {
|
fn toggle_prune_empty_effect() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
let before = app.workbook.active_view().prune_empty;
|
let before = app.model_state.workbook.active_view().prune_empty;
|
||||||
TogglePruneEmpty.apply(&mut app);
|
TogglePruneEmpty.apply(&mut app);
|
||||||
assert_ne!(app.workbook.active_view().prune_empty, before);
|
assert_ne!(app.model_state.workbook.active_view().prune_empty, before);
|
||||||
TogglePruneEmpty.apply(&mut app);
|
TogglePruneEmpty.apply(&mut app);
|
||||||
assert_eq!(app.workbook.active_view().prune_empty, before);
|
assert_eq!(app.model_state.workbook.active_view().prune_empty, before);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1726,6 +1742,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
let items: Vec<&str> = app
|
let items: Vec<&str> = app
|
||||||
|
.model_state
|
||||||
|
|
||||||
.workbook
|
.workbook
|
||||||
.model
|
.model
|
||||||
.category("Type")
|
.category("Type")
|
||||||
@@ -1736,7 +1754,7 @@ mod tests {
|
|||||||
assert!(!items.contains(&"Food"));
|
assert!(!items.contains(&"Food"));
|
||||||
|
|
||||||
RemoveCategory("Month".to_string()).apply(&mut app);
|
RemoveCategory("Month".to_string()).apply(&mut app);
|
||||||
assert!(app.workbook.model.category("Month").is_none());
|
assert!(app.model_state.workbook.model.category("Month").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Number format ───────────────────────────────────────────────────
|
// ── Number format ───────────────────────────────────────────────────
|
||||||
@@ -1745,7 +1763,7 @@ mod tests {
|
|||||||
fn set_number_format_effect() {
|
fn set_number_format_effect() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
SetNumberFormat(",.2f".to_string()).apply(&mut app);
|
SetNumberFormat(",.2f".to_string()).apply(&mut app);
|
||||||
assert_eq!(app.workbook.active_view().number_format, ",.2f");
|
assert_eq!(app.model_state.workbook.active_view().number_format, ",.2f");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Page selection ──────────────────────────────────────────────────
|
// ── Page selection ──────────────────────────────────────────────────
|
||||||
@@ -1759,7 +1777,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.workbook.active_view().page_selection("Type"),
|
app.model_state.workbook.active_view().page_selection("Type"),
|
||||||
Some("Food")
|
Some("Food")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1774,14 +1792,14 @@ mod tests {
|
|||||||
item: "Food".to_string(),
|
item: "Food".to_string(),
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(app.workbook.active_view().is_hidden("Type", "Food"));
|
assert!(app.model_state.workbook.active_view().is_hidden("Type", "Food"));
|
||||||
|
|
||||||
ShowItem {
|
ShowItem {
|
||||||
category: "Type".to_string(),
|
category: "Type".to_string(),
|
||||||
item: "Food".to_string(),
|
item: "Food".to_string(),
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(!app.workbook.active_view().is_hidden("Type", "Food"));
|
assert!(!app.model_state.workbook.active_view().is_hidden("Type", "Food"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Toggle group ────────────────────────────────────────────────────
|
// ── Toggle group ────────────────────────────────────────────────────
|
||||||
@@ -1795,7 +1813,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(
|
assert!(
|
||||||
app.workbook
|
app.model_state.workbook
|
||||||
.active_view()
|
.active_view()
|
||||||
.is_group_collapsed("Type", "MyGroup")
|
.is_group_collapsed("Type", "MyGroup")
|
||||||
);
|
);
|
||||||
@@ -1805,7 +1823,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(
|
assert!(
|
||||||
!app.workbook
|
!app.model_state.workbook
|
||||||
.active_view()
|
.active_view()
|
||||||
.is_group_collapsed("Type", "MyGroup")
|
.is_group_collapsed("Type", "MyGroup")
|
||||||
);
|
);
|
||||||
@@ -1816,9 +1834,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn cycle_axis_effect() {
|
fn cycle_axis_effect() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
let before = app.workbook.active_view().axis_of("Type");
|
let before = app.model_state.workbook.active_view().axis_of("Type");
|
||||||
CycleAxis("Type".to_string()).apply(&mut app);
|
CycleAxis("Type".to_string()).apply(&mut app);
|
||||||
let after = app.workbook.active_view().axis_of("Type");
|
let after = app.model_state.workbook.active_view().axis_of("Type");
|
||||||
assert_ne!(before, after);
|
assert_ne!(before, after);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user