diff --git a/src/ui/app.rs b/src/ui/app.rs index 98e4ef3..c499809 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -5,24 +5,25 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, Instant}; +use std::rc::Rc; + use crate::command::cmd::CmdContext; use crate::command::keymap::{Keymap, KeymapSet}; use crate::import::wizard::ImportWizard; use crate::model::cell::CellValue; use crate::model::Model; use crate::persistence; -use crate::ui::grid::{compute_col_widths, compute_row_header_width, compute_visible_cols, parse_number_format}; +use crate::ui::grid::{ + compute_col_widths, compute_row_header_width, compute_visible_cols, parse_number_format, +}; use crate::view::GridLayout; /// Drill-down state: frozen record snapshot + pending edits that have not /// yet been applied to the model. #[derive(Debug, Clone, Default)] pub struct DrillState { - /// Frozen snapshot of records shown in the drill view. - pub records: Vec<( - crate::model::cell::CellKey, - crate::model::cell::CellValue, - )>, + /// Frozen snapshot of records shown in the drill view (Rc for cheap cloning). + pub records: Rc>, /// Pending edits keyed by (record_idx, column_name) → new string value. /// column_name is either "Value" or a category name. pub pending_edits: std::collections::HashMap<(usize, String), String>, @@ -101,11 +102,18 @@ pub struct App { pub buffers: HashMap, /// Transient keymap for Emacs-style prefix key sequences (g→gg, y→yy, etc.) pub transient_keymap: Option>, + /// Current grid layout, derived from model + view + drill_state. + /// Rebuilt via `rebuild_layout()` after state changes. + pub layout: GridLayout, keymap_set: KeymapSet, } impl App { pub fn new(model: Model, file_path: Option) -> Self { + let layout = { + let view = model.active_view(); + GridLayout::with_frozen_records(&model, view, None) + }; Self { model, file_path, @@ -132,17 +140,26 @@ impl App { expanded_cats: std::collections::HashSet::new(), buffers: HashMap::new(), transient_keymap: None, + layout, keymap_set: KeymapSet::default_keymaps(), } } + /// Rebuild the grid layout from current model, view, and drill state. + /// Note: `with_frozen_records` already handles pruning internally. + pub fn rebuild_layout(&mut self) { + let view = self.model.active_view(); + let frozen = self.drill_state.as_ref().map(|s| Rc::clone(&s.records)); + self.layout = GridLayout::with_frozen_records(&self.model, view, frozen); + } + pub fn cmd_context(&self, key: KeyCode, _mods: KeyModifiers) -> CmdContext<'_> { let view = self.model.active_view(); - let frozen_records = self.drill_state.as_ref().map(|s| s.records.clone()); - let layout = GridLayout::with_frozen_records(&self.model, view, frozen_records); + let layout = &self.layout; let (sel_row, sel_col) = view.selected; CmdContext { model: &self.model, + layout, mode: &self.mode, selected: view.selected, row_offset: view.row_offset, @@ -159,23 +176,20 @@ impl App { cat_panel_cursor: self.cat_panel_cursor, view_panel_cursor: self.view_panel_cursor, tile_cat_idx: self.tile_cat_idx, - cell_key: layout.cell_key(sel_row, sel_col), - row_count: layout.row_count(), - col_count: layout.col_count(), - none_cats: layout.none_cats.clone(), - view_back_stack: self.view_back_stack.clone(), - view_forward_stack: self.view_forward_stack.clone(), + view_back_stack: &self.view_back_stack, + view_forward_stack: &self.view_forward_stack, display_value: { let key = layout.cell_key(sel_row, sel_col); if let Some(k) = &key { if let Some((idx, dim)) = crate::view::synthetic_record_info(k) { - // Synthetic records key: check pending drill edits first - self.drill_state.as_ref() + self.drill_state + .as_ref() .and_then(|s| s.pending_edits.get(&(idx, dim)).cloned()) .or_else(|| layout.resolve_display(k)) .unwrap_or_default() } else { - self.model.get_cell(k) + self.model + .get_cell(k) .map(|v| v.to_string()) .unwrap_or_default() } @@ -183,15 +197,17 @@ impl App { String::new() } }, - // Approximate visible rows from terminal size. - // Chrome: title(1) + border(2) + col_headers(n_col_levels) + separator(1) - // + tile_bar(1) + status_bar(1) = ~8 rows of chrome. visible_rows: (self.term_height as usize).saturating_sub(8), visible_cols: { let (fmt_comma, fmt_decimals) = parse_number_format(&view.number_format); - let col_widths = compute_col_widths(&self.model, &layout, fmt_comma, fmt_decimals); - let row_header_width = compute_row_header_width(&layout); - compute_visible_cols(&col_widths, row_header_width, self.term_width, view.col_offset) + let col_widths = compute_col_widths(&self.model, layout, fmt_comma, fmt_decimals); + let row_header_width = compute_row_header_width(layout); + compute_visible_cols( + &col_widths, + row_header_width, + self.term_width, + view.col_offset, + ) }, expanded_cats: &self.expanded_cats, key_code: key, @@ -202,6 +218,7 @@ impl App { for effect in effects { effect.apply(self); } + self.rebuild_layout(); } /// True when the model has no categories yet (show welcome screen) @@ -210,6 +227,8 @@ impl App { } pub fn handle_key(&mut self, key: KeyEvent) -> Result<()> { + self.rebuild_layout(); + // Transient keymap (prefix key sequence) takes priority if let Some(transient) = self.transient_keymap.take() { let effects = { diff --git a/src/ui/effect.rs b/src/ui/effect.rs index bc26390..a4f26f3 100644 --- a/src/ui/effect.rs +++ b/src/ui/effect.rs @@ -398,7 +398,7 @@ pub struct StartDrill(pub Vec<(CellKey, CellValue)>); impl Effect for StartDrill { fn apply(&self, app: &mut App) { app.drill_state = Some(super::app::DrillState { - records: self.0.clone(), + records: std::rc::Rc::new(self.0.clone()), pending_edits: std::collections::HashMap::new(), }); }