Formulas: trace arrows stay when the sheet is zoomed (#3097)

A zoom changes the workbook's revision, which the trace arrows are tied
to, so every zoom removed the arrows of Trace Precedents and Trace
Dependents. Zooming moves no cell: Trace::keep_through_zoom carries
arrows that were current over a zoom from the View tab, the Zoom dialog,
the status bar's buttons and slider, Ctrl+wheel and the control channel.
Undoing a zoom is still an edit and removes them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
This commit is contained in:
Ingmar Lübbe
2026-11-02 01:08:19 +01:00
co-authored by Claude Opus 5.5
parent f5030432fc
commit bf79931b30
6 changed files with 73 additions and 3 deletions
+4
View File
@@ -47,7 +47,11 @@ impl SheetApp {
return r;
}
let before = self.session.active_window();
let zoom_from = matches!(id, "view.zoom" | "view.zoom100" | "view.zoomToSelection").then(|| crate::grid::Trace::key(self));
let r = self.session.run(id, params);
if let Some(from) = zoom_from {
crate::grid::Trace::keep_through_zoom(self, from);
}
if id.starts_with("file.") && self.session.active_window() != before {
// File › New or Open from the Backstage view: the window it was in goes back to its sheet.
self.backstage = None;
+19 -1
View File
@@ -154,7 +154,7 @@ pub struct FillTag {
}
/// Trace Precedents / Trace Dependents arrows: they stay on the cell they were traced from, on its
/// sheet, while the selection moves. They go away when anything changes the workbook
/// sheet, while the selection moves. They go away when anything but a zoom changes the workbook
/// (`revision`): Excel removes them on edits to the traced formula and on inserted, deleted or
/// moved cells, this also on a new value in a precedent.
pub struct Trace {
@@ -171,6 +171,24 @@ impl Trace {
pub fn new(app: &SheetApp, items: serde_json::Value) -> Option<Trace> {
app.session.active().map(|d| Trace { uid: d.uid, revision: d.revision, sheet: d.wb.active_sheet(), cell: d.selection.active, items })
}
/// The active document's uid and revision, to hand [`Trace::keep_through_zoom`] after a zoom.
pub fn key(app: &SheetApp) -> Option<(u64, u64)> {
app.session.active().map(|d| (d.uid, d.revision))
}
/// Zooming changes the workbook (its revision) but moves no cell: arrows that were shown
/// before a zoom stay after it, as in Excel (#3097). `before` is [`Trace::key`] from then.
pub fn keep_through_zoom(app: &mut SheetApp, before: Option<(u64, u64)>) {
let now = Trace::key(app);
if let Some(t) = app.grid.trace.as_mut()
&& before == Some((t.uid, t.revision))
&& let Some((uid, revision)) = now
&& uid == t.uid
{
t.revision = revision;
}
}
}
#[derive(Default)]
+23 -1
View File
@@ -1,4 +1,3 @@
#[cfg(test)]
use super::Trace;
use super::cursor::update_cursor;
use super::editor::{in_cell_editor, list_picker, validation_arrow};
@@ -358,7 +357,9 @@ fn update_zoom_glide(app: &mut SheetApp, ui: &egui::Ui, place: (u64, usize), zoo
let glides = app.ui.animations && ui.ctx().global_style().animation_time > 0.0 && !app.shots.in_flight();
let share = egui::emath::exponential_smooth_factor(0.9, 0.1, ui.input(|i| i.stable_dt).min(0.1));
let z = if glides { zoom_glide_step(shown, to, share) } else { to };
let from = Trace::key(app);
let _ = app.session.run("view.zoom", json!({"percent": z}));
Trace::keep_through_zoom(app, from);
zooming.glide = (z != to).then_some((to, z));
ui.ctx().request_repaint();
}
@@ -945,4 +946,25 @@ mod trace_arrow_tests {
assert_eq!(revisions[0], revisions[1]);
assert!(arrows(&mut app).1.is_empty(), "Book1's arrows are not drawn on Book2");
}
#[test]
fn precedent_arrows_stay_through_a_zoom() {
let mut app = app();
app.run("home.insertSheet", json!({"name": "Two"})).unwrap();
app.run("sheet.activate", json!({"sheet": "Sheet1"})).unwrap();
app.run("cell.set", json!({"cell": "B2", "input": "1"})).unwrap();
app.run("cell.set", json!({"cell": "F7", "input": "=B2"})).unwrap();
select(&mut app, "F7");
trace(&mut app, "formulas.tracePrecedents");
app.run("view.zoom", json!({"percent": 75})).unwrap();
let (geo, lines) = arrows(&mut app);
assert_eq!(lines, [(center(&app, &geo, "B2"), center(&app, &geo, "F7"))], "#3097");
// Zooming another sheet leaves them too, and they are back on returning to their sheet.
app.run("sheet.activate", json!({"sheet": "Two"})).unwrap();
app.run("view.zoom", json!({"percent": 200})).unwrap();
assert!(arrows(&mut app).1.is_empty());
app.run("sheet.activate", json!({"sheet": "Sheet1"})).unwrap();
let (geo, lines) = arrows(&mut app);
assert_eq!(lines, [(center(&app, &geo, "B2"), center(&app, &geo, "F7"))]);
}
}
+2
View File
@@ -387,7 +387,9 @@ pub fn status_bar(app: &mut SheetApp, ui: &mut Ui) {
let slider = ui.add(egui::Slider::new(&mut z, 10.0..=400.0).show_value(false).logarithmic(true));
ui.ctx().accesskit_node_builder(slider.id, |node| node.set_label(app.ui.language.tr("Zoom")));
if slider.changed() {
let from = crate::grid::Trace::key(app);
let _ = app.session.run("view.zoom", json!({"percent": z.round()}));
crate::grid::Trace::keep_through_zoom(app, from);
}
if crate::widgets::icon_button(ui, Icon::Minus, t.text_dim, "Zoom Out", vec2(18.0, 18.0)).clicked() {
let z = ((zoom as f32 / 10.0).ceil() * 10.0 - 10.0).max(10.0);
+23
View File
@@ -276,3 +276,26 @@ fn page_and_point_units_scroll_the_grid_by_their_converted_distance() {
let point = h.state().view().scroll.y;
assert!(point > 0.0 && point < page, "40 points scroll less than a page: point {point}, page {page}");
}
#[test]
/// Zooming moves no cell: trace arrows shown before a Ctrl+wheel zoom are still shown after its
/// glide (#3097), while an edit still removes them.
fn trace_arrows_stay_through_a_wheel_zoom() {
let mut h = animated();
h.state_mut().run("cell.set", json!({"cell": "B2", "input": "1"})).unwrap();
h.state_mut().run("cell.set", json!({"cell": "F7", "input": "=B2"})).unwrap();
h.state_mut().run("selection.set", json!({"range": "F7"})).unwrap();
let r = h.state_mut().run("formulas.tracePrecedents", json!({})).unwrap();
let app = h.state_mut();
app.grid.trace = gridcraft_ui_egui::grid::Trace::new(app, r);
let current = |h: &Harness| {
let d = h.state().session.active().unwrap();
h.state().grid.trace.as_ref().is_some_and(|t| (t.uid, t.revision) == (d.uid, d.revision))
};
assert!(current(&h));
let seen = turn_and_watch(&mut h, 1.0, 40);
assert!(seen.windows(2).filter(|w| w[0] != w[1]).count() > 1, "the zoom glides over several frames: {seen:?}");
assert!(current(&h), "the arrows stay at {}%", zoom(&h));
h.state_mut().run("cell.set", json!({"cell": "B2", "input": "2"})).unwrap();
assert!(!current(&h), "an edit removes them");
}
+2 -1
View File
@@ -1,6 +1,6 @@
# UI parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (trace arrows stay through a zoom (#3097); earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **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
@@ -196,6 +196,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
| Date | Change | Summary |
|---|---|---|
| 2026-11-01 | minor | Trace arrows stay when the sheet is zoomed (View › Zoom, the status bar, Ctrl+wheel), as in Excel; every zoom used to remove them (#3097) |
| 2026-10-31 | minor | Esc in a File-page text field leaves the field and cancels editing; the next Esc returns to the sheet (#3035) |
| 2026-10-31 | minor | Recently Used lists the functions most recently entered in a formula from any source, kept with the preferences; the Insert Function dialog is titled as in Excel, its category list (Most Recently Used, All, each category) works, and its category column follows the widest name shown |
| 2026-10-31 | minor | Formulas › Function Library: Insert Function, the AutoSum split button and three columns of labelled small buttons with category colours; category menus end with Insert Function…; More Functions cascades; Home › Editing shares the AutoSum menu; MR key tip for Recently Used |