Merge PR #536 from NikitkaRa: UI: Grid Options sets spacing, snap and the on-screen grid

# Conflicts:
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/lib.rs
This commit is contained in:
Brandon Thomas
2026-10-14 14:39:01 -04:00
10 changed files with 245 additions and 14 deletions
+3
View File
@@ -35,6 +35,7 @@
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: chart trendlines are drawn; previous: trivial, chart trendlines round-trip; hours to ~95% per audience added; ready ~55%; stage stays alpha; full re-measure against PowerPoint for Mac 16.113.4) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: Font dialog; previous: minor, hours to ~95% per audience added; ready stays ~55%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: Paragraph dialog; previous: trivial, Font dialog; ready stays ~55%; stage stays alpha) · **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
@@ -213,6 +214,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-14 | Trim Media: start, end and fades open from the selected clip, so OK no longer resets them to zero |
| 2026-10-14 | Paragraph dialog (Home › Paragraph launcher): alignment, first-line and hanging indents, spacing before and after, line spacing, and a Tabs dialog, each applied in one undo step. Breadth stays 79% |
| 2026-10-14 | Font dialog (Home › Font, the shape menu, Cmd+T): family, size, bold, italic, underline, colour, strikethrough, baseline, caps and character spacing, applied in one undo step. Breadth stays 79% |
| 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 |
@@ -281,6 +283,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-14 | trivial | Progress log: Trim Media dialog follows the selection |
| 2026-10-14 | trivial | Progress log: Paragraph dialog |
| 2026-10-14 | trivial | Progress log: Font dialog |
| 2026-10-13 | trivial | Progress log: Grid Options dialog |
| 2026-10-13 | trivial | Progress log: wild text cases |
| 2026-10-13 | trivial | Progress log: slide show for interactive decks |
| 2026-10-12 | trivial | Progress log: File tab (Backstage, #76) |
+53 -12
View File
@@ -195,18 +195,10 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
painter.extend(dash);
}
}
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();
@@ -1064,6 +1056,43 @@ 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<f32> {
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 {
#![allow(clippy::unwrap_used, clippy::expect_used)]
@@ -1155,6 +1184,18 @@ mod tests {
h.run_steps(2);
assert_near(mid, slide_point(&h, c), "zoom out");
}
#[test]
fn grid_step_follows_spacing_and_rejects_unusable_values() {
let step = super::grid_line_step(72.0, 0.5).unwrap_or(0.0);
assert!((step - 36.0).abs() < 1e-3, "{step}");
assert!(super::grid_line_step(6.0, 1.0).is_some());
assert!(super::grid_line_step(0.0, 1.0).is_none());
assert!(super::grid_line_step(f64::NAN, 1.0).is_none());
assert!(super::grid_line_step(1_000.0, 1.0).is_none());
assert!(super::grid_line_step(6.0, 0.0).is_none());
assert!(super::grid_line_step(6.0, 0.1).is_none(), "a 6 pt grid at 10% zoom is finer than one screen point");
}
}
#[cfg(test)]
+39
View File
@@ -50,6 +50,7 @@ fn title(id: &str) -> &'static str {
"tabs" => "Tabs",
"about" => "About DeckCraft",
"preferences" => "Preferences",
"gridOptions" => "Grid Options",
"export" => "Export",
"symbol" => "Symbol",
"photoAlbum" => "Photo Album",
@@ -1901,6 +1902,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;
@@ -2265,6 +2267,43 @@ fn date_time_dialog(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
cancel
}
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' => '⁰',
+129
View File
@@ -177,6 +177,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", "Set Zoom", None, "{percent}"),
@@ -287,6 +288,18 @@ pub struct SlideApp {
pub(crate) slides_focused: bool,
}
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<Option<bool>, 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();
@@ -538,6 +551,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];
@@ -1581,6 +1637,79 @@ mod tests {
assert!(app.doc_window.is_none(), "closing the presentation closes the extra window");
assert!(app.session.active().is_none());
}
#[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| {
+1
View File
@@ -213,6 +213,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…", "view.zoomDialog"),
("Zoom In", "view.zoomIn"),
+3
View File
@@ -2160,6 +2160,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;
+3
View File
@@ -27,6 +27,7 @@
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (chart trendlines are drawn; previous: minor, chart trendlines round-trip; 3-D bevel, camera and face lighting rendered; IME composition and CJK interface text landed, #101; core-workflow alpha gate added; stage stays alpha; 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-14 · **Last updated:** 2026-10-14 · **Change:** minor (Font dialog; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Paragraph dialog; previous: minor, Font dialog) · **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
@@ -337,6 +338,7 @@ Detail in [ui-parity.md](ui-parity.md).
2–4 h.
- **Icon consistency across Craft apps** (#73, #56). 2–4 h; coordinate in craftrules.
- **Dialogs**: 28 dialogs vs PowerPoint's ~60 (Font, 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.
- **Dialogs**: 28 dialogs vs PowerPoint's ~60 (Font, Bullets and Numbering, Find/Replace dialog,
Replace Fonts, Action Settings, Print, Insert Object…). Size and Position is one of the 28. 8–14 h.
@@ -458,6 +460,7 @@ Detail in [localization-parity.md](localization-parity.md).
| 2026-10-14 | minor | Slide Master Preserve and Title/Footers placeholders. Checklist letters unchanged (breadth stays 79%); dialogs stay 27 |
| 2026-10-14 | minor | Paragraph dialog sets alignment, indentation, spacing and line spacing in one step; its Tabs… button opens a standalone Tabs dialog |
| 2026-10-14 | minor | Font dialog sets typeface, size, style, underline, colour, strikethrough, baseline, caps and character spacing in one step |
| 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-13 | minor | Interactive decks: triggers, return-to-slide state and click rules landed in the slide show; the remaining pieces are one new entry under Features |
| 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 |
+4
View File
@@ -16,6 +16,7 @@
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (custom show playback; previous: trivial, presenter timer) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (go to slide shows the typed number; previous: trivial, custom show playback) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (chart trendlines are drawn; the charts gap stays open for the data grid, chartex and secondary-axis export; previous: trivial, chart trendlines round-trip; the real-deck corpus stays open; core-workflow alpha gate added; stage stays alpha) · **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
@@ -117,6 +118,8 @@ From the local execution plan (`plan/execution-plan.md`, gitignored). Status as
| M10 | Views | done | grid options, New Window |
| M9 | SmartArt, equations, WordArt | partly | SmartArt text pane and layouts, equations (PR #55), text warps |
| M10 | Views | done | grid options, Arrange All |
| M9 | SmartArt, equations, WordArt | partly | SmartArt text pane and layouts, equations (PR #55), text warps |
| 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 | |
@@ -164,5 +167,6 @@ plan above.
| 2026-10-14 | trivial | Present workflow notes custom show playback |
| 2026-10-14 | trivial | Present workflow notes presenter timer pause/reset, clock and notes size. Display placement stays open |
| 2026-10-14 | trivial | Present workflow notes See All Slides and zoom |
| 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 |
+3
View File
@@ -19,6 +19,7 @@
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Slide Master Preserve and Title/Footers; automation counted at 236 commands; the menu measurement and the d556ff9 snapshot still count 226; percentages unchanged; previous: hours to ~95% per audience, ready 56% → 55%) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (227 commands; previous: minor, hours to ~95% per audience added; ready stays ~55%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (229 commands; previous: trivial, 227 commands; ready stays ~55%; stage stays alpha) · **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;
@@ -201,6 +202,7 @@ it is the Features row of the dimension table. Automation and Platforms are scor
| 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, Arrange All | [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, warp adjust handles, WordArt depth | [ui-parity.md](ui-parity.md) |
| Views | 4 | 90% | ~80% | 3–6 | Snap to grid and grid options, New Window | [ui-parity.md](ui-parity.md) |
@@ -346,6 +348,7 @@ scored done (16 open issues).
| 2026-10-14 | minor | Slide Master Preserve and Title/Footers placeholders. Automation in "what works today" is 236 commands; the menu-bar measurement and the d556ff9 snapshot still count 226. Percentages unchanged |
| 2026-10-14 | trivial | Command count 227 → 229 (Paragraph dialog and Tabs). Breadth and ready percentages unchanged |
| 2026-10-14 | trivial | Command count 226 → 227 (Font dialog). Breadth and ready percentages unchanged |
| 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 |
+7 -2
View File
@@ -20,6 +20,7 @@
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Design ▸ Effects; dialogs stay 27) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Slide Master Preserve and Title/Footers placeholders; previous: File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Trim Media dialog follows the selection; previous: File tab / Backstage, #76) · **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-14 · **Change:** minor (File › New starter decks; previous: minor, text editing keys observed in PowerPoint for Windows, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Slide Sorter sections: add, rename, collapse, drag a whole section; previous: text editing keys, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
@@ -111,6 +112,7 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`).
| 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 (start, end and fades follow the selection), 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`): Font, 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…) | 29 (`crates/ui-egui/src/dialogs.rs`): Font, Paragraph, Tabs, Table, Slide Size, Header and Footer, Hyperlink, Set Up Show, Zoom, Comment, Alt Text, Outline, Rename (section, shape, layout), 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
@@ -120,7 +122,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 |
@@ -199,8 +201,10 @@ on the slide as shapes. No pixel comparison against PowerPoint screenshots has b
| Window chrome and system appearance (#41, #71) | 3–5 |
| Grid Options, edit points UI, crop depth | 4–7 |
| Arrange All (New Window is one extra window) | 2–3 |
| 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
@@ -230,6 +234,7 @@ on the slide as shapes. No pixel comparison against PowerPoint screenshots has b
| 2026-10-14 | trivial | Trim Media dialog: start, end and fades follow the selected clip |
| 2026-10-14 | minor | Paragraph dialog from the Home › Paragraph launcher; Tabs… opens a standalone Tabs dialog |
| 2026-10-14 | minor | Font dialog from Home › Font, the shape menu and Cmd+T |
| 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 |