diff --git a/ROADMAP.md b/ROADMAP.md index 46a87add..db627fa9 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-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: Zoom dialog; breadth stays 79%; previous: minor, hours to ~95%) · **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-14 | View: the Zoom dialog takes a percent or Fit and applies it on OK. View ▸ Zoom opens it; the arrow still lists the presets | | 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-14 | trivial | Progress log: Zoom 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/dialogs.rs b/crates/ui-egui/src/dialogs.rs index d1934be9..a73454d9 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -117,6 +117,67 @@ fn num(d: &mut Dialog, key: &str, default: f64) -> f64 { d.get(key, "").trim().trim_end_matches(['"', '%', ' ']).parse::().ok().filter(|v| v.is_finite()).unwrap_or(default) } +/// Preset percents in the Zoom dialog, largest first. +const ZOOM_PRESETS: &[u32] = &[400, 200, 150, 100, 75, 66, 50, 33]; + +/// A typed zoom percent as a scale in `0.1..=4` (10%–400%). `None` when it is not a finite number. +pub fn parse_zoom_percent(text: &str) -> Option { + let t = text.trim().trim_end_matches('%').trim(); + if t.is_empty() { + return None; + } + let v = t.parse::().ok().filter(|n| n.is_finite())?; + Some(((v / 100.0) as f32).clamp(0.1, 4.0)) +} + +/// Zoom dialog: a percent or Fit. Nothing changes until OK. +fn zoom_dialog(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { + if !d.fields.contains_key("mode") { + let shown = app.ui.zoom.unwrap_or(app.canvas_scale); + let pct = if shown.is_finite() { (shown * 100.0).round().clamp(10.0, 400.0) } else { 100.0 }; + d.fields.insert("mode".into(), if app.ui.zoom.is_none() { "fit".into() } else { "percent".into() }); + d.fields.insert("percent".into(), format!("{pct:.0}")); + } + let mut mode = d.get("mode", "percent"); + let mut percent = d.get("percent", "100"); + ui.label("Zoom to"); + if ui.radio(mode == "fit", "Fit").clicked() { + mode = "fit".into(); + } + for pct in ZOOM_PRESETS { + let on = mode == "percent" && percent == pct.to_string(); + if ui.radio(on, format!("{pct}%")).clicked() { + mode = "percent".into(); + percent = pct.to_string(); + } + } + ui.horizontal(|ui| { + ui.label("Percent:"); + let edit = ui.add(egui::TextEdit::singleline(&mut percent).desired_width(72.0)); + if edit.changed() { + mode = "percent".into(); + } + ui.label("%"); + }); + d.fields.insert("mode".into(), mode.clone()); + d.fields.insert("percent".into(), percent.clone()); + let (ok, cancel) = buttons(ui, "OK"); + if ok { + if mode == "fit" { + app.ui.zoom = None; + return true; + } + match parse_zoom_percent(&percent) { + Some(scale) => app.ui.zoom = Some(scale), + None => { + app.set_status("Enter a zoom percent"); + return false; + } + } + } + ok || cancel +} + /// Returns true when the dialog should close. fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let run = |app: &mut SlideApp, id: &str, p: Value| { @@ -321,18 +382,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { }); done } - "zoom" => { - for pct in [400, 200, 150, 100, 75, 66, 50, 33] { - if ui.radio(false, format!("{pct}%")).clicked() { - let _ = app.run("view.zoom", json!({"percent": pct})); - } - } - if ui.button("Fit").clicked() { - app.ui.zoom = None; - } - let (ok, cancel) = buttons(ui, "OK"); - ok || cancel - } + "zoom" => zoom_dialog(app, ui, d), "comment" => { let mut v = d.get("text", ""); ui.add(egui::TextEdit::multiline(&mut v).hint_text("Start a conversation").desired_rows(4).desired_width(320.0)); @@ -829,3 +879,25 @@ pub fn paint_logo(p: &egui::Painter, r: Rect) { Stroke::NONE, )); } + +#[cfg(test)] +mod tests { + use super::parse_zoom_percent; + + fn near(v: f32, want: f32) -> bool { + (v - want).abs() < 1.0e-4 + } + + #[test] + fn zoom_percent_accepts_a_number_and_clamps() { + assert!(parse_zoom_percent("150").is_some_and(|v| near(v, 1.5))); + assert!(parse_zoom_percent("150%").is_some_and(|v| near(v, 1.5))); + assert!(parse_zoom_percent(" 100 ").is_some_and(|v| near(v, 1.0))); + assert!(parse_zoom_percent("1").is_some_and(|v| near(v, 0.1))); + assert!(parse_zoom_percent("900").is_some_and(|v| near(v, 4.0))); + assert!(parse_zoom_percent("").is_none()); + assert!(parse_zoom_percent("fit").is_none()); + assert!(parse_zoom_percent("nan").is_none()); + assert!(parse_zoom_percent("inf").is_none()); + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index ec97e6e6..e62e6ae7 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -144,7 +144,8 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("view.guides", "Guides", Some("Alt+F9"), "{on?: bool}"), ("view.zoomIn", "Zoom In", Some("Cmd+="), "{}"), ("view.zoomOut", "Zoom Out", Some("Cmd+-"), "{}"), - ("view.zoom", "Zoom…", None, "{percent}"), + ("view.zoom", "Set Zoom", None, "{percent}"), + ("view.zoomDialog", "Zoom…", None, "{}"), ("view.fit", "Fit to Window", None, "{}"), ("view.pane", "Task Pane", None, "{pane: format|animation|selection|comments|designer|background|null}"), ("view.tab", "Ribbon Tab", None, "{tab}"), @@ -472,6 +473,7 @@ impl SlideApp { let pct = p.get("percent").and_then(Value::as_f64).ok_or("missing `percent`")?; self.ui.zoom = Some((pct as f32 / 100.0).clamp(0.1, 4.0)); } + "view.zoomDialog" => self.dialog = Some(dialogs::Dialog::new("zoom")), "view.fit" => self.ui.zoom = None, "view.pane" => { let pane = p.get("pane").and_then(Value::as_str).map(String::from); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 25398b45..d32202e0 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -195,6 +195,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<(&'static str, &'static str)>)> { ("Gridlines", "view.gridlines"), ("Guides", "view.guides"), ("-", ""), + ("Zoom…", "view.zoomDialog"), ("Zoom In", "view.zoomIn"), ("Zoom Out", "view.zoomOut"), ("Fit to Window", "view.fit"), diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 428eff0b..96e7a868 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1848,14 +1848,24 @@ fn view(app: &mut SlideApp, ui: &mut Ui) { } }); group(ui, |ui| { - let r = big_button(ui, Icon::ZoomGlass, "Zoom", true); - egui::Popup::menu(&r).show(|ui| { - for pct in [400, 200, 150, 100, 75, 66, 50, 33] { - if ui.button(format!("{pct}%")).clicked() { - let _ = app.run("view.zoom", json!({"percent": pct})); + let (main, arrow) = big_button_ex(ui, Icon::ZoomGlass, "Zoom", true, true, false); + if main.on_hover_text("Zoom").clicked() { + let _ = app.run("view.zoomDialog", json!({})); + } + if let Some(a) = arrow { + egui::Popup::menu(&a).show(|ui| { + for pct in [400, 200, 150, 100, 75, 66, 50, 33] { + if ui.button(format!("{pct}%")).clicked() { + let _ = app.run("view.zoom", json!({"percent": pct})); + ui.close(); + } } - } - }); + if ui.button("Fit").clicked() { + let _ = app.run("view.fit", json!({})); + ui.close(); + } + }); + } if big_button(ui, Icon::FitToWindow, "Fit to\nWindow", true).clicked() { app.ui.zoom = None; } diff --git a/crates/ui-egui/src/status.rs b/crates/ui-egui/src/status.rs index 8e915b71..79b6a5d2 100644 --- a/crates/ui-egui/src/status.rs +++ b/crates/ui-egui/src/status.rs @@ -42,7 +42,7 @@ pub fn status_bar(app: &mut SlideApp, ui: &mut Ui) { let pct = (app.ui.zoom.unwrap_or(app.canvas_scale) * 100.0).round(); let lbl = right.add(egui::Label::new(egui::RichText::new(format!("{pct}%")).size(12.0).color(t.text_dim)).sense(Sense::click())); if lbl.clicked() { - app.dialog = Some(crate::dialogs::Dialog::new("zoom")); + let _ = app.run("view.zoomDialog", json!({})); } if small_icon(&mut right, Icon::ZoomIn, "Zoom In", false).clicked() { let _ = app.run("view.zoomIn", json!({})); diff --git a/docs/roadmap.md b/docs/roadmap.md index 97953034..db23c6e5 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-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Zoom dialog; previous: minor, core-workflow alpha gate) · **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 @@ -109,5 +109,6 @@ plan above. | Date | Change | Summary | |---|---|---| +| 2026-10-14 | trivial | View: Zoom dialog (percent or Fit) | | 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..55b6bd63 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-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Zoom dialog; breadth stays 79%; previous: minor, hours to ~95%) · **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; @@ -230,8 +230,8 @@ Carried from the 2026-10-07 ROADMAP status and updated with what has landed sinc - **Slide show:** full screen, keyboard/mouse navigation, blank screens, go-to-slide, pen, presenter window, rehearse timings, custom shows, Set Up Show, Reading View; links and shape actions followed on click. -- **Views:** Normal, Outline, Slide Sorter, Notes Page, Reading View, Slide Master; zoom; - grayscale. +- **Views:** Normal, Outline, Slide Sorter, Notes Page, Reading View, Slide Master; zoom + (dialog: percent or Fit); grayscale. - **Review:** comments with replies and resolve, accessibility checker, a basic spelling list. - **Media:** MP3, AAC/M4A, ALAC, WAV, AIFF, CAF, FLAC, Ogg Vorbis and Opus audio; H.264, HEVC, VP9 and AV1 video in MP4/MOV/WebM/MKV with pure-Rust decoders; poster frames, trim, fades, @@ -276,6 +276,7 @@ scored done (16 open issues). | Date | Change | Summary | |---|---|---| +| 2026-10-14 | trivial | Zoom dialog takes a percent or Fit. Breadth stays 79%; the zoom checklist row stays D | | 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..2bb8f53a 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-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Zoom dialog: percent or Fit, applies on OK; previous: File tab / Backstage, #76) · **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…) | 27 (`crates/ui-egui/src/dialogs.rs`): Table, Slide Size, Header and Footer, Hyperlink, Set Up Show, Zoom (percent or Fit, applies on OK), 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 | ## Direct manipulation @@ -148,6 +148,7 @@ imitate Microsoft's. No pixel comparison against PowerPoint screenshots has been | Date | Change | Summary | |---|---|---| +| 2026-10-14 | trivial | Zoom dialog: percent or Fit, applied on OK. View ▸ Zoom opens it; the arrow keeps the presets | | 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 |