diff --git a/src/ui/app.rs b/src/ui/app.rs index 94186a2..1b73bea 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -908,62 +908,50 @@ impl App { // ── Motion helpers ─────────────────────────────────────────────────────── fn move_selection(&mut self, dr: i32, dc: i32) { - // Compute max row/col from actual item counts so we never go out of bounds. - let row_max = { - let row_cats: Vec = self.model.active_view() - .map(|v| v.categories_on(crate::view::Axis::Row).into_iter().map(String::from).collect()) - .unwrap_or_default(); - row_cats.first() - .and_then(|c| self.model.category(c)) - .map(|c| c.items.len().saturating_sub(1)) - .unwrap_or(0) - }; - let col_max = { - let col_cats: Vec = self.model.active_view() - .map(|v| v.categories_on(crate::view::Axis::Column).into_iter().map(String::from).collect()) - .unwrap_or_default(); - col_cats.first() - .and_then(|c| self.model.category(c)) - .map(|c| c.items.len().saturating_sub(1)) - .unwrap_or(0) + // Use cross-product counts so multi-category axes navigate correctly. + let (row_max, col_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(); + let col_cats: Vec = view.categories_on(Axis::Column).into_iter().map(String::from).collect(); + let rm = cross_product_strs(&row_cats, &self.model, view).len().saturating_sub(1); + let cm = cross_product_strs(&col_cats, &self.model, view).len().saturating_sub(1); + (rm, cm) }; if let Some(view) = self.model.active_view_mut() { let (r, c) = view.selected; - view.selected = ( - (r as i32 + dr).clamp(0, row_max as i32) as usize, - (c as i32 + dc).clamp(0, col_max as i32) as usize, - ); + 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 _view_name = self.model.active_view.clone(); - if let Some(view) = self.model.active_view() { + let count = { + 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(); - let count = row_cats.first() - .and_then(|c| self.model.category(c)) - .map(|c| c.items.len().saturating_sub(1)) - .unwrap_or(0); - drop(view); - if let Some(view) = self.model.active_view_mut() { - view.selected.0 = count; - } + cross_product_strs(&row_cats, &self.model, view).len().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); } } } fn jump_to_last_col(&mut self) { - let _view_name = self.model.active_view.clone(); - if let Some(view) = self.model.active_view() { + let count = { + let view = match self.model.active_view() { Some(v) => v, None => return }; let col_cats: Vec = view.categories_on(Axis::Column).into_iter().map(String::from).collect(); - let count = col_cats.first() - .and_then(|c| self.model.category(c)) - .map(|c| c.items.len().saturating_sub(1)) - .unwrap_or(0); - drop(view); - if let Some(view) = self.model.active_view_mut() { - view.selected.1 = count; - } + cross_product_strs(&col_cats, &self.model, view).len().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); } } } @@ -1112,9 +1100,9 @@ impl App { pub fn selected_cell_key(&self) -> Option { let view = self.model.active_view()?; - let row_cats: Vec<&str> = view.categories_on(Axis::Row); - let col_cats: Vec<&str> = view.categories_on(Axis::Column); - let page_cats: Vec<&str> = view.categories_on(Axis::Page); + let row_cats: Vec = view.categories_on(Axis::Row).into_iter().map(String::from).collect(); + let col_cats: Vec = view.categories_on(Axis::Column).into_iter().map(String::from).collect(); + let page_cats: Vec = view.categories_on(Axis::Page).into_iter().map(String::from).collect(); let (sel_row, sel_col) = view.selected; let mut coords = vec![]; @@ -1125,25 +1113,20 @@ impl App { let sel = view.page_selection(cat_name) .map(String::from) .or_else(|| items.first().cloned())?; - coords.push((cat_name.to_string(), sel)); + coords.push((cat_name.clone(), sel)); } - for cat_name in &row_cats { - let items: Vec = self.model.category(cat_name) - .map(|c| c.ordered_item_names().into_iter() - .filter(|item| !view.is_hidden(cat_name, item)) - .map(String::from).collect()) - .unwrap_or_default(); - let item = items.get(sel_row)?.clone(); - coords.push((cat_name.to_string(), item)); + + // Use cross-product indexing so multi-category axes resolve correctly. + let row_items = cross_product_strs(&row_cats, &self.model, view); + let row_item = row_items.get(sel_row)?; + for (cat, item) in row_cats.iter().zip(row_item.iter()) { + coords.push((cat.clone(), item.clone())); } - for cat_name in &col_cats { - let items: Vec = self.model.category(cat_name) - .map(|c| c.ordered_item_names().into_iter() - .filter(|item| !view.is_hidden(cat_name, item)) - .map(String::from).collect()) - .unwrap_or_default(); - let item = items.get(sel_col)?.clone(); - coords.push((cat_name.to_string(), item)); + + let col_items = cross_product_strs(&col_cats, &self.model, view); + let col_item = col_items.get(sel_col)?; + for (cat, item) in col_cats.iter().zip(col_item.iter()) { + coords.push((cat.clone(), item.clone())); } Some(CellKey::new(coords))