diff --git a/ROADMAP.md b/ROADMAP.md index 46a87add..7d8fc2db 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~20 points and ~120–210 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (Grid Options dialog; previous: hours to ~95% per audience added; ready 56% → 55%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere @@ -128,6 +128,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-13 | Grid Options: spacing, snap to grid, snap to other objects, and whether gridlines and guides are drawn; the on-screen grid uses that spacing | | 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless | | 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline | | 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting | @@ -150,6 +151,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | Change | Summary | |---|---|---| +| 2026-10-13 | trivial | Progress log: Grid Options dialog | | 2026-10-13 | trivial | Progress log: wild text cases | | 2026-10-12 | trivial | Progress log: File tab (Backstage, #76) | | 2026-10-12 | trivial | Progress log: lines in scaled groups (#233) | diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 253765bf..4b37c2f5 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -135,18 +135,10 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { } app.perf.render_ms = app.textures.last_render_ms; media_frames(app, &painter, xf); - if app.ui.gridlines { - let step = 72.0 / 2.0 * scale; - let mut x = slide_rect.min.x + step; - while x < slide_rect.max.x { - painter.line_segment([pos2(x, slide_rect.min.y), pos2(x, slide_rect.max.y)], Stroke::new(0.5, Color32::from_black_alpha(40))); - x += step; - } - let mut y = slide_rect.min.y + step; - while y < slide_rect.max.y { - painter.line_segment([pos2(slide_rect.min.x, y), pos2(slide_rect.max.x, y)], Stroke::new(0.5, Color32::from_black_alpha(40))); - y += step; - } + if app.ui.gridlines + && let Some(step) = grid_line_step(app.session.prefs.grid_spacing, scale) + { + draw_grid(&painter, slide_rect, step); } if app.ui.guides { let c = slide_rect.center(); @@ -697,3 +689,56 @@ fn context_menu(app: &mut SlideApp, resp: &egui::Response) { } }); } + +/// Screen-point distance between grid lines. `spacing` is the grid spacing in slide points; +/// `scale` maps slide points onto the screen. `None` when the grid would not be a finite set of +/// lines (bad inputs, or so fine that a line would land on every pixel). +fn grid_line_step(spacing: f64, scale: f32) -> Option { + if !spacing.is_finite() || !(1.0..=288.0).contains(&spacing) || !scale.is_finite() || scale <= 0.0 { + return None; + } + let step = (spacing as f32) * scale; + if !step.is_finite() || step < 1.0 { + return None; + } + Some(step) +} + +fn draw_grid(painter: &egui::Painter, slide: Rect, step: f32) { + let stroke = Stroke::new(0.5, Color32::from_black_alpha(40)); + // A denser grid than this paints a grey slab and costs a frame. Skip it. + if !grid_fits(slide.width(), step) || !grid_fits(slide.height(), step) { + return; + } + let mut x = slide.min.x + step; + while x < slide.max.x { + painter.line_segment([pos2(x, slide.min.y), pos2(x, slide.max.y)], stroke); + x += step; + } + let mut y = slide.min.y + step; + while y < slide.max.y { + painter.line_segment([pos2(slide.min.x, y), pos2(slide.max.x, y)], stroke); + y += step; + } +} + +fn grid_fits(span: f32, step: f32) -> bool { + span.is_finite() && span > 0.0 && step >= 1.0 && span / step <= 800.0 +} + +#[cfg(test)] +mod tests { + use super::grid_line_step; + + #[test] + fn grid_step_follows_spacing_and_rejects_unusable_values() { + let step = grid_line_step(72.0, 0.5).unwrap_or(0.0); + assert!((step - 36.0).abs() < 1e-3, "{step}"); + assert!(grid_line_step(6.0, 1.0).is_some()); + assert!(grid_line_step(0.0, 1.0).is_none()); + assert!(grid_line_step(f64::NAN, 1.0).is_none()); + assert!(grid_line_step(1_000.0, 1.0).is_none()); + assert!(grid_line_step(6.0, 0.0).is_none()); + assert!(grid_line_step(6.0, 0.1).is_none(), "a 6 pt grid at 10% zoom is finer than one screen point"); + } +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index d1934be9..845ca485 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -45,6 +45,7 @@ fn title(id: &str) -> &'static str { "paragraph" => "Paragraph", "about" => "About DeckCraft", "preferences" => "Preferences", + "gridOptions" => "Grid Options", "export" => "Export", "symbol" => "Symbol", "equation" => "Equation", @@ -708,6 +709,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let (ok, _) = buttons(ui, "Close"); ok } + "gridOptions" => grid_options(app, ui, d), "preferences" => { ui.label(egui::RichText::new("General").font(theme::bold(13.0))); let mut dark = app.ui.brightness == theme::Brightness::Dark; @@ -738,6 +740,43 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { } } +fn format_spacing(v: f64) -> String { + if !v.is_finite() { + return "6".into(); + } + if (v - v.round()).abs() < 1e-6 && v.abs() < 1.0e6 { format!("{}", v.round() as i64) } else { format!("{v:.2}") } +} + +/// Grid Options: spacing, snap and whether the grid and guides are drawn. OK applies one +/// `view.gridOptions` call; Cancel leaves the presentation as it was. +fn grid_options(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { + ui.label(egui::RichText::new("Snap").font(theme::bold(13.0))); + let snap = check(ui, d, "Snap objects to grid", "snap", app.session.prefs.snap_to_grid); + let smart = check(ui, d, "Snap objects to other objects", "smart", app.session.prefs.smart_guides); + let spacing_default = format_spacing(app.session.prefs.grid_spacing); + field(ui, d, "Spacing (pt):", "spacing", &spacing_default); + ui.add_space(6.0); + ui.label(egui::RichText::new("Display").font(theme::bold(13.0))); + let gridlines = check(ui, d, "Display grid on screen", "gridlines", app.ui.gridlines); + let guides = check(ui, d, "Display drawing guides on screen", "guides", app.ui.guides); + if let Some(err) = d.fields.get("error").cloned().filter(|s| !s.is_empty()) { + ui.add_space(4.0); + ui.label(egui::RichText::new(err).color(Color32::from_rgb(0xB0, 0x30, 0x30))); + } + let (ok, cancel) = buttons(ui, "OK"); + if ok { + let spacing = num(d, "spacing", app.session.prefs.grid_spacing); + match app.run("view.gridOptions", json!({"spacing": spacing, "snap": snap, "smartGuides": smart, "gridlines": gridlines, "guides": guides})) { + Ok(_) => return true, + Err(e) => { + d.fields.insert("error".into(), e); + return false; + } + } + } + cancel +} + fn pretty_equation(s: &str) -> String { let sup = |c: char| match c { '0' => '⁰', diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index ec97e6e6..4de17d60 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -142,6 +142,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("view.ruler", "Ruler", None, "{on?: bool}"), ("view.gridlines", "Gridlines", Some("Shift+F9"), "{on?: bool}"), ("view.guides", "Guides", Some("Alt+F9"), "{on?: bool}"), + ("view.gridOptions", "Grid Options…", None, "{spacing?: pt, snap?: bool, smartGuides?: bool, gridlines?: bool, guides?: bool}"), ("view.zoomIn", "Zoom In", Some("Cmd+="), "{}"), ("view.zoomOut", "Zoom Out", Some("Cmd+-"), "{}"), ("view.zoom", "Zoom…", None, "{percent}"), @@ -229,6 +230,18 @@ pub struct SlideApp { pub selpane: selpane::State, } +fn grid_options_touched(p: &Value) -> bool { + ["spacing", "snap", "smartGuides", "gridlines", "guides"].iter().any(|k| p.get(*k).is_some()) +} + +fn grid_flag(p: &Value, key: &str) -> Result, String> { + match p.get(key) { + None => Ok(None), + Some(Value::Bool(b)) => Ok(Some(*b)), + Some(_) => Err(format!("`{key}` must be true or false")), + } +} + impl SlideApp { pub fn new(session: Session, mut services: Services) -> Self { let audio_out = services.audio_out.take(); @@ -458,6 +471,49 @@ impl SlideApp { "view.ruler" => self.ui.ruler = self.flag(p, self.ui.ruler), "view.gridlines" => self.ui.gridlines = self.flag(p, self.ui.gridlines), "view.guides" => self.ui.guides = self.flag(p, self.ui.guides), + "view.gridOptions" => { + if grid_options_touched(p) { + let spacing = match p.get("spacing") { + None => None, + Some(raw) => { + let n = raw.as_f64().filter(|v| v.is_finite()).ok_or("`spacing` must be a finite number of points")?; + if !(1.0..=288.0).contains(&n) { + return Err("`spacing` must be from 1 to 288 points".into()); + } + Some(n) + } + }; + let snap = grid_flag(p, "snap")?; + let smart = grid_flag(p, "smartGuides")?; + let gridlines = grid_flag(p, "gridlines")?; + let guides = grid_flag(p, "guides")?; + if let Some(n) = spacing { + self.session.prefs.grid_spacing = n; + } + if let Some(b) = snap { + self.session.prefs.snap_to_grid = b; + } + if let Some(b) = smart { + self.session.prefs.smart_guides = b; + self.ui.smart_guides = b; + } + if let Some(b) = gridlines { + self.ui.gridlines = b; + } + if let Some(b) = guides { + self.ui.guides = b; + } + } else { + self.dialog = Some(dialogs::Dialog::new("gridOptions")); + } + return Ok(json!({ + "spacing": self.session.prefs.grid_spacing, + "snap": self.session.prefs.snap_to_grid, + "smartGuides": self.session.prefs.smart_guides, + "gridlines": self.ui.gridlines, + "guides": self.ui.guides, + })); + } "view.zoomIn" | "view.zoomOut" => { let cur = self.ui.zoom.unwrap_or(self.canvas_scale); let steps = [0.1, 0.25, 0.33, 0.5, 0.66, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0]; @@ -1305,6 +1361,79 @@ mod tests { app.run("app.home", json!({})).unwrap(); assert!(!app.backstage_open(), "with nothing open the start screen shows anyway"); } + + #[test] + fn grid_options_sets_spacing_and_display_or_opens_the_dialog() { + let mut app = SlideApp::new(Session::with_new(), Services::default()); + assert!((app.session.prefs.grid_spacing - 6.0).abs() < 1e-9); + let v = + app.run("view.gridOptions", json!({"spacing": 72.0, "snap": false, "smartGuides": false, "gridlines": true, "guides": true})).unwrap(); + assert_eq!(v["spacing"], 72.0); + assert_eq!(v["snap"], false); + assert_eq!(v["smartGuides"], false); + assert_eq!(v["gridlines"], true); + assert_eq!(v["guides"], true); + assert!(app.dialog.is_none()); + assert!(app.ui.gridlines && app.ui.guides); + assert!(!app.session.prefs.snap_to_grid && !app.ui.smart_guides); + + app.run("view.gridOptions", json!({"snap": true})).unwrap(); + assert!(app.session.prefs.snap_to_grid); + assert!((app.session.prefs.grid_spacing - 72.0).abs() < 1e-9); + assert!(app.dialog.is_none()); + + for bad in [json!({"spacing": 0}), json!({"spacing": 289}), json!({"spacing": "72"}), json!({"spacing": null}), json!({"snap": 1})] { + assert!(app.run("view.gridOptions", bad).is_err(), "rejected a bad grid option"); + } + assert!((app.session.prefs.grid_spacing - 72.0).abs() < 1e-9); + assert!(app.session.prefs.snap_to_grid); + + app.run("view.gridOptions", json!({})).unwrap(); + assert_eq!(app.dialog.as_ref().map(|d| d.id.as_str()), Some("gridOptions")); + assert!((app.session.prefs.grid_spacing - 72.0).abs() < 1e-9); + } + + #[test] + fn grid_options_dialog_lists_spacing_and_display() { + use egui_kittest::kittest::Queryable; + let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SlideApp::new(Session::with_new(), Services::default()), + ); + harness.run_steps(2); + harness.state_mut().run("view.gridOptions", json!({})).unwrap(); + harness.run_steps(2); + assert!(harness.query_by_label_contains("Display grid on screen").is_some()); + assert!(harness.query_by_label_contains("Snap objects to grid").is_some()); + assert!(harness.query_by_label_contains("Spacing").is_some()); + let show_grid = harness + .query_all_by_role_and_label(egui::accesskit::Role::CheckBox, "Display grid on screen") + .next() + .unwrap_or_else(|| panic!("no grid checkbox")); + show_grid.click_accesskit(); + harness.run_steps(2); + harness.query_all_by_role_and_label(egui::accesskit::Role::Button, "OK").next().unwrap_or_else(|| panic!("no OK button")).click_accesskit(); + harness.run_steps(2); + assert!(harness.state().dialog.is_none(), "OK closes Grid Options"); + assert!(harness.state().ui.gridlines, "OK applies the grid checkbox"); + assert!((harness.state().session.prefs.grid_spacing - 6.0).abs() < 1e-9); + + harness.state_mut().run("view.gridOptions", json!({})).unwrap(); + harness.run_steps(2); + harness + .query_all_by_role_and_label(egui::accesskit::Role::Button, "Cancel") + .next() + .unwrap_or_else(|| panic!("no Cancel button")) + .click_accesskit(); + harness.run_steps(2); + assert!(harness.state().dialog.is_none(), "Cancel closes Grid Options"); + assert!(harness.state().ui.gridlines, "Cancel leaves the grid on"); + assert!((harness.state().session.prefs.grid_spacing - 6.0).abs() < 1e-9); + } + fn canvas_harness() -> egui_kittest::Harness<'static, SlideApp> { let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( |ui, app: &mut SlideApp| { diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 25398b45..ba9dfb38 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -194,6 +194,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<(&'static str, &'static str)>)> { ("Ruler", "view.ruler"), ("Gridlines", "view.gridlines"), ("Guides", "view.guides"), + ("Grid Options…", "view.gridOptions"), ("-", ""), ("Zoom In", "view.zoomIn"), ("Zoom Out", "view.zoomOut"), diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 428eff0b..722a3d05 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1842,6 +1842,9 @@ fn view(app: &mut SlideApp, ui: &mut Ui) { app.ui.guides = gd; } }); + if big_button(ui, Icon::Gridlines, "Grid\nOptions", true).clicked() { + let _ = app.run("view.gridOptions", json!({})); + } let n = app.ui.notes; if big_button_ex(ui, Icon::Notes, "Notes", true, false, n).0.clicked() { app.ui.notes = !n; diff --git a/docs/gaps.md b/docs/gaps.md index a2894b42..4c68b1af 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where DeckCraft falls short of PowerPoint -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (3-D bevel, camera and face lighting rendered; previous: minor, IME composition and CJK interface text landed, #101; previous: minor, core-workflow alpha gate added; stage stays alpha; before that: major, first itemized gap list; full re-measure against PowerPoint for Mac 16.113.4 and the open GitHub issues) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (Grid Options dialog; previous: minor, 3-D bevel, camera and face lighting rendered; previous: minor, IME composition and CJK interface text landed, #101) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Every known shortfall, one entry each, ranked by how much it stops someone who presents with PowerPoint from switching. This is the work list: pick from the top. Numbers are summarized in @@ -203,7 +203,7 @@ Detail in [ui-parity.md](ui-parity.md). - **Window chrome** (#71 macOS controls misaligned, #26 GNOME title bar, #41 system light/dark). 2–4 h. - **Icon consistency across Craft apps** (#73, #56). 2–4 h; coordinate in craftrules. -- **Dialogs**: 27 dialogs vs PowerPoint's ~60 (Font, Bullets and Numbering, Find/Replace dialog, +- **Dialogs**: 28 dialogs vs PowerPoint's ~60 (Font, Bullets and Numbering, Find/Replace dialog, Replace Fonts, Action Settings, Print, Insert Object…). 8–14 h. - **Command palette defaults to an Export entry that fails** (#79). 0.5 h. @@ -256,6 +256,7 @@ Detail in [localization-parity.md](localization-parity.md). | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | Grid Options dialog sets spacing, snap and whether gridlines and guides show; the on-screen grid follows that spacing | | 2026-10-13 | minor | Shape, picture and table rendering checked against PowerPoint on ~150 synthetic edge-case slides; 3-D bevels, cameras and face lighting now render approximately (gap narrowed to extrusion, rigs and the 3-D pane) | | 2026-10-12 | minor | IME composition in slide text and notes, and CJK interface text with real glyphs (#101): the IME gap is now Korean on the web and table-cell underlines | | 2026-10-12 | minor | EMF/WMF text, pattern and hatch brushes drawn; legacy OLE and ActiveX previews shown; the SVG gap keeps only EMF clip regions | diff --git a/docs/roadmap.md b/docs/roadmap.md index 97953034..11bfc0aa 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # DeckCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (core-workflow alpha gate added; stage stays alpha; previous: major, created; milestones carried from the local execution plan, the beta plan built from gaps.md) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (Grid Options; previous: core-workflow alpha gate added; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Forward-looking: where DeckCraft is going and in what order. The one-page summary is [ROADMAP.md](../ROADMAP.md); every shortfall is in [gaps.md](gaps.md); numbers are in @@ -80,7 +80,7 @@ From the local execution plan (`plan/execution-plan.md`, gitignored). Status as | M7 | Slide show | mostly | second-display placement, laser, record | | M8 | Tables and charts | partly | chart data grid, elements, chartex types, draw table | | M9 | SmartArt, equations, WordArt | partly | SmartArt text pane and layouts, equations (PR #55), text warps | -| M10 | Views | done | grid options, New Window | +| M10 | Views | done | New Window | | M11 | Output | partly | vector PDF, SVG, GIF, MP4, print | | M12 | Review | partly | dictionaries, AutoCorrect, find/replace dialog, compare | | M13 | Automation and CLI parity | done | | @@ -109,5 +109,6 @@ plan above. | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | M10: Grid Options dialog; New Window stays open | | 2026-10-10 | minor | Added the alpha gate (six core workflows): all pass, two with partial depth; stage stays alpha | | 2026-10-10 | major | Created: current focus, ranked beta plan with hours, milestones M0–M14 with status, the old alpha checklist as history | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 99653dc7..cf565002 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # DeckCraft vs Microsoft PowerPoint: parity assessment -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready recomputed from written weights, 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; replaces the parity sections of ROADMAP.md) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (Views: Grid Options landed; New Window remains; previous: hours to ~95% per audience added; ready 56% → 55%) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) The authoritative answer to "how close is DeckCraft to PowerPoint, and how much work is left?". [ROADMAP.md](../ROADMAP.md) summarizes it; [gaps.md](gaps.md) itemizes every shortfall; @@ -182,7 +182,7 @@ it is the Features row of the dimension table. Automation and Platforms are scor | Application shell | 5 | 71% | ~55% | 8–14 | No native macOS menu bar, thin start screen (templates, recent), one window per presentation missing | [ui-parity.md](ui-parity.md) | | Files (commands) | 4 | 69% | ~50% | see file formats | Print, reuse slides, passwords, legacy and macro formats | [file-format-parity.md](file-format-parity.md) | | Slides and sections | 6 | 90% | ~85% | 3–5 | Zoom (summary/section/slide), copying slides between decks keeps formatting only partly | [parity-checklist.md](parity-checklist.md) | -| Views | 4 | 90% | ~80% | 3–6 | Snap to grid and grid options, New Window | [ui-parity.md](ui-parity.md) | +| Views | 4 | 90% | ~80% | 3–5 | New Window | [ui-parity.md](ui-parity.md) | | Masters and themes | 7 | 85% | ~70% | 8–14 | Notes and Handout masters, theme variants, `.thmx`, effect schemes | [parity-checklist.md](parity-checklist.md) | | Shapes | 7 | 86% | ~75% | 8–14 | Edit points, text warp, WordArt depth | [ui-parity.md](ui-parity.md) | | Format (fill, line, effects) | 6 | 81% | ~65% | 8–14 | Format Shape pane depth, bevel and 3-D, picture/texture fill options | [ui-parity.md](ui-parity.md) | @@ -276,6 +276,7 @@ scored done (16 open issues). | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | Views: Grid Options dialog landed (spacing, snap, display). Remaining in that row: New Window. Percentages unchanged | | 2026-10-10 | minor | Added the readiness-by-audience table: each number gets its own hours to ~95% (full 340–600, mainstream 150–250, essentials 50–90). The full number stays an additive weighted sum; no change to the percentages | | 2026-10-10 | minor | Added the mainstream practitioner (~53%) and essentials user (~62%) numbers with written weights, discounts and user evidence. Features dimension recomputed from written area weights (66% judgement → 62.4% weighted mean), which moves ready for real work from ~56% to ~55%; stage unchanged | | 2026-10-10 | minor | Stage checked against the core-workflow gate (all six pass); stays alpha | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index c403f05e..af76d104 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity with PowerPoint -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (File tab / Backstage, #76; previous: off-slide objects in Normal view, #270) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (Grid Options dialog; previous: File tab / Backstage, #76; previous: off-slide objects in Normal view, #270) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (text editing keys observed in PowerPoint for Windows, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) @@ -64,7 +64,7 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`). | Animation Pane | list + advanced timeline | list, reorder, timing; no timeline | | Comments pane (threads) | yes | yes | | Design Ideas (Designer) | yes (cloud) | no | -| Dialogs | ~60 (Font, Paragraph, Bullets and Numbering, Find, Replace, Replace Fonts, Hyperlink, Action Settings, Header and Footer, Slide Size, Set Up Show, Custom Shows, Print, Page Setup, Insert Object, Symbol, Equation, Chart data sheet, SmartArt chooser, Preferences…) | 27 (`crates/ui-egui/src/dialogs.rs`): Table, Slide Size, Header and Footer, Hyperlink, Set Up Show, Zoom, Comment, Alt Text, Outline, Rename (section, shape, layout), Paragraph, About, Preferences, Export, Symbol, Equation (placeholder), Spelling, Accessibility, Language, Custom Shows, Chart Data, Trim Media, SmartArt chooser, Quit, Notes Pages | +| Dialogs | ~60 (Font, Paragraph, Bullets and Numbering, Find, Replace, Replace Fonts, Hyperlink, Action Settings, Header and Footer, Slide Size, Set Up Show, Custom Shows, Print, Page Setup, Insert Object, Symbol, Equation, Chart data sheet, SmartArt chooser, Preferences…) | 28 (`crates/ui-egui/src/dialogs.rs`): Table, Slide Size, Header and Footer, Hyperlink, Set Up Show, Zoom, Comment, Alt Text, Outline, Rename (section, shape, layout), Paragraph, About, Preferences, Grid Options, Export, Symbol, Equation (placeholder), Spelling, Accessibility, Language, Custom Shows, Chart Data, Trim Media, SmartArt chooser, Quit, Notes Pages | ## Direct manipulation @@ -74,7 +74,7 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`). | Marquee, Shift/Cmd multi-select, Tab to cycle | yes | yes | | Objects off the slide: drawn on the area around it in Normal view (full strength, master graphics too), selectable and movable there; clipped in the show, exports and thumbnails | yes | yes (#270) | | Smart guides (edges, centres, equal spacing) | yes | yes | -| Snap to grid, grid options, drawing guides | yes | gridlines, guides and snap to grid (engine setting); no Grid Options dialog or menu toggle | +| Snap to grid, grid options, drawing guides | yes | Grid Options (`view.gridOptions`): spacing, snap to grid, snap to other objects, display grid and guides; the on-screen grid uses that spacing | | Nudge (arrows), fine nudge (Option+arrows) | yes | yes | | Shift-constrained move, Option-drag to duplicate | yes | yes | | Connector glue and rerouting | yes | yes | @@ -139,15 +139,16 @@ imitate Microsoft's. No pixel comparison against PowerPoint screenshots has been | Format Shape pane depth | 6–10 | | Missing dialogs (Font, Bullets and Numbering, Find/Replace, Replace Fonts, Action Settings, Print, Insert Object) | 8–14 | | Window chrome, system appearance, palette fixes (#41, #71, #79) | 3–5 | -| Grid Options, edit points UI, crop depth | 4–7 | +| Edit points UI, crop depth | 3–6 | | Multiple windows | 3–5 | | A screenshot comparison pass on the most-used tabs | 2–4 | -| **Total** | **~30–52** | +| **Total** | **~29–51** | ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | Grid Options dialog: spacing, snap, and whether gridlines and guides are drawn; the on-screen grid uses that spacing | | 2026-10-12 | minor | File tab (Backstage) observed in PowerPoint for Windows and matched (#76); the start screen is File › Home | | 2026-10-12 | minor | Off-slide objects drawn and editable around the slide in Normal view (#270) | | 2026-10-12 | minor | Selection Pane matched to PowerPoint (#133): drag and drop, arrows, groups, eye, Lock |