refactor: make active_view and axis_of infallible
Both functions previously returned Option despite their invariants guaranteeing a value: active_view always names an existing view (maintained by new/switch_view/delete_view), and axis_of only returns None for categories never registered with the view (a programming error). Callers no longer need to handle the impossible None case, eliminating ~15 match/if-let Option guards across app.rs, dispatch.rs, grid.rs, tile_bar.rs, and category_panel.rs. Also adds Model::evaluate_f64 (returns 0.0 for empty cells) and collapses the double match-on-axis pattern in tile_bar/category_panel into a single axis_display(Axis) helper. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
114
src/ui/app.rs
114
src/ui/app.rs
@ -209,10 +209,9 @@ impl App {
|
||||
|
||||
// 0 = first col, $ = last col
|
||||
(KeyCode::Char('0'), KeyModifiers::NONE) => {
|
||||
if let Some(view) = self.model.active_view_mut() {
|
||||
view.selected.1 = 0;
|
||||
view.col_offset = 0;
|
||||
}
|
||||
let view = self.model.active_view_mut();
|
||||
view.selected.1 = 0;
|
||||
view.col_offset = 0;
|
||||
}
|
||||
(KeyCode::Char('$'), _) => { self.jump_to_last_col(); }
|
||||
|
||||
@ -304,7 +303,8 @@ impl App {
|
||||
match (first, key.code) {
|
||||
// gg = first row
|
||||
('g', KeyCode::Char('g')) => {
|
||||
if let Some(view) = self.model.active_view_mut() {
|
||||
{
|
||||
let view = self.model.active_view_mut();
|
||||
view.selected = (0, view.selected.1);
|
||||
view.row_offset = 0;
|
||||
}
|
||||
@ -510,9 +510,7 @@ impl App {
|
||||
if rest.is_empty() {
|
||||
self.status_msg = "Usage: :set-format <fmt> e.g. ,.0 ,.2 .4".to_string();
|
||||
} else {
|
||||
if let Some(view) = self.model.active_view_mut() {
|
||||
view.number_format = rest.to_string();
|
||||
}
|
||||
self.model.active_view_mut().number_format = rest.to_string();
|
||||
self.status_msg = format!("Number format set to '{rest}'");
|
||||
self.dirty = true;
|
||||
}
|
||||
@ -636,9 +634,7 @@ impl App {
|
||||
}
|
||||
KeyCode::Enter | KeyCode::Char(' ') => {
|
||||
if let Some(cat_name) = cat_names.get(self.cat_panel_cursor) {
|
||||
if let Some(view) = self.model.active_view_mut() {
|
||||
view.cycle_axis(cat_name);
|
||||
}
|
||||
self.model.active_view_mut().cycle_axis(cat_name);
|
||||
}
|
||||
}
|
||||
// n — add a new category
|
||||
@ -805,7 +801,7 @@ impl App {
|
||||
}
|
||||
KeyCode::Enter | KeyCode::Char(' ') => {
|
||||
if let Some(name) = cat_names.get(cat_idx) {
|
||||
if let Some(view) = self.model.active_view_mut() { view.cycle_axis(name); }
|
||||
self.model.active_view_mut().cycle_axis(name);
|
||||
self.dirty = true;
|
||||
}
|
||||
self.mode = AppMode::Normal;
|
||||
@ -917,57 +913,42 @@ impl App {
|
||||
|
||||
fn move_selection(&mut self, dr: i32, dc: i32) {
|
||||
let (row_max, col_max) = {
|
||||
let view = match self.model.active_view() { Some(v) => v, None => return };
|
||||
let layout = GridLayout::new(&self.model, view);
|
||||
let layout = GridLayout::new(&self.model, self.model.active_view());
|
||||
(layout.row_count().saturating_sub(1), layout.col_count().saturating_sub(1))
|
||||
};
|
||||
|
||||
if let Some(view) = self.model.active_view_mut() {
|
||||
let (r, c) = view.selected;
|
||||
let nr = (r as i32 + dr).clamp(0, row_max as i32) as usize;
|
||||
let nc = (c as i32 + dc).clamp(0, col_max as i32) as usize;
|
||||
view.selected = (nr, nc);
|
||||
// Keep cursor in visible area (approximate viewport: 20 rows, 8 cols)
|
||||
if nr < view.row_offset { view.row_offset = nr; }
|
||||
if nr >= view.row_offset + 20 { view.row_offset = nr.saturating_sub(19); }
|
||||
if nc < view.col_offset { view.col_offset = nc; }
|
||||
if nc >= view.col_offset + 8 { view.col_offset = nc.saturating_sub(7); }
|
||||
}
|
||||
let view = self.model.active_view_mut();
|
||||
let (r, c) = view.selected;
|
||||
let nr = (r as i32 + dr).clamp(0, row_max as i32) as usize;
|
||||
let nc = (c as i32 + dc).clamp(0, col_max as i32) as usize;
|
||||
view.selected = (nr, nc);
|
||||
// Keep cursor in visible area (approximate viewport: 20 rows, 8 cols)
|
||||
if nr < view.row_offset { view.row_offset = nr; }
|
||||
if nr >= view.row_offset + 20 { view.row_offset = nr.saturating_sub(19); }
|
||||
if nc < view.col_offset { view.col_offset = nc; }
|
||||
if nc >= view.col_offset + 8 { view.col_offset = nc.saturating_sub(7); }
|
||||
}
|
||||
|
||||
fn jump_to_last_row(&mut self) {
|
||||
let count = {
|
||||
let view = match self.model.active_view() { Some(v) => v, None => return };
|
||||
GridLayout::new(&self.model, view).row_count().saturating_sub(1)
|
||||
};
|
||||
if let Some(view) = self.model.active_view_mut() {
|
||||
view.selected.0 = count;
|
||||
if count >= view.row_offset + 20 { view.row_offset = count.saturating_sub(19); }
|
||||
}
|
||||
let count = GridLayout::new(&self.model, self.model.active_view()).row_count().saturating_sub(1);
|
||||
let view = self.model.active_view_mut();
|
||||
view.selected.0 = count;
|
||||
if count >= view.row_offset + 20 { view.row_offset = count.saturating_sub(19); }
|
||||
}
|
||||
|
||||
fn jump_to_last_col(&mut self) {
|
||||
let count = {
|
||||
let view = match self.model.active_view() { Some(v) => v, None => return };
|
||||
GridLayout::new(&self.model, view).col_count().saturating_sub(1)
|
||||
};
|
||||
if let Some(view) = self.model.active_view_mut() {
|
||||
view.selected.1 = count;
|
||||
if count >= view.col_offset + 8 { view.col_offset = count.saturating_sub(7); }
|
||||
}
|
||||
let count = GridLayout::new(&self.model, self.model.active_view()).col_count().saturating_sub(1);
|
||||
let view = self.model.active_view_mut();
|
||||
view.selected.1 = count;
|
||||
if count >= view.col_offset + 8 { view.col_offset = count.saturating_sub(7); }
|
||||
}
|
||||
|
||||
fn scroll_rows(&mut self, delta: i32) {
|
||||
let row_max = {
|
||||
let view = match self.model.active_view() { Some(v) => v, None => return };
|
||||
GridLayout::new(&self.model, view).row_count().saturating_sub(1)
|
||||
};
|
||||
if let Some(view) = self.model.active_view_mut() {
|
||||
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); }
|
||||
}
|
||||
let row_max = GridLayout::new(&self.model, self.model.active_view()).row_count().saturating_sub(1);
|
||||
let view = self.model.active_view_mut();
|
||||
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); }
|
||||
}
|
||||
|
||||
/// Navigate to the next (`forward=true`) or previous (`forward=false`) cell
|
||||
@ -976,10 +957,7 @@ impl App {
|
||||
let query = self.search_query.to_lowercase();
|
||||
if query.is_empty() { return; }
|
||||
|
||||
let view = match self.model.active_view() {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
let view = self.model.active_view();
|
||||
let layout = GridLayout::new(&self.model, view);
|
||||
let (cur_row, cur_col) = view.selected;
|
||||
|
||||
@ -1022,7 +1000,8 @@ impl App {
|
||||
if let Some(flat) = target_flat {
|
||||
let ri = flat / total_cols;
|
||||
let ci = flat % total_cols;
|
||||
if let Some(view) = self.model.active_view_mut() {
|
||||
{
|
||||
let view = self.model.active_view_mut();
|
||||
view.selected = (ri, ci);
|
||||
// Adjust scroll offsets to keep cursor visible
|
||||
if ri < view.row_offset { view.row_offset = ri; }
|
||||
@ -1038,15 +1017,14 @@ impl App {
|
||||
/// Gather (cat_name, items, current_idx) for all non-empty page categories.
|
||||
fn page_cat_data(&self) -> Vec<(String, Vec<String>, usize)> {
|
||||
let page_cats: Vec<String> = self.model.active_view()
|
||||
.map(|v| v.categories_on(Axis::Page).into_iter().map(String::from).collect())
|
||||
.unwrap_or_default();
|
||||
.categories_on(Axis::Page).into_iter().map(String::from).collect();
|
||||
page_cats.into_iter().filter_map(|cat| {
|
||||
let items: Vec<String> = self.model.category(&cat)
|
||||
.map(|c| c.ordered_item_names().into_iter().map(String::from).collect())
|
||||
.unwrap_or_default();
|
||||
if items.is_empty() { return None; }
|
||||
let current = self.model.active_view()
|
||||
.and_then(|v| v.page_selection(&cat))
|
||||
.page_selection(&cat)
|
||||
.map(String::from)
|
||||
.or_else(|| items.first().cloned())
|
||||
.unwrap_or_default();
|
||||
@ -1066,10 +1044,9 @@ impl App {
|
||||
indices[i] += 1;
|
||||
if indices[i] >= data[i].1.len() { indices[i] = 0; } else { carry = false; }
|
||||
}
|
||||
if let Some(view) = self.model.active_view_mut() {
|
||||
for (i, (cat, items, _)) in data.iter().enumerate() {
|
||||
view.set_page_selection(cat, &items[indices[i]]);
|
||||
}
|
||||
let view = self.model.active_view_mut();
|
||||
for (i, (cat, items, _)) in data.iter().enumerate() {
|
||||
view.set_page_selection(cat, &items[indices[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1088,17 +1065,16 @@ impl App {
|
||||
borrow = false;
|
||||
}
|
||||
}
|
||||
if let Some(view) = self.model.active_view_mut() {
|
||||
for (i, (cat, items, _)) in data.iter().enumerate() {
|
||||
view.set_page_selection(cat, &items[indices[i]]);
|
||||
}
|
||||
let view = self.model.active_view_mut();
|
||||
for (i, (cat, items, _)) in data.iter().enumerate() {
|
||||
view.set_page_selection(cat, &items[indices[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cell key resolution ──────────────────────────────────────────────────
|
||||
|
||||
pub fn selected_cell_key(&self) -> Option<CellKey> {
|
||||
let view = self.model.active_view()?;
|
||||
let view = self.model.active_view();
|
||||
let (sel_row, sel_col) = view.selected;
|
||||
GridLayout::new(&self.model, view).cell_key(sel_row, sel_col)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user