diff --git a/crates/ui-egui/src/tabs.rs b/crates/ui-egui/src/tabs.rs index ea11615d0..917dc3b7f 100644 --- a/crates/ui-egui/src/tabs.rs +++ b/crates/ui-egui/src/tabs.rs @@ -22,6 +22,12 @@ fn navigator_items(sheets: &[Arc]) -> Vec<(String, String)> { sheets.iter().enumerate().filter(|(_, s)| s.visibility == Visibility::Visible).map(|(i, s)| (i.to_string(), s.name.clone())).collect() } +/// Workbook index of the first (or, with `last`, the last) visible sheet: where Ctrl-clicking a tab arrow lands. +fn edge_sheet(sheets: &[Arc], last: bool) -> Option { + let mut visible = sheets.iter().enumerate().filter(|(_, s)| s.visibility == Visibility::Visible).map(|(i, _)| i); + if last { visible.next_back() } else { visible.next() } +} + /// Switch to sheet `i` from the tab strip or the navigator. In point mode the formula being typed keeps going on the /// other sheet; otherwise the edit is committed first. fn activate_sheet(app: &mut SheetApp, i: usize) { @@ -117,10 +123,22 @@ pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) { ui.add_space(8.0); if view.show_sheet_tabs { if crate::widgets::icon_button(ui, Icon::Left, t.text_dim, app.ui.language.tr("Previous sheet"), vec2(24.0, 24.0)).clicked() { - app.run_or_alert("sheet.previous", json!({})); + // Ctrl-click jumps to the first visible sheet, like Excel's tab scroll buttons. + let edge = + if ui.input(|i| i.modifiers.command) { app.session.active().and_then(|d| edge_sheet(&d.wb.sheets, false)) } else { None }; + match edge { + Some(i) => activate_sheet(app, i), + None => app.run_or_alert("sheet.previous", json!({})), + } } if crate::widgets::icon_button(ui, Icon::Right, t.text_dim, app.ui.language.tr("Next sheet"), vec2(24.0, 24.0)).clicked() { - app.run_or_alert("sheet.next", json!({})); + // Ctrl-click jumps to the last visible sheet. + let edge = + if ui.input(|i| i.modifiers.command) { app.session.active().and_then(|d| edge_sheet(&d.wb.sheets, true)) } else { None }; + match edge { + Some(i) => activate_sheet(app, i), + None => app.run_or_alert("sheet.next", json!({})), + } } let all = crate::widgets::icon_button(ui, Icon::SheetList, t.text_dim, app.ui.language.tr("All sheets"), vec2(24.0, 24.0)); all.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, app.ui.language.tr("All sheets"))); @@ -522,7 +540,8 @@ fn status_bar_menu(app: &mut SheetApp, ui: &mut Ui) { #[cfg(test)] mod tests { use super::{ - MIN_TRACK, NEW_SHEET_WIDTH, SPLIT_WIDTH, activate_sheet, dragged_share, drop_copies_sheet, navigator_items, rename_needs_focus, tab_drop, + MIN_TRACK, NEW_SHEET_WIDTH, SPLIT_WIDTH, activate_sheet, dragged_share, drop_copies_sheet, edge_sheet, navigator_items, rename_needs_focus, + tab_drop, }; use crate::SheetApp; use egui::Modifiers; @@ -614,6 +633,16 @@ mod tests { assert_eq!(navigator_items(&sheets), vec![("0".to_string(), "Data".to_string()), ("3".to_string(), "Summary".to_string())]); } + #[test] + fn ctrl_click_on_the_tab_arrows_lands_on_the_first_and_last_visible_sheet() { + let mut hidden = Sheet::new("Hidden"); + hidden.visibility = Visibility::Hidden; + let sheets = [hidden.clone(), Sheet::new("A"), Sheet::new("B"), Sheet::new("C"), hidden].map(Arc::new); + assert_eq!(edge_sheet(&sheets, false), Some(1)); + assert_eq!(edge_sheet(&sheets, true), Some(3)); + assert_eq!(edge_sheet(&[], true), None); + } + #[test] fn a_rejected_commit_keeps_the_sheet_and_the_edit() { let mut session = gridcraft_engine::Session::new();