#681: Reveal the selected worksheet tab after navigation
This commit is contained in:
parent
b3acae6b77
commit
f7f04df4d7
|
|
@ -40,6 +40,15 @@ pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) {
|
|||
// Keep worksheet tabs in their own horizontally scrollable
|
||||
// viewport, leaving room for the New Sheet button and grid scrollbar.
|
||||
let tabs_width = (ui.available_width() - 180.0).max(100.0);
|
||||
// Reveal the selected sheet after navigation or layout changes, but
|
||||
// allow browsing other tabs without snapping back on every frame.
|
||||
let reveal_key = egui::Id::new((d.uid, d.wb.active_sheet, &sheets, tabs_width.to_bits()));
|
||||
let reveal_id = ui.id().with("worksheet_tabs_reveal");
|
||||
let reveal_active = ui.ctx().data_mut(|data| {
|
||||
let changed = data.get_temp::<egui::Id>(reveal_id) != Some(reveal_key);
|
||||
data.insert_temp(reveal_id, reveal_key);
|
||||
changed
|
||||
});
|
||||
ui.allocate_ui_with_layout(vec2(tabs_width, 30.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
// Translate vertical wheel movement into horizontal
|
||||
// worksheet-tab scrolling.
|
||||
|
|
@ -68,6 +77,9 @@ pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) {
|
|||
let tw = ui.painter().layout_no_wrap(name.clone(), font.clone(), t.text).size().x;
|
||||
let w = (tw + 32.0).max(72.0);
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 30.0), Sense::click_and_drag());
|
||||
if active && reveal_active {
|
||||
resp.scroll_to_me(None);
|
||||
}
|
||||
let r = r.shrink2(vec2(0.0, 1.0));
|
||||
if active {
|
||||
ui.painter().rect_filled(Rect::from_min_max(pos2(r.left(), r.top() - 2.0), r.max), 4.0, t.tab_active);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
//! Overflowing worksheet tabs, exercised through their actual painted geometry and input.
|
||||
use egui::{Color32, Event, Modifiers, PointerButton, Rect, Shape, pos2, vec2};
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_ui_egui::SheetApp;
|
||||
use serde_json::json;
|
||||
|
||||
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
||||
|
||||
fn harness(width: f32, active: usize) -> Harness {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("sheet.rename", json!({"sheet":0,"name":"Tab00"})).unwrap();
|
||||
for i in 1..30 {
|
||||
s.execute("home.insertSheet", json!({"name":format!("Tab{i:02}")})).unwrap();
|
||||
}
|
||||
s.execute("sheet.activate", json!({"sheet":active})).unwrap();
|
||||
let mut fonts_ready = false;
|
||||
let mut h = Harness::builder().with_step_dt(1.0 / 60.0).with_size(vec2(width, 120.0)).build_ui_state(
|
||||
move |ui, app: &mut SheetApp| {
|
||||
if !fonts_ready {
|
||||
ui.ctx().set_fonts(gridcraft_ui_egui::theme::font_definitions());
|
||||
fonts_ready = true;
|
||||
return;
|
||||
}
|
||||
app.grid.cells_rect = Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(ui.max_rect().width(), 80.0)));
|
||||
gridcraft_ui_egui::tabs::sheet_tabs(app, ui);
|
||||
},
|
||||
SheetApp::new(s, Default::default()),
|
||||
);
|
||||
h.run_steps(20);
|
||||
h
|
||||
}
|
||||
|
||||
fn tab(h: &Harness, name: &str) -> Option<(Rect, Rect)> {
|
||||
h.output().shapes.iter().find_map(|s| match &s.shape {
|
||||
Shape::Text(t) if t.galley.job.text == name => Some((t.visual_bounding_rect(), s.clip_rect)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_visible(h: &Harness, name: &str) {
|
||||
let (text, clip) = tab(h, name).unwrap_or_else(|| panic!("{name} was not painted in the tab viewport"));
|
||||
assert!(clip.contains_rect(text), "{name} is clipped: text={text:?}, viewport={clip:?}");
|
||||
}
|
||||
|
||||
fn click(h: &mut Harness, at: egui::Pos2) {
|
||||
h.input_mut().events.extend([
|
||||
Event::PointerMoved(at),
|
||||
Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE },
|
||||
]);
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_and_activating_late_sheets_reveals_their_tabs() {
|
||||
for width in [500.0, 1000.0] {
|
||||
let mut h = harness(width, 29);
|
||||
assert_visible(&h, "Tab29");
|
||||
h.state_mut().run("sheet.activate", json!({"sheet":0})).unwrap();
|
||||
h.run_steps(20);
|
||||
assert_visible(&h, "Tab00");
|
||||
h.state_mut().run("sheet.activate", json!({"sheet":20})).unwrap();
|
||||
h.run_steps(20);
|
||||
assert_visible(&h, "Tab20");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_arrows_reveal_the_selected_tab() {
|
||||
let mut h = harness(700.0, 0);
|
||||
let (_, clip) = tab(&h, "Tab00").unwrap();
|
||||
let next = pos2(clip.left() - 20.0, clip.center().y);
|
||||
for _ in 0..29 {
|
||||
click(&mut h, next);
|
||||
}
|
||||
assert_eq!(h.state().session.doc().unwrap().wb.active_sheet, 29);
|
||||
assert_visible(&h, "Tab29");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_wheel_and_trackpad_browsing_does_not_change_selection_or_snap_back() {
|
||||
for delta in [vec2(0.0, -350.0), vec2(-350.0, 0.0)] {
|
||||
let mut h = harness(700.0, 0);
|
||||
let before = h.state().session.doc().unwrap().wb.clone();
|
||||
let (_, clip) = tab(&h, "Tab00").unwrap();
|
||||
h.input_mut().events.extend([
|
||||
Event::PointerMoved(clip.center()),
|
||||
Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
phase: egui::TouchPhase::Start,
|
||||
delta: vec2(0.0, 0.0),
|
||||
modifiers: Modifiers::NONE,
|
||||
},
|
||||
]);
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
phase: egui::TouchPhase::Move,
|
||||
delta,
|
||||
modifiers: Modifiers::NONE,
|
||||
});
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
phase: egui::TouchPhase::End,
|
||||
delta: vec2(0.0, 0.0),
|
||||
modifiers: Modifiers::NONE,
|
||||
});
|
||||
h.run_steps(40);
|
||||
assert!(!tab(&h, "Tab00").is_some_and(|(r, c)| c.contains_rect(r)), "gesture should browse away from the selected tab: {delta:?}");
|
||||
assert_eq!(h.state().session.doc().unwrap().wb, before, "browsing tabs must not edit the workbook");
|
||||
h.run_steps(40);
|
||||
assert!(!tab(&h, "Tab00").is_some_and(|(r, c)| c.contains_rect(r)), "idle frames must not snap back");
|
||||
h.state_mut().run("sheet.activate", json!({"sheet":29})).unwrap();
|
||||
h.run_steps(20);
|
||||
assert_visible(&h, "Tab29");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn hidden_sheets_long_names_and_window_resize_keep_active_tab_visible() {
|
||||
let mut h = harness(1000.0, 0);
|
||||
for sheet in 1..10 {
|
||||
h.state_mut().run("sheet.hide", json!({"sheet":sheet})).unwrap();
|
||||
}
|
||||
let name = "Quarterly results and forecasts";
|
||||
h.state_mut().run("sheet.rename", json!({"sheet":29,"name":name})).unwrap();
|
||||
h.state_mut().run("sheet.activate", json!({"sheet":29})).unwrap();
|
||||
h.run_steps(20);
|
||||
assert_visible(&h, name);
|
||||
h.set_size(vec2(500.0, 120.0));
|
||||
h.run_steps(20);
|
||||
assert_visible(&h, name);
|
||||
h.set_size(vec2(1000.0, 120.0));
|
||||
h.run_steps(20);
|
||||
assert_visible(&h, name);
|
||||
assert_eq!(h.state().session.doc().unwrap().wb.active_sheet, 29);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_sheet_button_stays_reachable_and_reveals_the_inserted_tab() {
|
||||
let mut h = harness(700.0, 29);
|
||||
let (_, clip) = tab(&h, "Tab29").unwrap();
|
||||
click(&mut h, pos2(clip.right() + 18.0, clip.center().y));
|
||||
let d = h.state().session.doc().unwrap();
|
||||
assert_eq!(d.wb.sheets.len(), 31);
|
||||
let name = d.wb.active().unwrap().name.clone();
|
||||
assert_visible(&h, &name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_leaves_the_worksheet_scrollbar_reachable() {
|
||||
for width in [500.0, 700.0, 1200.0] {
|
||||
let mut h = harness(width, 0);
|
||||
let (bar, clip) = h
|
||||
.output()
|
||||
.shapes
|
||||
.iter()
|
||||
.find_map(|s| match &s.shape {
|
||||
Shape::Rect(r) if r.fill == Color32::from_gray(190) && r.rect.height() == 8.0 => Some((r.rect, s.clip_rect)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("worksheet scrollbar thumb must be painted");
|
||||
assert!(clip.contains_rect(bar), "worksheet scrollbar is offscreen: {bar:?}, {clip:?}");
|
||||
let at = bar.center();
|
||||
h.input_mut().events.extend([
|
||||
Event::PointerMoved(at),
|
||||
Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE },
|
||||
]);
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerMoved(at + vec2(12.0, 0.0)));
|
||||
h.step();
|
||||
assert!(h.state().view().scroll.x > 0.0, "worksheet scrollbar must respond to dragging");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Target-app parity: GridCraft vs Microsoft Excel
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (stability: command-sequence fuzzing against undo/redo, XLSX round-trip and full-recalc invariants) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (sheet-tab navigation reveals the selected tab without interrupting manual scrolling) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
|
||||
This is the authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it,
|
||||
[`gaps.md`](gaps.md) lists every shortfall, and the area checklists hold the detail:
|
||||
|
|
@ -332,6 +332,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Sheet tabs: reveal the selected tab on navigation and resize while preserving manual scrolling (#681); no score change |
|
||||
| 2026-10-11 | minor | Stability: command-sequence fuzz harness (`crates/engine/tests/command_sequence_fuzz.rs`) checks random command sequences against undo/redo, XLSX round-trip, full-recalc and structure invariants; the sequence bugs it found are fixed (no score change) |
|
||||
| 2026-10-11 | minor | Password-encrypted workbooks open (#11): Essentials file-opening work 24–36 h |
|
||||
| 2026-10-11 | minor | Worksheet functions 505 / 523 (96.6%; 496 working): ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# UI parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (AutoFit reserves filter dropdown space, observed against Windows Excel) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (sheet-tab navigation reveals the selected tab without interrupting manual scrolling) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
|
||||
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
|
||||
|
|
@ -21,7 +21,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
|
|||
| Ribbon collapse, simplified ribbon, customize ribbon/QAT | yes | partial | customization missing |
|
||||
| Key tips (Alt / F10 then letters) | yes, every ribbon control | **initial** (#43, 10-10): Alt or F10 shows key tips on Home for alignment, Paste Special and AutoFit plus legacy Alt sequences; other tabs and controls not yet (#33, #49) | 3–5 h |
|
||||
| Formula bar with Name Box, expand, function hints | yes | yes | done |
|
||||
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | rename focus bug on macOS (#145) |
|
||||
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | active tab revealed on navigation and resize (#681); manual wheel/trackpad browsing preserved; rename focus bug on macOS (#145) |
|
||||
| Status bar: Sum/Count/Average and more, view buttons, zoom | yes | yes | customizable statistics partial |
|
||||
| Task panes (Format Chart, Comments, Watch, Selection, PivotTable fields) | yes | yes | element-level Format pane for shapes and pictures missing |
|
||||
| Backstage (File) view: Info, New with templates, Open recent, Export, Options | yes | partial | Options dialog and template gallery missing |
|
||||
|
|
|
|||
Loading…
Reference in New Issue