From 6ba72453385ec54ee0a826bed7caa9d279cee40a Mon Sep 17 00:00:00 2001 From: Ed L Date: Sun, 22 Mar 2026 00:02:01 -0700 Subject: [PATCH] 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 --- src/ui/app.rs | 72 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 1b73bea..63f48a3 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -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, usize)> { let page_cats: Vec = self.model.active_view() .map(|v| v.categories_on(Axis::Page).into_iter().map(String::from).collect()) .unwrap_or_default(); - for cat_name in &page_cats { - let items: Vec = self.model.category(cat_name) + page_cats.into_iter().filter_map(|cat| { + let items: Vec = self.model.category(&cat) .map(|c| c.ordered_item_names().into_iter().map(String::from).collect()) .unwrap_or_default(); - if items.is_empty() { continue; } + if items.is_empty() { return None; } let current = self.model.active_view() - .and_then(|v| v.page_selection(cat_name)) + .and_then(|v| v.page_selection(&cat)) .map(String::from) - .unwrap_or_else(|| items[0].clone()); - let idx = items.iter().position(|i| i == ¤t).unwrap_or(0); - let next_idx = (idx + 1).min(items.len() - 1); - if let Some(view) = self.model.active_view_mut() { - view.set_page_selection(cat_name, &items[next_idx]); + .or_else(|| items.first().cloned()) + .unwrap_or_default(); + let idx = items.iter().position(|i| *i == current).unwrap_or(0); + Some((cat, items, 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 = 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) { - let page_cats: Vec = self.model.active_view() - .map(|v| v.categories_on(Axis::Page).into_iter().map(String::from).collect()) - .unwrap_or_default(); - for cat_name in &page_cats { - let items: Vec = self.model.category(cat_name) - .map(|c| c.ordered_item_names().into_iter().map(String::from).collect()) - .unwrap_or_default(); - if items.is_empty() { continue; } - let current = self.model.active_view() - .and_then(|v| v.page_selection(cat_name)) - .map(String::from) - .unwrap_or_else(|| items[0].clone()); - let idx = items.iter().position(|i| i == ¤t).unwrap_or(0); - let prev_idx = idx.saturating_sub(1); - if let Some(view) = self.model.active_view_mut() { - view.set_page_selection(cat_name, &items[prev_idx]); + let data = self.page_cat_data(); + if data.is_empty() { return; } + // Odometer: decrement from last category, borrow propagates backward. + let mut indices: Vec = data.iter().map(|(_, _, i)| *i).collect(); + let mut borrow = true; + for i in (0..data.len()).rev() { + if !borrow { break; } + if indices[i] == 0 { + indices[i] = data[i].1.len().saturating_sub(1); + } else { + indices[i] -= 1; + 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]]); } - break; } }