diff --git a/crates/ui-egui/src/tabs.rs b/crates/ui-egui/src/tabs.rs index 917dc3b7f..462dc2e4c 100644 --- a/crates/ui-egui/src/tabs.rs +++ b/crates/ui-egui/src/tabs.rs @@ -61,6 +61,18 @@ fn tab_drop(i: usize, steps: i64, copy: bool) -> Option { Some(json!({"sheet": i, "to": to})) } +/// Where a request to scroll the active tab into view is kept until the tabs are drawn. +fn reveal_request_id(scope: egui::Id) -> egui::Id { + scope.with("worksheet_tabs_reveal_request") +} + +/// Asks the tab strip to bring the active tab into view in this frame, even when the active sheet +/// did not change (a Ctrl-click on an arrow whose edge sheet is already active). +fn request_reveal(ui: &Ui) { + let id = reveal_request_id(ui.scope_id()); + ui.ctx().data_mut(|data| data.insert_temp(id, true)); +} + /// The New sheet (+) button and the gap before it. const NEW_SHEET_WIDTH: f32 = 32.0; /// The handle between the tabs and the horizontal scrollbar. @@ -127,7 +139,10 @@ pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) { 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), + Some(i) => { + activate_sheet(app, i); + request_reveal(ui); + } None => app.run_or_alert("sheet.previous", json!({})), } } @@ -136,7 +151,10 @@ pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) { 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), + Some(i) => { + activate_sheet(app, i); + request_reveal(ui); + } None => app.run_or_alert("sheet.next", json!({})), } } @@ -181,10 +199,13 @@ pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) { // allow browsing other tabs without snapping back on every frame. let reveal_key = egui::Id::unique((d.uid, d.wb.active_sheet(), &sheets, tabs_width.to_bits())); let reveal_id = ui.scope_id().with("worksheet_tabs_reveal"); + let request_id = reveal_request_id(ui.scope_id()); let reveal_active = ui.ctx().data_mut(|data| { let changed = data.get_temp::(reveal_id) != Some(reveal_key); data.insert_temp(reveal_id, reveal_key); - changed + // A Ctrl-click on an arrow whose edge sheet is already active changes nothing above, + // yet asks to bring that tab into view. + changed | data.remove_temp::(request_id).unwrap_or(false) }); if view.show_sheet_tabs { ui.allocate_ui_with_layout(vec2(tabs_width, 30.0), egui::Layout::left_to_right(egui::Align::Center), |ui| { diff --git a/crates/ui-egui/tests/it/sheet_tabs.rs b/crates/ui-egui/tests/it/sheet_tabs.rs index 460162e67..725e761c6 100644 --- a/crates/ui-egui/tests/it/sheet_tabs.rs +++ b/crates/ui-egui/tests/it/sheet_tabs.rs @@ -58,6 +58,20 @@ fn click(h: &mut Harness, at: egui::Pos2) { h.run_steps(20); } +fn ctrl_click(h: &mut Harness, at: egui::Pos2) { + let m = Modifiers::COMMAND; + h.input_mut().events.extend([ + Event::ModifiersChanged(m), + Event::PointerMoved(at), + Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: m }, + ]); + h.step(); + h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: m }); + h.run_steps(20); + h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE)); + h.run_steps(2); +} + #[test] fn opening_and_activating_late_sheets_reveals_their_tabs() { for width in [500.0, 1000.0] { @@ -85,6 +99,36 @@ fn navigation_arrows_reveal_the_selected_tab() { assert_visible(&h, "Tab29"); } +/// A Ctrl/Cmd-click on the Next arrow jumps to the last sheet; repeated after the strip was scrolled +/// away from it, the already active edge tab is brought back into view. +#[test] +fn repeated_ctrl_click_on_the_arrow_reveals_the_active_edge_tab() { + let mut h = harness(700.0, 0); + let (_, clip) = tab(&h, "Tab00").unwrap(); + let next = pos2(clip.left() - 44.0, clip.center().y); + ctrl_click(&mut h, next); + assert_eq!(h.state().session.doc().unwrap().wb.active_sheet(), 29); + assert_visible(&h, "Tab29"); + // Browse the strip back to the start without changing the selection. + h.input_mut().events.extend([Event::PointerMoved(clip.center())]); + for _ in 0..3 { + h.input_mut().events.push(Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + source: egui::MouseWheelSource::Unknown, + phase: egui::TouchPhase::Move, + delta: vec2(0.0, 600.0), + modifiers: Modifiers::NONE, + }); + h.run_steps(10); + } + h.run_steps(40); + assert!(!tab(&h, "Tab29").is_some_and(|(r, c)| c.contains_rect(r)), "the strip was scrolled away from the active tab"); + assert_eq!(h.state().session.doc().unwrap().wb.active_sheet(), 29); + ctrl_click(&mut h, next); + assert_eq!(h.state().session.doc().unwrap().wb.active_sheet(), 29); + assert_visible(&h, "Tab29"); +} + #[test] /// Horizontal wheel/trackpad browsing leaves the workbook unchanged, does not snap back on idle frames, and activating a sheet still reveals its tab. fn manual_wheel_and_trackpad_browsing_does_not_change_selection_or_snap_back() {