diff --git a/ROADMAP.md b/ROADMAP.md index 46a87add..f6505018 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: Trim Media dialog follows the selection; 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 | Trim Media: start, end and fades open from the selected clip, so OK no longer resets them to zero | | 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: Trim Media dialog follows the selection | | 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..85a5f900 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -117,6 +117,57 @@ 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) } +/// Seconds for a Trim Media field. Three decimal places, trailing zeros dropped, so the text +/// round-trips the millisecond value the clip stores. +fn ms_as_seconds(ms: u32) -> String { + let s = format!("{:.3}", f64::from(ms) / 1000.0); + let trimmed = s.trim_end_matches('0').trim_end_matches('.'); + if trimmed.is_empty() { "0".to_string() } else { trimmed.to_string() } +} + +/// Parse a seconds field back to milliseconds. Blank, junk and negatives become 0; the result is +/// rounded so `0.333` is 333 ms, not 332. +fn seconds_text_to_ms(text: &str) -> u32 { + let v = text.trim().parse::().ok().filter(|v| v.is_finite()).unwrap_or(0.0); + let ms = (v.max(0.0) * 1000.0).round(); + if ms >= f64::from(u32::MAX) { u32::MAX } else { ms as u32 } +} + +/// Start, end trim, fade in and fade out, as the Trim Media dialog shows them. +struct TrimSeed { + start: String, + end: String, + fade_in: String, + fade_out: String, +} + +impl TrimSeed { + fn zeros() -> Self { + TrimSeed { start: "0".into(), end: "0".into(), fade_in: "0".into(), fade_out: "0".into() } + } + fn from_clip(m: &deckcraft_model::MediaClip) -> Self { + TrimSeed { + start: ms_as_seconds(m.trim_start_ms), + end: ms_as_seconds(m.trim_end_ms), + fade_in: ms_as_seconds(m.fade_in_ms), + fade_out: ms_as_seconds(m.fade_out_ms), + } + } +} + +/// The first selected audio or video. Other shapes in the selection are skipped. No media means +/// the old zeros, which OK then stores. +fn trim_seed(st: Option<&deckcraft_engine::DocState>) -> TrimSeed { + let Some(st) = st else { return TrimSeed::zeros() }; + st.selected_shapes() + .into_iter() + .find_map(|sh| match &sh.kind { + deckcraft_model::ShapeKind::Media(m) => Some(TrimSeed::from_clip(m)), + _ => None, + }) + .unwrap_or_else(TrimSeed::zeros) +} + /// 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| { @@ -646,16 +697,22 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { ok || cancel } "trim" => { - field(ui, d, "Start time (s):", "start", "0"); - field(ui, d, "End trim (s):", "end", "0"); - field(ui, d, "Fade in (s):", "fin", "0"); - field(ui, d, "Fade out (s):", "fout", "0"); + let seed = trim_seed(app.session.active()); + field(ui, d, "Start time (s):", "start", &seed.start); + field(ui, d, "End trim (s):", "end", &seed.end); + field(ui, d, "Fade in (s):", "fin", &seed.fade_in); + field(ui, d, "Fade out (s):", "fout", &seed.fade_out); let (ok, cancel) = buttons(ui, "OK"); if ok { run( app, "media.options", - json!({"trimStart": (num(d, "start", 0.0) * 1000.0) as u64, "trimEnd": (num(d, "end", 0.0) * 1000.0) as u64, "fadeIn": (num(d, "fin", 0.0) * 1000.0) as u64, "fadeOut": (num(d, "fout", 0.0) * 1000.0) as u64}), + json!({ + "trimStart": seconds_text_to_ms(&d.get("start", &seed.start)), + "trimEnd": seconds_text_to_ms(&d.get("end", &seed.end)), + "fadeIn": seconds_text_to_ms(&d.get("fin", &seed.fade_in)), + "fadeOut": seconds_text_to_ms(&d.get("fout", &seed.fade_out)), + }), ); } ok || cancel @@ -829,3 +886,50 @@ pub fn paint_logo(p: &egui::Painter, r: Rect) { Stroke::NONE, )); } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use deckcraft_model::{MediaClip, Presentation, ShapeId, ShapeKind}; + + use super::{ms_as_seconds, seconds_text_to_ms, trim_seed}; + + #[test] + fn trim_seconds_round_trip_the_stored_milliseconds() { + for ms in [0, 1, 250, 333, 1000, 1500, 60_000] { + let text = ms_as_seconds(ms); + assert_eq!(seconds_text_to_ms(&text), ms, "{text}"); + } + assert_eq!(seconds_text_to_ms(""), 0); + assert_eq!(seconds_text_to_ms("nope"), 0); + assert_eq!(seconds_text_to_ms("-2"), 0); + assert_eq!(seconds_text_to_ms("1.5"), 1500); + assert_eq!(seconds_text_to_ms("999999999"), u32::MAX); + } + + #[test] + fn trim_dialog_reads_the_selected_clip() { + assert_eq!(trim_seed(None).start, "0"); + let mut doc = Presentation::default(); + let media_id = { + let slide = Arc::make_mut(&mut doc.slides[0]); + let title = slide.shapes[0].id; + let clip = MediaClip { trim_start_ms: 1500, trim_end_ms: 250, fade_in_ms: 333, fade_out_ms: 1000, ..Default::default() }; + let mut media = slide.shapes[0].clone(); + media.id = ShapeId(900); + media.kind = ShapeKind::Media(clip); + slide.shapes.push(media); + (title, ShapeId(900)) + }; + let mut st = deckcraft_engine::DocState::new(doc, None, "t".into()); + st.selection.shapes = vec![media_id.0]; + assert_eq!(trim_seed(Some(&st)).start, "0", "a title shape is not a clip"); + st.selection.shapes = vec![media_id.0, media_id.1]; + let seed = trim_seed(Some(&st)); + assert_eq!(seed.start, "1.5"); + assert_eq!(seed.end, "0.25"); + assert_eq!(seed.fade_in, "0.333"); + assert_eq!(seed.fade_out, "1"); + } +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index c403f05e..1064be49 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 (Trim Media dialog follows the selection; 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, 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 | ## Direct manipulation @@ -148,6 +148,7 @@ imitate Microsoft's. No pixel comparison against PowerPoint screenshots has been | Date | Change | Summary | |---|---|---| +| 2026-10-14 | trivial | Trim Media dialog: start, end and fades follow the selected clip | | 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 |