fix: page navigation works with multiple page-axis categories

[/] previously broke after the first page category due to a hard-coded
`break`. Replaced with odometer-style navigation: ] advances the last
page category, carrying into the previous when it wraps (like digit
incrementing). [ decrements the same way. Single-category behaviour is
unchanged except it now wraps around instead of clamping at the end.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ed L
2026-03-22 00:02:01 -07:00
parent 8063a484e1
commit 6ba7245338

View File

@ -1052,47 +1052,63 @@ impl App {
} }
} }
fn page_next(&mut self) { /// 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() let page_cats: Vec<String> = self.model.active_view()
.map(|v| v.categories_on(Axis::Page).into_iter().map(String::from).collect()) .map(|v| v.categories_on(Axis::Page).into_iter().map(String::from).collect())
.unwrap_or_default(); .unwrap_or_default();
for cat_name in &page_cats { page_cats.into_iter().filter_map(|cat| {
let items: Vec<String> = self.model.category(cat_name) let items: Vec<String> = self.model.category(&cat)
.map(|c| c.ordered_item_names().into_iter().map(String::from).collect()) .map(|c| c.ordered_item_names().into_iter().map(String::from).collect())
.unwrap_or_default(); .unwrap_or_default();
if items.is_empty() { continue; } if items.is_empty() { return None; }
let current = self.model.active_view() let current = self.model.active_view()
.and_then(|v| v.page_selection(cat_name)) .and_then(|v| v.page_selection(&cat))
.map(String::from) .map(String::from)
.unwrap_or_else(|| items[0].clone()); .or_else(|| items.first().cloned())
let idx = items.iter().position(|i| i == &current).unwrap_or(0); .unwrap_or_default();
let next_idx = (idx + 1).min(items.len() - 1); let idx = items.iter().position(|i| *i == current).unwrap_or(0);
if let Some(view) = self.model.active_view_mut() { Some((cat, items, idx))
view.set_page_selection(cat_name, &items[next_idx]); }).collect()
}
fn page_next(&mut self) {
let data = self.page_cat_data();
if data.is_empty() { return; }
// Odometer: advance from last category, carry propagates backward.
let mut indices: Vec<usize> = data.iter().map(|(_, _, i)| *i).collect();
let mut carry = true;
for i in (0..data.len()).rev() {
if !carry { break; }
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]]);
} }
break;
} }
} }
fn page_prev(&mut self) { fn page_prev(&mut self) {
let page_cats: Vec<String> = self.model.active_view() let data = self.page_cat_data();
.map(|v| v.categories_on(Axis::Page).into_iter().map(String::from).collect()) if data.is_empty() { return; }
.unwrap_or_default(); // Odometer: decrement from last category, borrow propagates backward.
for cat_name in &page_cats { let mut indices: Vec<usize> = data.iter().map(|(_, _, i)| *i).collect();
let items: Vec<String> = self.model.category(cat_name) let mut borrow = true;
.map(|c| c.ordered_item_names().into_iter().map(String::from).collect()) for i in (0..data.len()).rev() {
.unwrap_or_default(); if !borrow { break; }
if items.is_empty() { continue; } if indices[i] == 0 {
let current = self.model.active_view() indices[i] = data[i].1.len().saturating_sub(1);
.and_then(|v| v.page_selection(cat_name)) } else {
.map(String::from) indices[i] -= 1;
.unwrap_or_else(|| items[0].clone()); borrow = false;
let idx = items.iter().position(|i| i == &current).unwrap_or(0); }
let prev_idx = idx.saturating_sub(1); }
if let Some(view) = self.model.active_view_mut() { if let Some(view) = self.model.active_view_mut() {
view.set_page_selection(cat_name, &items[prev_idx]); for (i, (cat, items, _)) in data.iter().enumerate() {
view.set_page_selection(cat, &items[indices[i]]);
} }
break;
} }
} }