refactor(navigation): include AppMode in view navigation stack

Introduce `ViewFrame` to store both the view name and the `AppMode` when
pushing to the navigation stack. Update `view_back_stack` and
`view_forward_stack` to use `ViewFrame` instead of `String` . Update
`CmdContext` and `Effect` implementations (SwitchView, ViewBack,
ViewForward) to handle the new `ViewFrame` structure. Add `is_editing()`
helper to `AppMode` .

Co-Authored-By: fiddlerwoaroof/git-smart-commit (gemma-4-26B-A4B-it-UD-Q5_K_XL.gguf)
This commit is contained in:
Edward Langley
2026-04-15 21:32:34 -07:00
parent 23c7c530e3
commit 7c00695398
4 changed files with 49 additions and 18 deletions

View File

@ -20,6 +20,13 @@ use crate::ui::grid::{
};
use crate::view::GridLayout;
/// A saved view+mode pair for the navigation stack.
#[derive(Debug, Clone, PartialEq)]
pub struct ViewFrame {
pub view_name: String,
pub mode: AppMode,
}
/// Drill-down state: frozen record snapshot + pending edits that have not
/// yet been applied to the model.
#[derive(Debug, Clone, Default)]
@ -88,6 +95,11 @@ impl AppMode {
}
}
/// True for any cell-editing mode (normal or records).
pub fn is_editing(&self) -> bool {
matches!(self, Self::Editing { .. } | Self::RecordsEditing { .. })
}
pub fn editing() -> Self {
Self::Editing {
minibuf: MinibufferConfig {
@ -173,9 +185,9 @@ pub struct App {
pub tile_cat_idx: usize,
/// View navigation history: views visited before the current one.
/// Pushed on SwitchView, popped by `<` (back).
pub view_back_stack: Vec<String>,
pub view_back_stack: Vec<ViewFrame>,
/// Views that were "back-ed" from, available for forward navigation (`>`).
pub view_forward_stack: Vec<String>,
pub view_forward_stack: Vec<ViewFrame>,
/// Frozen records list for the drill view. When present, this is the
/// snapshot that records-mode layouts iterate — records don't disappear
/// when filters would change. Pending edits are stored alongside and

View File

@ -4,7 +4,7 @@ use std::path::PathBuf;
use crate::model::cell::{CellKey, CellValue};
use crate::view::Axis;
use super::app::{App, AppMode};
use super::app::{App, AppMode, ViewFrame};
pub(crate) const RECORD_COORDS_CANNOT_BE_EMPTY: &str = "Record coordinates cannot be empty";
@ -193,7 +193,10 @@ impl Effect for SwitchView {
fn apply(&self, app: &mut App) {
let current = app.model.active_view.clone();
if current != self.0 {
app.view_back_stack.push(current);
app.view_back_stack.push(ViewFrame {
view_name: current,
mode: app.mode.clone(),
});
app.view_forward_stack.clear();
}
let _ = app.model.switch_view(&self.0);
@ -205,10 +208,14 @@ impl Effect for SwitchView {
pub struct ViewBack;
impl Effect for ViewBack {
fn apply(&self, app: &mut App) {
if let Some(prev) = app.view_back_stack.pop() {
if let Some(frame) = app.view_back_stack.pop() {
let current = app.model.active_view.clone();
app.view_forward_stack.push(current);
let _ = app.model.switch_view(&prev);
app.view_forward_stack.push(ViewFrame {
view_name: current,
mode: app.mode.clone(),
});
let _ = app.model.switch_view(&frame.view_name);
app.mode = frame.mode;
}
}
}
@ -218,10 +225,14 @@ impl Effect for ViewBack {
pub struct ViewForward;
impl Effect for ViewForward {
fn apply(&self, app: &mut App) {
if let Some(next) = app.view_forward_stack.pop() {
if let Some(frame) = app.view_forward_stack.pop() {
let current = app.model.active_view.clone();
app.view_back_stack.push(current);
let _ = app.model.switch_view(&next);
app.view_back_stack.push(ViewFrame {
view_name: current,
mode: app.mode.clone(),
});
let _ = app.model.switch_view(&frame.view_name);
app.mode = frame.mode;
}
}
}
@ -1134,8 +1145,8 @@ mod tests {
SwitchView("View 2".to_string()).apply(&mut app);
assert_eq!(app.model.active_view.as_str(), "View 2");
assert_eq!(app.view_back_stack, vec!["Default".to_string()]);
// Forward stack should be cleared
assert_eq!(app.view_back_stack.len(), 1);
assert_eq!(app.view_back_stack[0].view_name, "Default");
assert!(app.view_forward_stack.is_empty());
}
@ -1156,13 +1167,15 @@ mod tests {
// Go back
ViewBack.apply(&mut app);
assert_eq!(app.model.active_view.as_str(), "Default");
assert_eq!(app.view_forward_stack, vec!["View 2".to_string()]);
assert_eq!(app.view_forward_stack.len(), 1);
assert_eq!(app.view_forward_stack[0].view_name, "View 2");
assert!(app.view_back_stack.is_empty());
// Go forward
ViewForward.apply(&mut app);
assert_eq!(app.model.active_view.as_str(), "View 2");
assert_eq!(app.view_back_stack, vec!["Default".to_string()]);
assert_eq!(app.view_back_stack.len(), 1);
assert_eq!(app.view_back_stack[0].view_name, "Default");
assert!(app.view_forward_stack.is_empty());
}