diff --git a/ROADMAP.md b/ROADMAP.md index 46a87add..49276273 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -128,6 +128,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-13 | The command palette skips commands that need a value, so searching for Export runs the export dialog (#79) | | 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: command palette Export (#79) | | 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/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index ce909b02..4e3ac8e5 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -331,6 +331,78 @@ pub(crate) fn current_shapes<'a>(doc: &'a deckcraft_model::Presentation, sel: &c } } +/// The command palette runs a command with `{}`. True when the params doc names a value that +/// call would have to supply (`path`, `rows`, `path? | data`), so the palette must not offer it. +/// A name ending in `?` is optional. Text after `→` is the result, not an input. +pub fn needs_arguments(params: &str) -> bool { + let input = params.split('→').next().unwrap_or(params); + let Some(start) = input.find('{') else { return false }; + let inside = input.get(start + 1..).unwrap_or(""); + let inside = inside.split('}').next().unwrap_or(inside); + split_fields(inside).iter().any(|f| field_requires(f.trim())) +} + +fn split_fields(s: &str) -> Vec<&str> { + let mut out = Vec::new(); + let mut start = 0; + let mut depth = 0i32; + let mut quote = false; + for (i, c) in s.char_indices() { + if quote { + if c == '"' { + quote = false; + } + continue; + } + match c { + '"' => quote = true, + '[' | '(' => depth += 1, + ']' | ')' => depth = depth.saturating_sub(1), + ',' if depth == 0 => { + out.push(s.get(start..i).unwrap_or("")); + start = i + c.len_utf8(); + } + _ => {} + } + } + out.push(s.get(start..).unwrap_or("")); + out +} + +/// A field needs a value unless every alternative (`path? | data`) is an optional parameter. +fn field_requires(field: &str) -> bool { + if field.is_empty() { + return false; + } + let alts: Vec<&str> = if field.contains(" | ") { field.split(" | ").map(str::trim).filter(|s| !s.is_empty()).collect() } else { vec![field] }; + let mut saw = false; + let mut all_optional = true; + for alt in alts { + let Some(optional) = param_optional(alt) else { continue }; + saw = true; + all_optional &= optional; + } + saw && !all_optional +} + +/// `Some(true)` when `alt` starts with an optional parameter name, `Some(false)` when required. +fn param_optional(alt: &str) -> Option { + let alt = alt.trim(); + let name_end = alt.find(|c: char| !c.is_ascii_alphanumeric()).unwrap_or(alt.len()); + let name = alt.get(..name_end).unwrap_or(""); + if name.is_empty() || !name.starts_with(|c: char| c.is_ascii_alphabetic()) { + return None; + } + let rest = alt.get(name_end..).unwrap_or("").trim_start(); + if rest.starts_with('?') { + Some(true) + } else if rest.is_empty() || rest.starts_with(':') { + Some(false) + } else { + None + } +} + /// Base64 (standard alphabet) for binary payloads in JSON params and results. pub fn base64_encode(data: &[u8]) -> String { const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; @@ -373,3 +445,32 @@ pub fn base64_decode(s: &str) -> Option> { } Some(out) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_palette_skips_a_command_that_needs_a_value() { + assert!(needs_arguments("{path}")); + assert!(needs_arguments("{path, format: png|jpeg|pptx, slide?: index}")); + assert!(needs_arguments("{path? | data: base64, name?, rect?}")); + assert!(needs_arguments("{rows, cols, rect?}")); + assert!(needs_arguments("{axis: horizontal|vertical, ids?}")); + assert!(needs_arguments("{percent}")); + assert!(!needs_arguments("{}")); + assert!(!needs_arguments("{on?: bool}")); + assert!(!needs_arguments("{theme?: name, size?: [w,h] pt, blank?: bool}")); + assert!(!needs_arguments("{uid?: one entry, else all}")); + assert!(!needs_arguments("{locked?: bool (default: toggle), ids?}")); + assert!(!needs_arguments("{} → [{name, label, category}]")); + assert!(!needs_arguments("{title?, subject?} → properties")); + assert!(!needs_arguments("{slides?: [index] | \"1-3,5\", includeHidden?}")); + let export = command_specs().iter().find(|c| c.id == "file.export").expect("export"); + let save = command_specs().iter().find(|c| c.id == "file.save").expect("save"); + let audio = command_specs().iter().find(|c| c.id == "insert.audio").expect("audio"); + assert!(needs_arguments(export.params)); + assert!(!needs_arguments(save.params)); + assert!(needs_arguments(audio.params)); + } +} diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 25398b45..e8ec5a3b 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -85,13 +85,20 @@ pub fn palette(app: &mut SlideApp, ctx: &egui::Context) { let r = ui.add(egui::TextEdit::singleline(&mut query).hint_text("Search commands…").desired_width(f32::INFINITY).font(theme::font(15.0))); r.request_focus(); let q = query.to_lowercase(); + // Commands that need a path or other value fail when the palette runs them with + // `{}`. The dialog command (`app.exportDialog`) stays; `file.export` does not. let mut items: Vec<(String, String, Option<&'static str>)> = app .session .commands() .into_iter() - .filter(|c| c.enabled) + .filter(|c| c.enabled && !deckcraft_engine::cmd::needs_arguments(c.params)) .map(|c| (c.id.to_string(), c.label.to_string(), c.shortcut)) - .chain(crate::UI_COMMANDS.iter().map(|c| (c.0.to_string(), c.1.to_string(), c.2))) + .chain( + crate::UI_COMMANDS + .iter() + .filter(|c| !deckcraft_engine::cmd::needs_arguments(c.3)) + .map(|c| (c.0.to_string(), c.1.to_string(), c.2)), + ) .filter(|(id, label, _)| q.is_empty() || label.to_lowercase().contains(&q) || id.to_lowercase().contains(&q)) .collect(); items.sort_by_key(|(_, l, _)| !l.to_lowercase().starts_with(&q)); diff --git a/docs/gaps.md b/docs/gaps.md index a2894b42..f1bafb58 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-10 · **Last updated:** 2026-10-13 · **Change:** trivial (command palette no longer offers Export without a path, #79; previous: minor, 3-D bevel, camera and face lighting rendered) · **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 @@ -205,7 +205,6 @@ Detail in [ui-parity.md](ui-parity.md). - **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, Replace Fonts, Action Settings, Print, Insert Object…). 8–14 h. -- **Command palette defaults to an Export entry that fails** (#79). 0.5 h. ## Hardware @@ -256,6 +255,7 @@ Detail in [localization-parity.md](localization-parity.md). | Date | Change | Summary | |---|---|---| +| 2026-10-13 | trivial | Command palette skips commands that need a value, so Export opens the dialog (#79) | | 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/target-app-parity.md b/docs/target-app-parity.md index 99653dc7..0c1be669 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-10 · **Last updated:** 2026-10-13 · **Change:** trivial (essentials discount no longer cites the palette Export failure, #79; percentages unchanged; previous: minor, hours to ~95% per audience) · **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; @@ -150,7 +150,7 @@ mainstream number leaves out. | Discount | Factor | Evidence | |---|---:|---| | Launch and stability | ×0.90 | Some Windows PCs can't start the app (#40, #59), and the workaround is an environment variable; Close loses work (#60) | -| Discoverability and UI clarity | ×0.95 | Familiar ribbon and tabs; no native menu bar, start-screen glitches, a command palette whose default entry fails (#79) | +| Discoverability and UI clarity | ×0.95 | Familiar ribbon and tabs; no native menu bar, start-screen glitches | | Opening files people send them | ×0.90 | `.pptx` opens, but real decks are untested and EMF pictures and PowerPoint icons can come in blank (#56) | 80.1 × 0.90 × 0.95 × 0.90 = **61.6 → ~62%**. @@ -276,6 +276,7 @@ scored done (16 open issues). | Date | Change | Summary | |---|---|---| +| 2026-10-13 | trivial | Essentials discoverability note: the command palette's Export failure (#79) is fixed; the ×0.95 discount stays for the menu bar and start screen | | 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..1a187df7 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-10 · **Last updated:** 2026-10-13 · **Change:** trivial (command search skips Export without a path, #79; previous: minor, 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) @@ -21,7 +21,7 @@ AGENTS.md §2). Dimension score: **~60% ready** (estimated), **30–50 h** to pa | File tab (Backstage): Home, New, Open, Info, Save, Save As, Print, Share, Export, Close, Account, Options | yes | yes (#76), see [File tab](#file-tab-backstage); no Share (cloud) | Video/GIF/CD exports, templates beyond the built-in themes | | Start screen: new, themes, templates, recent, open | yes, the same page as File › Home | yes: File › Home with Home, New, Open, Account and Options (#76) | | | Status bar: slide counter, language, notes, comments, views, zoom slider | yes | yes | | -| Command search (Tell Me) | yes | command palette | #79 default entry fails | +| Command search (Tell Me) | yes | command palette | | | Light / dark appearance | follows the system | light or dark; doesn't follow the system (#41) | 1–2 h | | One window per presentation, New Window, Arrange All | yes | tabs in one window; no New Window | 3–5 h | | Quick Access Toolbar customization | yes | no | P1 partial | @@ -138,7 +138,7 @@ imitate Microsoft's. No pixel comparison against PowerPoint screenshots has been | Native macOS menu bar from `menu_tree()`, filled out to PowerPoint's menu items | 3–5 | | 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 | +| Window chrome and system appearance (#41, #71) | 3–5 | | Grid Options, edit points UI, crop depth | 4–7 | | Multiple windows | 3–5 | | A screenshot comparison pass on the most-used tabs | 2–4 | @@ -148,6 +148,7 @@ imitate Microsoft's. No pixel comparison against PowerPoint screenshots has been | Date | Change | Summary | |---|---|---| +| 2026-10-13 | trivial | Command search no longer offers an Export command that fails without a path (#79) | | 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 |