From be028cc6cc7cbc2489b695956541319fafcb40e6 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sun, 11 Oct 2026 12:09:30 +0200 Subject: [PATCH] View: Formula Bar and Zoom 100% as engine commands (#289) - view.zoom100 sets the active sheet's zoom to 100% (the ribbon's 100% button runs it). - view.formulaBar {on?} toggles a formulaBar preference (Prefs, saved in prefs.json on desktop; an application option, not in the file). The UI shows the formula bar from it; the ribbon checkbox, native menu and ui.set {formulaBar} go through the command. - The UI-only interceptions and their entries in engine.commands are gone, so the CLI and MCP can run both. Parity checklist regenerated (260 / 290). Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 6 ++++-- crates/engine/src/cmd/view.rs | 30 ++++++++++++++++++++++++++++++ crates/engine/src/lib.rs | 3 +++ crates/ui-egui/src/control.rs | 10 ++++++---- crates/ui-egui/src/lib.rs | 9 +-------- crates/ui-egui/src/ribbon.rs | 6 +++--- docs/control-protocol.md | 2 +- docs/parity-checklist.md | 13 +++++-------- docs/target-app-parity.md | 5 +++-- 9 files changed, 56 insertions(+), 28 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 1189fd721..fcf6ca634 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (full re-measure against Excel for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (View › Formula Bar and Zoom 100% as engine commands, catalog 260 / 290, #289) · **Target:** Microsoft Excel (Microsoft 365) GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web. @@ -21,7 +21,7 @@ This page is the summary. The detail is in: | Number | Value | Kind | |---|---|---| -| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 258 / 290 (89.0%), worksheet functions 501 / 523 (95.8%; 492 working, 94.1%) | +| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 260 / 290 (89.7%), worksheet functions 501 / 523 (95.8%; 492 working, 94.1%) | | **Ready for real work** | **~50%** | estimated from written weights (51.6); [target-app-parity.md](docs/target-app-parity.md) | | **Mainstream practitioner** | **~50%** | estimated: typical analyst/finance user, weekly features, × 0.90 interaction × 0.92 stability × 0.88 file exchange; [method](docs/target-app-parity.md#mainstream-practitioner) | | **Essentials user** | **~63%** | estimated: core features only, × 0.92 launch/stability × 0.95 clarity × 0.90 opening sent files; [method](docs/target-app-parity.md#essentials-user) | @@ -131,6 +131,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | What landed | |---|---| +| 2026-10-11 | View › Formula Bar and Zoom 100% reachable from the CLI and MCP (#289) | | 2026-10-10 | Ribbon translated into Japanese, Chinese, Korean, Brazilian Portuguese and Russian with system-language default (#31, #104, #82); CJK/Thai/Hebrew font fallback (#38); clear messages for encrypted and `.xls` files (#17); smooth line charts (#62). Progress docs restructured to the craftrules standard; full re-measure (this page). Initial ribbon key tips (#43), in-place text box editing (#84), copy as HTML table (#36). ODS and XLSB data import (#100, #101); recalc linear in the number of range formulas (#66); RISC-V Linux build; Flatpak repository on Pages; web cell-commit trap fixed | | 2026-10-09 | Community fixes: spill range operator `A1#`, sheet-scoped names, protection refuses locked-cell edits, Insert/Delete Cells adjust references, `DATE(1900,2,29)`, SUMIF sizing, `ROUND` at 15 digits, `_xlpm.` LAMBDA parameters in XLSX, vertical scrollbar, system appearance, border previews; v0.4.0 | | 2026-10-08 | v0.2.0 and v0.3.0 releases; Flatpak, AppImage zsync, branded DMG; desktop log file; DirectX 12 default on Windows | @@ -142,6 +143,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Catalog 258 → 260 / 290: View › Formula Bar and Zoom 100% (#289) | | 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% per audience; full number unchanged (additive weighted sum) | | 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%); languages updated for the 10-10 merges (ribbon in ja, zh, ko, pt-BR, ru); localization 5%→10%; full number re-checked (51.6, unchanged) | | 2026-10-10 | minor | Checked against the alpha gate (six core workflows pass): stays alpha | diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index d8450cdbf..ac97040f2 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -28,6 +28,11 @@ pub fn specs() -> Vec { cmd!("view.showZeros", "Show Zeros", [], None, "{on?}", has_doc, |s, p| sheet_flag(s, p, "zeros")), cmd!("view.rightToLeft", "Sheet Right-to-Left", ["Page Layout", "Sheet Options"], None, "{on?}", has_doc, |s, p| sheet_flag(s, p, "rtl")), cmd!("view.zoom", "Zoom", ["View", "Zoom"], None, "{percent: 10..400}", has_doc, zoom), + cmd!("view.zoom100", "100%", ["View", "Zoom"], None, "{}", has_doc, |s, _| zoom(s, &json!({"percent": 100}))), + cmd!(query "view.formulaBar", "Formula Bar", ["View", "Show"], None, "{on?: bool}", always, |s, p| { + s.prefs.formula_bar = bool_param(p, "on").unwrap_or(!s.prefs.formula_bar); + Ok(json!({"on": s.prefs.formula_bar})) + }), cmd!("view.zoomToSelection", "Zoom to Selection", ["View", "Zoom"], None, "{viewWidth?, viewHeight?}", has_doc, zoom_selection), cmd!("pageLayout.orientation", "Orientation", ["Page Layout", "Page Setup"], None, "{orientation: portrait|landscape}", has_doc, orientation), cmd!("pageLayout.size", "Size", ["Page Layout", "Page Setup"], None, "{paper: Letter|Legal|A4|A3|A5|Tabloid|Executive}", has_doc, paper), @@ -336,3 +341,28 @@ fn theme(s: &mut Session, p: &Json) -> Result { #[allow(dead_code)] fn unused(_: CellRef) {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn zoom_100_resets_the_active_sheet_zoom() { + let mut s = Session::new(); + s.new_workbook(); + s.execute("view.zoom", json!({"percent": 150})).unwrap(); + assert_eq!(s.execute("view.zoom100", json!({})).unwrap()["zoom"], 100); + assert_eq!(s.doc().unwrap().wb.active().unwrap().zoom, 100); + } + + #[test] + fn formula_bar_toggles_without_a_workbook_and_defaults_to_shown() { + let mut s = Session::new(); + assert!(s.prefs.formula_bar); + assert_eq!(s.execute("view.formulaBar", json!({})).unwrap()["on"], false); + assert_eq!(s.execute("view.formulaBar", json!({"on": true})).unwrap()["on"], true); + // A prefs.json saved before the option existed keeps the formula bar shown. + let old: crate::Prefs = serde_json::from_str(r#"{"userName": "Ann"}"#).unwrap(); + assert!(old.formula_bar); + } +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index de786a1f4..081964282 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -150,6 +150,8 @@ pub struct Prefs { pub r1c1: bool, /// Words added with Add to Dictionary. pub user_dictionary: Vec, + /// View › Show › Formula Bar (an application option in Excel, not saved in the file). + pub formula_bar: bool, } impl Default for Prefs { @@ -163,6 +165,7 @@ impl Default for Prefs { sheets_in_new_workbook: 1, r1c1: false, user_dictionary: vec![], + formula_bar: true, } } } diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index e6da0f00b..6160f0a59 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -168,11 +168,9 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) - let mut v: Vec = app.session.commands().into_iter().map(|c| serde_json::to_value(c).unwrap_or_default()).collect(); for (id, label, params) in [ ("ui.ribbonTab", "Ribbon Tab", ""), - ("view.formulaBar", "Formula Bar", ""), ("view.collapseRibbon", "Collapse Ribbon", ""), ("view.darkMode", "Dark Mode", ""), ("view.theme", "Display Theme", ""), - ("view.zoom100", "100%", ""), ("ui.dialog", "Open Dialog", ""), ("app.language.set", "Interface Language", "{language: \"en\"|\"zh\"|\"ja\"|\"ko\"|\"ru\"|\"pt\"} (alias: code)"), ("app.language.english", "Interface Language: English", ""), @@ -196,12 +194,16 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) - app.ui.system_theme = false; } if let Some(v) = p.get("formulaBar").and_then(Json::as_bool) { - app.ui.formula_bar = v; + let _ = app.run("view.formulaBar", json!({"on": v})); } if let Some(v) = p.get("ribbonCollapsed").and_then(Json::as_bool) { app.ui.ribbon_collapsed = v; } - ok(serde_json::to_value(&app.ui).unwrap_or_default()) + let mut out = serde_json::to_value(&app.ui).unwrap_or_default(); + if let Some(o) = out.as_object_mut() { + o.insert("formulaBar".into(), json!(app.session.prefs.formula_bar)); + } + ok(out) } "ui.dialog" => { app.open_dialog(s("name").unwrap_or(""), p.clone()); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 1225eecdf..a799cc50e 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -45,7 +45,6 @@ pub struct UiState { pub system_theme: bool, pub ribbon_tab: String, pub ribbon_collapsed: bool, - pub formula_bar: bool, pub formula_bar_expanded: bool, pub status_bar: bool, pub recent: Vec, @@ -64,7 +63,6 @@ impl Default for UiState { system_theme: false, ribbon_tab: "Home".into(), ribbon_collapsed: false, - formula_bar: true, formula_bar_expanded: false, status_bar: true, recent: vec![], @@ -294,10 +292,6 @@ impl SheetApp { self.ui.ribbon_tab = p.get("tab").and_then(Json::as_str).unwrap_or("Home").to_string(); Ok(Json::Null) } - "view.formulaBar" => { - self.ui.formula_bar = p.get("on").and_then(Json::as_bool).unwrap_or(!self.ui.formula_bar); - Ok(json!({"on": self.ui.formula_bar})) - } "view.collapseRibbon" => { self.ui.ribbon_collapsed = !self.ui.ribbon_collapsed; Ok(Json::Null) @@ -319,7 +313,6 @@ impl SheetApp { } Ok(json!({"mode": self.ui.theme_mode()})) } - "view.zoom100" => return Some(self.session.run("view.zoom", json!({"percent": 100})).inspect(|_| self.after_engine())), "app.language.set" => { // `{"language": "ja"}`; `code` is accepted as an alias. let code = p.get("language").or_else(|| p.get("code")).and_then(Json::as_str).unwrap_or(""); @@ -608,7 +601,7 @@ impl SheetApp { self.ribbon_keys(&ctx); ribbon::title_bar(self, ui); ribbon::show(self, ui); - if self.ui.formula_bar { + if self.session.prefs.formula_bar { formula_bar::show(self, ui); } if self.ui.status_bar { diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 264479a8c..5af6c00ab 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1636,9 +1636,9 @@ fn view(app: &mut SheetApp, ui: &mut Ui) { let s = app.session.active().and_then(|d| d.wb.active().map(|s| (s.show_gridlines, s.show_headings))).unwrap_or((true, true)); let lang = app.ui.language; ui.vertical(|ui| { - let mut fb = app.ui.formula_bar; + let mut fb = app.session.prefs.formula_bar; if ui.checkbox(&mut fb, lang.tr("Formula Bar")).changed() { - app.ui.formula_bar = fb; + act(app, "view.formulaBar", json!({"on": fb})); } let mut g = s.0; if ui.checkbox(&mut g, lang.tr("Gridlines")).changed() { @@ -1654,7 +1654,7 @@ fn view(app: &mut SheetApp, ui: &mut Ui) { app.open_dialog("zoom", json!({})); } if big_button(ui, Icon::Search, "100%", "Zoom to 100%", false).clicked() { - act(app, "view.zoom", json!({"percent": 100})); + act(app, "view.zoom100", json!({})); } if big_button(ui, Icon::Zoom, "Zoom to\nSelection", "Zoom to Selection", false).clicked() { let r = app.grid.cells_rect.unwrap_or(Rect::from_min_size(pos2(0.0, 0.0), vec2(1200.0, 700.0))); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index aeb540d82..2b1beaad7 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -33,7 +33,7 @@ Errors come back as `{"id": …, "ok": false, "error": "…"}`. | `ui.ribbon` | `{tab}` | Switch ribbon tab | | `ui.dialog` / `ui.dialog.set` / `ui.dialog.confirm` / `ui.dialog.cancel` | `{name}` / `{field, value}` | Open, fill in and confirm dialogs | | `ui.screenshot` | `{path?}` | PNG of the window (base64 when no path) | -| `ui.set` | `{dark?, formulaBar?, ribbonCollapsed?}` | UI preferences | +| `ui.set` | `{dark?, formulaBar?, ribbonCollapsed?}` | UI preferences (`formulaBar` runs `view.formulaBar`) | | `ui.resize` / `ui.focus` | `{width, height}` | Window control | | `app.open` / `app.save` / `app.quit` | `{path}` / `{path?}` | Files and lifetime | diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index e98835d31..613b5c006 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -4,7 +4,7 @@ Each entry is an Excel ribbon or menu command (feature names only). It counts as implemented when GridCraft registers a command with the same id. This measures breadth only; the overall assessment (depth, fidelity, effort) is in [target-app-parity.md](target-app-parity.md). -**Overall: 258 / 290 commands implemented (89.0%).** +**Overall: 260 / 290 commands implemented (89.7%).** | Tab | Implemented | Total | Parity | |---|---|---|---| @@ -16,7 +16,7 @@ Each entry is an Excel ribbon or menu command (feature names only). It counts as | Formulas | 23 | 23 | 100% | | Data | 20 | 28 | 71% | | Review | 9 | 18 | 50% | -| View | 18 | 20 | 90% | +| View | 20 | 20 | 100% | | Automate | 3 | 3 | 100% | | Table Design | 12 | 13 | 92% | | Chart Design | 8 | 8 | 100% | @@ -302,7 +302,7 @@ Missing: - `review.allowEditRanges` Allow Edit Ranges (Protect) - `review.showChanges` Show Changes (Changes) -## View (18 / 20, 90%) +## View (20 / 20, 100%) Implemented: @@ -311,7 +311,9 @@ Implemented: - `view.pageLayout` Page Layout (Workbook Views) - `view.customViews` Custom Views (Workbook Views) - `view.ruler` Ruler (Show) +- `view.formulaBar` Formula Bar (Show) - `view.zoom` Zoom (Zoom) +- `view.zoom100` 100% (Zoom) - `view.zoomToSelection` Zoom to Selection (Zoom) - `view.newWindow` New Window (Window) - `view.arrangeAll` Arrange All (Window) @@ -325,11 +327,6 @@ Implemented: - `view.switchWindows` Switch Windows (Window) - `view.macros` Macros (Macros) -Missing: - -- `view.formulaBar` Formula Bar (Show) -- `view.zoom100` 100% (Zoom) - ## Automate (3 / 3, 100%) Implemented: diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2e5ed01cd..e70bcdbc8 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Target-app parity: GridCraft vs Microsoft Excel -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first full assessment; re-measure against Excel for Mac 16.113.4, replacing the estimates that lived in ROADMAP.md) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (View › Formula Bar and Zoom 100% as engine commands, catalog 260 / 290, #289) · **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: @@ -13,7 +13,7 @@ generated ribbon checklist is [`parity-checklist.md`](parity-checklist.md). | Number | Value | Kind | |---|---|---| | Feature breadth (weighted by use) | **~75%** | estimated from the area table below; anchored on two measurements | -| Ribbon and menu catalog | **258 / 290 (89.0%)** | measured: `cargo xtask parity` (`crates/engine/src/catalog.rs` ∩ registered command ids) | +| Ribbon and menu catalog | **260 / 290 (89.7%)** | measured: `cargo xtask parity` (`crates/engine/src/catalog.rs` ∩ registered command ids) | | Worksheet functions, by name | **501 / 523 (95.8%)** | measured: registered functions vs Microsoft's published function list (method below) | | Worksheet functions that do real work | **492 / 523 (94.1%)** | measured: excludes 9 stubs (7 CUBE functions, WEBSERVICE, FILTERXML) | | **Ready for real work** | **~50%** | estimated; written weights below (51.6 computed) | @@ -332,6 +332,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | View › Formula Bar and Zoom 100% are engine commands (CLI/MCP); catalog 258 → 260 / 290 (#289) | | 2026-10-10 | minor | Readiness-by-audience table: hours to ~95% for each of the three numbers (full 650–1,050, mainstream 200–310, essentials 60–100). Full number confirmed as the additive weighted sum over the written dimension weights (no change) | | 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%) with written weights and discounts, and user-evidence counts; re-checked the full number after the 10-10 merges (charts 38→30, localization 5→10; still 51.6, ~50%) | | 2026-10-10 | minor | Stage checked against the new alpha gate (six core workflows, all pass): stays alpha |