Merge PR #593 from NikitkaRa: File: six original starter decks

# Conflicts:
#	crates/engine/src/lib.rs
This commit is contained in:
Brandon Thomas
2026-10-14 13:21:23 -04:00
10 changed files with 472 additions and 24 deletions
+3
View File
@@ -18,6 +18,7 @@
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Size and Position dialog; breadth stays 79%, ready stays ~55%; previous: minor, hours to ~95% per audience) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: previous and next comment; breadth stays 79%; previous: minor, hours to ~95%) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (View › New Window; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (six original starter decks; previous: minor, 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
@@ -169,6 +170,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-14 | Size and Position dialog: height, width, rotation, scale and lock aspect ratio in one undo step, from Shape Format › Size and the shape menu. Breadth stays 79% |
| 2026-10-14 | Review: Previous and Next comment walk every comment in the deck; the Comments pane lists every slide and highlights the one you landed on |
| 2026-10-13 | View › New Window opens a second window on the same presentation. The window has its own slide and shows edits made in the first window. Arrange All is not included |
| 2026-10-14 | File › New starter decks: Briefing, Lesson, Pitch, Field Notes, Quarterly and Evening Talk, original text on our themes (`file.new {template}`, `file.templates`). The large template gallery is still open |
| 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 |
@@ -211,6 +213,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-14 | trivial | Progress log: Size and Position dialog. Breadth stays 79%, ready stays ~55% |
| 2026-10-14 | trivial | Progress log: previous and next comment |
| 2026-10-13 | trivial | Progress log: View › New Window |
| 2026-10-14 | trivial | Progress log: six original starter decks |
| 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) |
+50 -8
View File
@@ -8,7 +8,16 @@ use crate::{DocState, EngineError, Result, Session};
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(noundo "file.new", "New Presentation", ["File"], Some("Cmd+N"), "{theme?: name, size?: [w,h] pt, blank?: bool}", always, new),
cmd!(
noundo "file.new",
"New Presentation",
["File"],
Some("Cmd+N"),
"{theme?: name, template?: starter name, size?: [w,h] pt, blank?: bool}",
always,
new
),
cmd!(query "file.templates", "Starter Templates", [], None, "{} → [{name, summary, theme, slides}]", always, templates),
cmd!(noundo "file.open", "Open…", ["File"], Some("Cmd+O"), "{path}", always, open),
cmd!(noundo "file.openBytes", "Open Data", [], None, "{name, data: base64}", always, open_bytes),
cmd!(noundo "file.save", "Save", ["File"], Some("Cmd+S"), "{path?, format?: deckcraft|pptx}", has_doc, save),
@@ -58,18 +67,51 @@ fn theme_named(name: &str) -> Option<deckcraft_model::Theme> {
deckcraft_model::theme::builtin_themes().into_iter().find(|t| t.name.eq_ignore_ascii_case(name))
}
fn new(s: &mut Session, p: &Value) -> Result<Value> {
let theme = str_param(p, "theme").and_then(theme_named).unwrap_or_default();
let size = p
.get("size")
fn slide_size(p: &Value) -> deckcraft_geom::Size {
p.get("size")
.and_then(Value::as_array)
.and_then(|a| Some(deckcraft_geom::Size::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?)))
.filter(|s| s.width >= 1.0 && s.height >= 1.0 && s.width < 5000.0 && s.height < 5000.0)
.unwrap_or(defaults::WIDE);
let doc = defaults::blank_presentation(size, theme, !bool_or(p, "blank", false));
.unwrap_or(defaults::WIDE)
}
fn templates(_s: &mut Session, _p: &Value) -> Result<Value> {
Ok(json!({
"templates": crate::templates::starters().iter().map(|t| json!({
"name": t.name,
"summary": t.summary,
"theme": t.theme,
"slides": t.slides.len(),
})).collect::<Vec<_>>(),
}))
}
fn new(s: &mut Session, p: &Value) -> Result<Value> {
let template = str_param(p, "template").filter(|t| !t.is_empty());
let theme_name = str_param(p, "theme").filter(|t| !t.is_empty());
if template.is_some() && theme_name.is_some() {
return Err(bad("file.new", "pass `theme` or `template`, not both"));
}
if template.is_some() && bool_or(p, "blank", false) {
return Err(bad("file.new", "a template is not a blank presentation"));
}
let size = slide_size(p);
let (doc, template_name) = if let Some(name) = template {
let doc = crate::templates::build(name, size)?;
let chosen = crate::templates::find(name).map(|t| t.name);
(doc, chosen)
} else {
let theme = theme_name.and_then(theme_named).unwrap_or_default();
(defaults::blank_presentation(size, theme, !bool_or(p, "blank", false)), None)
};
let slides = doc.slides.len();
let title = s.next_untitled();
let i = s.add_document(DocState::new(doc, None, title));
Ok(json!({"document": i}))
let mut out = json!({"document": i, "slides": slides});
if let Some(name) = template_name {
out["template"] = json!(name);
}
Ok(out)
}
/// Recognise and read presentation bytes (native, PPTX).
+1
View File
@@ -17,6 +17,7 @@ pub mod links;
pub mod recovery;
pub mod sample;
pub mod sounds;
pub mod templates;
pub mod tools;
use std::sync::Arc;
+337
View File
@@ -0,0 +1,337 @@
//! Original starter presentations for File › New.
//!
//! Each deck is a few slides of our own text on one of our themes. Nothing here is copied from
//! another product's templates.
use std::sync::Arc;
use deckcraft_geom::Size;
use deckcraft_model::text::TextBody;
use deckcraft_model::{Presentation, defaults};
use serde_json::Value;
use crate::Result;
use crate::cmd::{bad, slide::find_layout};
/// One slide of a starter: a layout, a title, text for each non-title placeholder in order, and
/// optional speaker notes.
pub struct SlideSeed {
pub layout: &'static str,
pub title: &'static str,
pub bodies: &'static [&'static str],
pub notes: &'static str,
}
/// A starter deck shown in the template gallery.
pub struct Starter {
pub name: &'static str,
pub summary: &'static str,
pub theme: &'static str,
pub slides: &'static [SlideSeed],
}
const fn seed(layout: &'static str, title: &'static str, bodies: &'static [&'static str], notes: &'static str) -> SlideSeed {
SlideSeed { layout, title, bodies, notes }
}
const STARTERS: &[Starter] = &[
Starter {
name: "Briefing",
summary: "A weekly check-in: what changed, what's stuck, and who does the next thing",
theme: "Harbor",
slides: &[
seed("title", "Weekly briefing", &["What changed, what's stuck, and what we do next"], "Open with what changed since last week."),
seed(
"titleAndContent",
"This week's agenda",
&["Shipped since last time\n\tOne concrete change\nStill open\n\tThe decision we need\nNext\n\tWho does it, and by when"],
"",
),
seed("sectionHeader", "Where we are", &["A short picture before the details"], ""),
seed(
"twoContent",
"Done and next",
&[
"Done\n\tThe change is in people's hands\n\tWe wrote down what we learned",
"Next\n\tPick the one risk to retire\n\tSay who owns it",
],
"",
),
seed("title", "See you next week", &["Bring one number and one question"], ""),
],
},
Starter {
name: "Lesson",
summary: "A short class that ends with the learner explaining the idea",
theme: "Meadow",
slides: &[
seed("title", "How a lock works", &["A short lesson for people new to the idea"], ""),
seed(
"titleAndContent",
"By the end",
&["You can say what a lock protects\n\tAnd what it does not\nYou can sketch one in three steps"],
"",
),
seed("sectionHeader", "The idea", &["One metaphor, then the real thing"], ""),
seed(
"titleAndContent",
"Three steps",
&["Name the thing being shared\nDecide who may change it\nLeave it so the next person can tell"],
"",
),
seed("title", "Try it", &["Explain a lock to someone who has never seen one"], ""),
],
},
Starter {
name: "Pitch",
summary: "A ten-minute conversation about one promise, not a tour of features",
theme: "Ember",
slides: &[
seed("title", "A quieter inbox", &["Ten minutes. One promise."], ""),
seed(
"titleAndContent",
"The mess",
&[
"People miss the one message that mattered\n\tIt arrived between newsletters\nThey spend the morning sorting\n\tAnd still feel behind",
],
"",
),
seed("sectionHeader", "A smaller promise", &["Show the one message. Hide the rest until asked."], ""),
seed(
"comparison",
"Today and after",
&[
"Today",
"The important note is buried\nFinding it takes a search",
"After",
"It sits at the top until you deal with it\nEverything else waits",
],
"",
),
seed("title", "The ask", &["Twenty people, two weeks, one question: did the right note surface?"], ""),
],
},
Starter {
name: "Field Notes",
summary: "What an outing set out to see, and what it actually found",
theme: "Coral Reef",
slides: &[
seed("title", "Saturday on the ridge", &["What we went to see, and what we found"], ""),
seed("titleAndContent", "Plan", &["Leave at dawn\n\tCold, clear, little wind\nWalk the west path\nBe back before dusk"], ""),
seed("sectionHeader", "What we saw", &["Notes taken on the spot"], ""),
seed(
"twoContent",
"Expected and found",
&[
"Expected\n\tA clear view from the ridge\n\tThe creek on the map",
"Found\n\tLow cloud on the ridge\n\tThe creek, and a better pencil",
],
"",
),
seed("title", "Pack list for next time", &["Warmer gloves. A second pencil. The map that shows the creek."], ""),
],
},
Starter {
name: "Quarterly",
summary: "A report meant to be read aloud in half an hour",
theme: "Paper",
slides: &[
seed("title", "Spring report", &["Read aloud in thirty minutes"], ""),
seed(
"titleAndContent",
"Headlines",
&["The waiting list got shorter\n\tBy the amount we promised\nTwo things slipped\n\tBoth have a new date"],
"",
),
seed("sectionHeader", "The numbers", &["Only the ones that change a decision"], ""),
seed("titleAndContent", "What we will stop", &["The report nobody reads\nThe meeting that repeats the report"], ""),
seed("title", "Questions we want", &["Where should the next quarter refuse work?"], ""),
],
},
Starter {
name: "Evening Talk",
summary: "A public talk the audience can check on the way home",
theme: "Nocturne",
slides: &[
seed("title", "Lights out", &["A talk about how cities sleep"], ""),
seed("titleAndContent", "Road map", &["Why the sky looks orange\nWhat a dark street is for\nOne change a block can make"], ""),
seed("sectionHeader", "Look up", &["The part you can check on the way home"], ""),
seed(
"titleAndContent",
"One change",
&["Shield the lamp so light goes down\n\tNot into bedroom windows\nLeave a path you can still walk"],
"",
),
seed("title", "Walk home", &["Count the lamps you can see from your pillow"], ""),
],
},
];
/// The starter gallery, in the order File › New shows it.
pub fn starters() -> &'static [Starter] {
STARTERS
}
/// Starters whose name, summary, theme or a slide title contains `query` (empty matches all).
pub fn matching(query: &str) -> Vec<&'static Starter> {
let q = query.trim().to_lowercase();
let hit = |text: &str| text.to_lowercase().contains(&q);
starters()
.iter()
.filter(|t| q.is_empty() || hit(t.name) || hit(t.summary) || hit(t.theme) || t.slides.iter().any(|slide| hit(slide.title)))
.collect()
}
pub fn find(name: &str) -> Option<&'static Starter> {
let name = name.trim();
starters().iter().find(|t| t.name.eq_ignore_ascii_case(name))
}
fn text_with_levels(text: &str) -> TextBody {
let mut body = TextBody::from_text(text);
for para in &mut body.paragraphs {
let tabs = para.runs.first().map(|r| r.text.chars().take_while(|c| *c == '\t').count()).unwrap_or(0);
if tabs > 0
&& let Some(run) = para.runs.first_mut()
{
run.text = run.text.trim_start_matches('\t').to_string();
para.level = tabs.min(8) as u8;
}
}
body
}
/// A new presentation filled with `name`'s slides. `size` is the slide size in points.
pub fn build(name: &str, size: Size) -> Result<Presentation> {
let starter = find(name).ok_or_else(|| {
let names = starters().iter().map(|t| t.name).collect::<Vec<_>>().join(", ");
bad("file.new", format!("no template `{name}`; use {names}"))
})?;
let theme = deckcraft_model::theme::builtin_themes()
.into_iter()
.find(|t| t.name.eq_ignore_ascii_case(starter.theme))
.ok_or_else(|| bad("file.new", format!("template `{}` names a missing theme `{}`", starter.name, starter.theme)))?;
let mut doc = defaults::blank_presentation(size, theme, false);
for seed in starter.slides {
let key = Value::String(seed.layout.to_string());
let layout = find_layout(&doc, &key, 0)
.ok_or_else(|| bad("file.new", format!("template `{}` uses an unknown layout `{}`", starter.name, seed.layout)))?;
let mut slide = defaults::new_slide(&mut doc, layout);
if !seed.title.is_empty()
&& let Some(sh) = slide.shapes.iter_mut().find(|sh| sh.ph_type().is_some_and(|k| k.is_title()))
{
sh.text = Some(text_with_levels(seed.title));
}
let mut bodies = seed.bodies.iter();
for sh in slide.shapes.iter_mut().filter(|sh| sh.ph_type().is_some_and(|k| !k.is_title())) {
let Some(text) = bodies.next() else { break };
if !text.is_empty() {
sh.text = Some(text_with_levels(text));
}
}
if bodies.next().is_some() {
return Err(bad("file.new", format!("template `{}` has more text than the `{}` layout holds", starter.name, seed.layout)));
}
if !seed.notes.is_empty() {
slide.notes = text_with_levels(seed.notes);
}
doc.slides.push(Arc::new(slide));
}
Ok(doc)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::Session;
fn body(doc: &Presentation, slide: usize, n: usize) -> (String, u8) {
let para_slide = &doc.slides[slide];
let text = para_slide
.shapes
.iter()
.filter(|sh| sh.ph_type().is_some_and(|k| !k.is_title()))
.nth(n)
.and_then(|sh| sh.text.as_ref())
.cloned()
.unwrap_or_default();
let level = text.paragraphs.get(1).map(|p| p.level).unwrap_or(0);
(text.text(), level)
}
#[test]
fn every_starter_opens_on_its_theme_and_is_a_clean_document() {
let mut s = Session::new();
for starter in starters() {
s.execute("file.new", &json!({"template": starter.name.to_lowercase()})).unwrap();
let st = s.active().unwrap();
assert_eq!(st.doc.masters[0].theme.name, starter.theme, "{}", starter.name);
assert_eq!(st.doc.slides.len(), starter.slides.len(), "{}", starter.name);
assert_eq!(st.doc.slides[0].title(), starter.slides[0].title, "{}", starter.name);
assert!(!st.is_dirty(), "{}", starter.name);
assert!(st.history.undo.is_empty(), "{}", starter.name);
assert_eq!(st.selection.slide, 0);
}
let listed = s.execute("file.templates", &json!({})).unwrap();
assert_eq!(listed["templates"].as_array().map(Vec::len), Some(starters().len()));
}
#[test]
fn briefing_keeps_notes_columns_and_outline_levels() {
let mut s = Session::new();
s.execute("file.new", &json!({"template": "Briefing", "size": [800.0, 450.0]})).unwrap();
let doc = &s.active().unwrap().doc;
assert_eq!(doc.slide_size.width, 800.0);
assert_eq!(doc.slides[0].notes_text(), "Open with what changed since last week.");
let (agenda, level) = body(doc, 1, 0);
assert!(agenda.contains("One concrete change"), "{agenda}");
assert_eq!(level, 1);
let (done, _) = body(doc, 3, 0);
let (next, _) = body(doc, 3, 1);
assert!(done.starts_with("Done"), "{done}");
assert!(next.starts_with("Next"), "{next}");
}
#[test]
fn pitch_comparison_fills_both_columns() {
let doc = build("Pitch", defaults::WIDE).unwrap();
assert_eq!(body(&doc, 3, 0).0, "Today");
assert!(body(&doc, 3, 1).0.contains("buried"));
assert_eq!(body(&doc, 3, 2).0, "After");
assert!(body(&doc, 3, 3).0.contains("top"));
}
#[test]
fn search_matches_name_summary_and_theme() {
assert_eq!(matching("ridge").iter().map(|t| t.name).collect::<Vec<_>>(), vec!["Field Notes"]);
assert_eq!(matching("nocturne").iter().map(|t| t.name).collect::<Vec<_>>(), vec!["Evening Talk"]);
assert_eq!(matching("").len(), starters().len());
assert!(matching("facet").is_empty());
}
#[test]
fn unknown_blank_and_theme_are_refused() {
let mut s = Session::new();
let unknown = s.execute("file.new", &json!({"template": "Ion"})).unwrap_err();
assert!(unknown.to_string().contains("Briefing"), "{unknown}");
let both = s.execute("file.new", &json!({"template": "Lesson", "theme": "Harbor"})).unwrap_err();
assert!(both.to_string().contains("not both"), "{both}");
let blank = s.execute("file.new", &json!({"template": "Lesson", "blank": true})).unwrap_err();
assert!(blank.to_string().contains("blank"), "{blank}");
assert!(s.active().is_none());
}
#[test]
fn a_starter_round_trips_through_pptx() {
let doc = build("Evening Talk", defaults::WIDE).unwrap();
let bytes = deckcraft_pptx::export(&doc).unwrap();
let back = deckcraft_pptx::import(&bytes).unwrap();
assert_eq!(back.slides.len(), doc.slides.len());
for (before, after) in doc.slides.iter().zip(back.slides.iter()) {
assert_eq!(after.title(), before.title());
}
assert!(back.slides[3].shapes.iter().any(|sh| sh.text.as_ref().is_some_and(|t| t.text().contains("bedroom"))));
}
}
+46 -7
View File
@@ -504,15 +504,14 @@ fn tile(ui: &mut Ui, label: &str, theme: Option<&deckcraft_model::Theme>, big: b
}
/// The built-in themes and the sample deck, filtered by `query` (after a blank presentation when
/// `blank`); at most `limit` theme tiles.
fn new_tiles(app: &mut SlideApp, ui: &mut Ui, query: &str, limit: usize, blank: bool) {
/// `blank`); at most `limit` theme tiles. False when nothing matched.
fn new_tiles(app: &mut SlideApp, ui: &mut Ui, query: &str, limit: usize, blank: bool) -> bool {
let q = query.trim().to_lowercase();
let themes: Vec<deckcraft_model::Theme> =
deckcraft_model::theme::builtin_themes().into_iter().filter(|th| q.is_empty() || th.name.to_lowercase().contains(&q)).collect();
let sample = q.is_empty() || "sample deck northern lights".contains(&q);
if themes.is_empty() && !sample {
dim(ui, &format!("No themes match “{}”.", query.trim()));
return;
return false;
}
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
@@ -538,6 +537,25 @@ fn new_tiles(app: &mut SlideApp, ui: &mut Ui, query: &str, limit: usize, blank:
}
}
});
true
}
/// Starter decks whose name, summary, theme or a slide title matches `query`, at most `limit` tiles.
fn template_tiles(app: &mut SlideApp, ui: &mut Ui, query: &str, limit: usize) -> bool {
let list = deckcraft_engine::templates::matching(query);
if list.is_empty() {
return false;
}
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
for starter in list.into_iter().take(limit) {
let theme = deckcraft_model::theme::builtin_themes().into_iter().find(|th| th.name == starter.theme);
if tile(ui, starter.name, theme.as_ref(), false).on_hover_text(starter.summary).clicked() {
let _ = app.run("file.new", json!({"template": starter.name}));
}
}
});
true
}
/// File name and folder of a recent path, split as text (nothing on disk is read).
@@ -655,6 +673,17 @@ fn home(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage, out: &mut Outcome)
ui.add_space(6.0);
new_tiles(app, ui, "", 5, true);
ui.add_space(22.0);
ui.horizontal(|ui| {
ui.label(RichText::new("Templates").font(theme::bold(16.0)).color(t.text));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if arrow_link(ui, "More templates") {
out.page = Some(Page::New);
}
});
});
ui.add_space(6.0);
template_tiles(app, ui, "", 3);
ui.add_space(22.0);
ui.horizontal(|ui| {
ui.label(RichText::new("Recent").font(theme::bold(16.0)).color(t.text));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
@@ -675,7 +704,7 @@ fn home(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage, out: &mut Outcome)
ui.hyperlink_to("Join the ArtCraft community on Discord", deckcraft_engine::links::DISCORD);
}
/// New: a blank presentation, then every theme with a search box.
/// New: a blank presentation, then every theme and starter template, with a search box.
fn new_page(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage) {
let t = Tokens::get(ui.ctx());
if tile(ui, "Blank Presentation", None, true).clicked() {
@@ -686,10 +715,20 @@ fn new_page(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage) {
let (r, _) = ui.allocate_exact_size(vec2(w, 1.0), Sense::hover());
ui.painter().line_segment([r.left_center(), r.right_center()], Stroke::new(1.0, t.separator));
ui.add_space(14.0);
search(ui, &mut bs.query, "Search for themes", 380.0);
search(ui, &mut bs.query, "Search for themes and templates", 380.0);
ui.add_space(14.0);
let q = bs.query.clone();
new_tiles(app, ui, &q, usize::MAX, false);
let themes = new_tiles(app, ui, &q, usize::MAX, false);
let templates = if q.trim().is_empty() || !deckcraft_engine::templates::matching(&q).is_empty() {
ui.add_space(18.0);
heading(ui, "Templates");
template_tiles(app, ui, &q, usize::MAX)
} else {
false
};
if !themes && !templates {
dim(ui, &format!("No themes or templates match “{}”.", q.trim()));
}
}
/// Open: Recent and Browse on the left; the recent presentations (with a search box) on the right.
+15
View File
@@ -179,6 +179,21 @@ fn every_page_renders() {
assert!(h.state_mut().run("app.backstage", json!({"page": "share"})).is_err(), "Share (cloud) isn't offered");
}
/// A starter template from File › New opens its deck and leaves the File tab.
#[test]
fn new_template_opens_a_starter_deck() {
let mut h = harness(new_app());
h.state_mut().run("app.backstage", json!({"page": "new"})).unwrap();
h.run_steps(3);
h.get_by_label("Briefing").click();
h.run_steps(3);
let doc = &h.state().session.active().unwrap().doc;
assert_eq!(doc.masters[0].theme.name, "Harbor");
assert_eq!(doc.slides[0].title(), "Weekly briefing");
assert_eq!(doc.slides.len(), 5);
assert!(h.state().backstage.is_none(), "the starter deck shows");
}
/// New and Home create presentations, which leaves the File tab.
#[test]
fn new_creates_a_presentation() {
+7 -2
View File
@@ -12,6 +12,7 @@
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (chart trendlines round-trip; previous: minor, 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 (Insert › Icons places a Lucide stroke set; 3-D models and stock content remain; 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 (Size and Position dialog; previous: minor, 3-D bevel, camera and face lighting) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (six original starter decks on File › New; 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
@@ -333,8 +334,11 @@ Detail in [localization-parity.md](localization-parity.md).
## Ecosystem
- **8 original themes, no template gallery.** PowerPoint offers dozens of designs online and
offline. 8–14 h for a larger original set (asset policy: our own designs only).
- **Six original starter decks; PowerPoint's large template gallery remains.** File › New and the
start screen offer Briefing, Lesson, Pitch, Field Notes, Quarterly and Evening Talk
(`file.new {template}`), each a few slides on one of our themes. Dozens of further offline
designs, and any online gallery, are still absent. 4–8 h for a larger original set (asset policy:
our own designs only).
- **No add-ins or VBA.** Out of scope by policy; MCP/CLI automation is the alternative.
## AI features
@@ -364,6 +368,7 @@ Detail in [localization-parity.md](localization-parity.md).
| 2026-10-14 | trivial | 3-D extrusions draw their sides under turned cameras |
| 2026-10-14 | minor | Insert › Icons places Lucide 1.56.0 stroke icons as custom shapes. The checklist row stays missing until 3-D models and stock content exist |
| 2026-10-14 | minor | Size and Position dialog landed (position, size, rotation, scale, lock aspect, one undo). The dialog gap remains Font, Bullets and Numbering, Find/Replace, Replace Fonts, Action Settings, Print, Insert Object and the rest of the ~60 |
| 2026-10-14 | minor | File › New has six original starter decks; a large template gallery is still open |
| 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 -2
View File
@@ -8,6 +8,7 @@
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (`corpus_media.rs` keeps 3-D models and animation bounce end; real-deck corpus still open) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (chart trendlines round-trip; the real-deck corpus stays open; previous: minor, core-workflow alpha gate added; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Review previous/next comment; previous: minor, core-workflow alpha gate) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (six original starter decks; the large template gallery stays after beta; previous: minor, 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
@@ -84,8 +85,8 @@ reference renders, a Windows tester with the affected GPUs, native speakers to r
SmartArt editing, chartex chart types, the remaining animation effects and motion paths, Notes and
Handout masters, edit points, text warp and 3-D, proofing dictionaries, Zoom, recording, video and
GIF export, legacy `.ppt` import and `.odp` (owner decision), GPU rasterization and hardware video
decode, the other 21 languages with RTL mirroring, a larger original template gallery, and local AI
features (owner decision on models). Itemized with hours in [gaps.md](gaps.md).
decode, the other 21 languages with RTL mirroring, a larger template gallery (six original starters
ship), and local AI features (owner decision on models). Itemized with hours in [gaps.md](gaps.md).
## Milestones
@@ -144,5 +145,6 @@ plan above.
| 2026-10-14 | trivial | Current focus: `corpus_media.rs` keeps 3-D models and animation bounce end; the real-deck corpus stays open |
| 2026-10-14 | trivial | Chart trendlines round-trip in an owned corpus slice; the real-deck corpus (item 2) stays open |
| 2026-10-14 | trivial | Review workflow: previous and next comment across the deck |
| 2026-10-14 | minor | After beta: six original starter decks exist; a larger template gallery remains |
| 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 |
+5 -2
View File
@@ -4,6 +4,7 @@
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (media remainder no longer lists icons: Insert › Icons is a Lucide stroke set; percentages unchanged; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (227 commands; breadth stays 79%, ready stays ~55%; previous: minor, hours to ~95% per audience) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (228 commands; previous/next comment; breadth stays 79%; previous: minor, hours to ~95%) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (six original starter decks; ecosystem stays ~25% until the gallery is large; previous: minor, 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;
@@ -75,7 +76,7 @@ The weights are what those users actually depend on, written down so the total c
| Hardware | 5% | ~40% | 25–45 | [hardware-parity.md](hardware-parity.md) | GPU-composited window (wgpu), but CPU slide raster, software-only video decode, presenter view not placed on the second display, no pen pressure, no pinch zoom |
| Platforms | 5% | ~80% | 5–10 | [ROADMAP.md](../ROADMAP.md) | Ahead in reach: macOS, Windows x64/x86/arm64, Linux (5 formats), FreeBSD and the web, all from one release workflow. Held back by the Windows GPU crashes and Linux/Windows clipboard gaps |
| Localization | 5% | ~8% | 55–90 + native review | [localization-parity.md](localization-parity.md) | English only; no string catalog. PowerPoint for Mac ships 26 languages. Bidi and Arabic shaping in slide text exist |
| Ecosystem (templates, add-ins) | 3% | ~25% | 10–20 | [gaps.md](gaps.md) | 8 original themes vs PowerPoint's large template gallery; no add-ins or VBA (out of scope by policy) |
| Ecosystem (templates, add-ins) | 3% | ~25% | 10–20 | [gaps.md](gaps.md) | 8 original themes and 6 original starter decks; PowerPoint's large template gallery remains; no add-ins or VBA (out of scope by policy) |
| AI features | 2% | ~10% | 20–40 (owner decision on models) | [gaps.md](gaps.md) | No Designer, Copilot, Speaker Coach, live subtitles or Translate (all Microsoft cloud services). Agents drive every command over MCP instead |
| **Total** | 100% | **~55%** (Σ weight × ready = 55.3) | **~365–650** | | |
| Automation (✱, not weighted) | | ahead | 0 | [mcp.md](mcp.md), [control-protocol.md](control-protocol.md) | Every command is scriptable from the CLI, a JSON control channel and MCP, headless or against the running app; PowerPoint for Mac has AppleScript and VBA only |
@@ -214,7 +215,8 @@ Carried from the 2026-10-07 ROADMAP status and updated with what has landed sinc
inheritance (slide → layout → master → theme), sections, notes, comments, custom shows,
header/footer fields, embedded media, unlimited undo on shared slide snapshots.
- **Themes:** 8 original themes with colour and font schemes, custom colours and fonts,
backgrounds (solid, gradient, picture, pattern), slide size presets.
backgrounds (solid, gradient, picture, pattern), slide size presets. File › New also offers 6
original starter decks (Briefing, Lesson, Pitch, Field Notes, Quarterly, Evening Talk).
- **Shapes:** ~180 preset geometries with adjustment handles, text boxes, pictures (crop,
corrections, animated GIFs that play and pause), tables with styles, charts (column, bar, line,
area, pie, doughnut, scatter and more), basic SmartArt, groups, connectors glued to sites,
@@ -284,6 +286,7 @@ scored done (16 open issues).
| 2026-10-14 | minor | Media remainder: Insert › Icons places Lucide stroke icons. 3-D models and stock content remain. Headline percentages unchanged; the checklist row stays missing |
| 2026-10-14 | trivial | Size and Position dialog. Command count 226 → 227. Breadth stays 79%, ready stays ~55%, UI stays ~60% |
| 2026-10-14 | trivial | 228 commands; Review lists previous and next comment. Breadth stays 79%; the comments checklist row stays D |
| 2026-10-14 | minor | Ecosystem evidence: six original starter decks. The dimension stays ~25% because PowerPoint's large gallery is still absent |
| 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 |
+4 -3
View File
@@ -9,7 +9,7 @@
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Comments pane lists every slide; Review Previous/Next; 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 (View › New Window opens a second window on the same presentation; 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)
> **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)
How DeckCraft looks and feels next to PowerPoint: the shell, the ribbon, menus, panes, dialogs,
direct manipulation, keyboard and precision. Observations of PowerPoint are kept locally in
@@ -49,8 +49,8 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`).
| Page | PowerPoint | DeckCraft |
|---|---|---|
| Home | Prompt box (Copilot), New row of themes with More themes, Recent / Favorites / Shared with Me with a file search, More presentations | DeckCraft's mark, Blank Presentation and the first themes with More themes →, Recent with a file search and More presentations → (to Open). No Copilot prompt, Favorites or Shared with Me |
| New | Blank Presentation, template search with suggested searches, gallery of online templates and themes | Blank Presentation, search box, the built-in themes and the sample deck (`file.new {theme}`). No online templates |
| Home | Prompt box (Copilot), New row of themes with More themes, Recent / Favorites / Shared with Me with a file search, More presentations | DeckCraft's mark, Blank Presentation and the first themes with More themes →, the first starter decks with More templates →, Recent with a file search and More presentations → (to Open). No Copilot prompt, Favorites or Shared with Me |
| New | Blank Presentation, template search with suggested searches, gallery of online templates and themes | Blank Presentation, search box, the built-in themes, six original starter decks (`file.new {template}`) and the sample deck (`file.new {theme}`). No online templates |
| Open | Recent, Shared with Me, OneDrive, This PC, Add a Place, Browse; Presentations / Folders; Recover Unsaved Presentations | Recent and Browse (`app.openDialog`), recent presentations with a search box (`file.open`), Recover Unsaved Presentations (`file.recovery.open`). No cloud places or Folders tab |
| Info | Name and path, Upload / Share / Copy path / Open file location; Protect, Inspect, Version History, Manage Presentation; Properties, Related Dates, Related People | Name and path, Copy path, Open file location; Inspect Presentation (Accessibility Checker), Manage Presentation (Revert); Size, Slides, Hidden slides and editable Title, Subject, Tags, Categories, Company, Comments, Author (`file.info`, `file.properties`). No Protect, Version History or dates |
| Save | Saves; a presentation never saved goes to Save As | Same (`file.save`) |
@@ -168,6 +168,7 @@ on the slide as shapes. No pixel comparison against PowerPoint screenshots has b
| 2026-10-14 | minor | Size and Position dialog on Shape Format › Size and the shape context menu. Breadth stays 79%; UI ready stays ~60% |
| 2026-10-14 | trivial | Comments pane lists every slide; Review ▸ Previous and Next walk the deck |
| 2026-10-13 | minor | View › New Window: a second window on the same presentation, with its own slide; Arrange All is still missing |
| 2026-10-14 | minor | File › Home and New show six original starter decks; online templates are still absent |
| 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 |