From 45b848dc670fe5848e52ff41d1afb6e6a9db5ed2 Mon Sep 17 00:00:00 2001 From: Ed L Date: Mon, 23 Mar 2026 23:32:26 -0700 Subject: [PATCH] fix: navigation bounds and stale state after model load - app.rs: scroll_rows (Ctrl+D/U) now clamps to the cross-product row count and follows the viewport, matching move_selection's behaviour. Previously it could push selected past the last row, causing selected_cell_key to return None and silently ignoring edits. - model.rs: add normalize_view_state() which resets row/col offsets to zero on all views. - main.rs, dispatch.rs, app.rs: call normalize_view_state() after every model replacement (initial load, :Load command, wizard import) so stale offsets from a previous session can't hide the grid. - app.rs: clamp formula_cursor to the current formula list length at the top of handle_formula_panel_key so a model reload with fewer formulas can't leave the cursor pointing past the end. Co-Authored-By: Claude Sonnet 4.6 --- src/main.rs | 5 ++++- src/ui/app.rs | 20 +++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index b46a71e..017c6a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,7 +73,10 @@ fn main() -> Result<()> { // Load or create model let mut model = if let Some(ref path) = file_path { if path.exists() { - persistence::load(path).with_context(|| format!("Failed to load {}", path.display()))? + let mut m = persistence::load(path) + .with_context(|| format!("Failed to load {}", path.display()))?; + m.normalize_view_state(); + m } else { let name = path.file_stem() .and_then(|s| s.to_str()) diff --git a/src/ui/app.rs b/src/ui/app.rs index 63f48a3..004b17d 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -594,6 +594,11 @@ impl App { // ── Panel key handlers ─────────────────────────────────────────────────── fn handle_formula_panel_key(&mut self, key: KeyEvent) -> Result<()> { + // Clamp cursor in case the formula list shrank since it was last set. + let flen = self.model.formulas.len(); + if flen == 0 { self.formula_cursor = 0; } + else { self.formula_cursor = self.formula_cursor.min(flen - 1); } + match key.code { KeyCode::Esc | KeyCode::Tab => { self.mode = AppMode::Normal; } KeyCode::Char('a') | KeyCode::Char('n') | KeyCode::Char('o') => { @@ -882,8 +887,10 @@ impl App { KeyCode::Backspace => wizard.pop_name_char(), KeyCode::Enter => { match wizard.build_model() { - Ok(model) => { + Ok(mut model) => { + model.normalize_view_state(); self.model = model; + self.formula_cursor = 0; self.dirty = true; self.status_msg = "Import successful! Press :w to save.".to_string(); self.mode = AppMode::Normal; @@ -956,9 +963,16 @@ impl App { } fn scroll_rows(&mut self, delta: i32) { + let row_max = { + let view = match self.model.active_view() { Some(v) => v, None => return }; + let row_cats: Vec = view.categories_on(Axis::Row).into_iter().map(String::from).collect(); + cross_product_strs(&row_cats, &self.model, view).len().saturating_sub(1) + }; if let Some(view) = self.model.active_view_mut() { - let new_r = (view.selected.0 as i32 + delta).max(0) as usize; - view.selected.0 = new_r; + let nr = (view.selected.0 as i32 + delta).clamp(0, row_max as i32) as usize; + view.selected.0 = nr; + if nr < view.row_offset { view.row_offset = nr; } + if nr >= view.row_offset + 20 { view.row_offset = nr.saturating_sub(19); } } }