From 2702b48530a17057626472cdc66f5b65b724f41e Mon Sep 17 00:00:00 2001 From: Nikita Rakitin Date: Wed, 14 Oct 2026 04:26:02 +0300 Subject: [PATCH] Show: show the slide number while typing it --- ROADMAP.md | 2 + crates/ui-egui/src/show.rs | 96 ++++++++++++++++++++++++++++++++++++-- docs/control-protocol.md | 4 +- docs/roadmap.md | 5 +- docs/slideshow-parity.md | 5 +- docs/target-app-parity.md | 5 +- 6 files changed, 104 insertions(+), 13 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 4adde1a7..a333388a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -128,6 +128,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-14 | During a show, a typed slide number is shown and Enter goes to that slide. Breadth stays 79% | | 2026-10-14 | A custom show plays only its slides: Show in the Custom Shows dialog, Set Up Slide Show, or a click action. Breadth stays 79% | | 2026-10-14 | Presenter view pauses and resets its timer (T, R) and changes notes size (A-/A+, brackets). The clock shows local time. Second-display placement stays with #433. Breadth stays 79% | | 2026-10-14 | See All Slides and zoom during a show: a thumbnail grid (G, or All), click to jump, and click-to-zoom at 2× (Z). Presenter view has Black, White, All slides and Zoom. Breadth stays 79% | @@ -153,6 +154,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | Change | Summary | |---|---|---| +| 2026-10-14 | trivial | Progress log: go to slide shows the typed number. Breadth stays 79% | | 2026-10-14 | trivial | Progress log: custom show playback. Breadth stays 79% | | 2026-10-14 | trivial | Progress log: presenter timer pause/reset, clock and notes size. Breadth stays 79% | | 2026-10-14 | trivial | Progress log: See All Slides and zoom during a show. Breadth stays 79% | diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs index 6b472c9d..977240c4 100644 --- a/crates/ui-egui/src/show.rs +++ b/crates/ui-egui/src/show.rs @@ -37,7 +37,9 @@ pub struct Show { thumbs: Vec<(usize, TextureHandle)>, pen: bool, strokes: Vec>, + /// Digits typed for Go to Slide, shown until Enter, Esc, or a short pause. goto: String, + goto_at: f64, started: bool, show_start: f64, /// Elapsed time in presenter view. Pausing it does not pause the show. @@ -106,6 +108,7 @@ impl Show { pen: false, strokes: vec![], goto: String::new(), + goto_at: 0.0, started: false, show_start: 0.0, timer: PresenterTimer::start(0.0), @@ -303,7 +306,8 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { for (k, m) in keys { use egui::Key::*; if matches!(k, Escape | Minus) { - if !show.back_out() { + let cancelled = k == Escape && show.clear_goto(); + if !cancelled && !show.back_out() { keep = false; } continue; @@ -323,15 +327,21 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { } } Enter => { - if let Ok(n) = show.goto.parse::() { - actions.push(show.state.goto(&doc, n.saturating_sub(1))); - show.goto.clear(); + if !show.goto.is_empty() { + if let Some(i) = show.take_goto() + && i < doc.slides.len() + { + actions.push(show.state.goto(&doc, i)); + } } else if show.ended { keep = false; } else { actions.push(show.state.next(&doc)); } } + Backspace if !show.goto.is_empty() => { + show.goto.pop(); + } ArrowLeft | ArrowUp | Backspace | PageUp | P if !(k == P && m.command) => { show.ended = false; actions.push(show.state.prev(&doc)); @@ -358,11 +368,14 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { OpenBracket if plain(m) => show.nudge_notes(-NOTES_STEP), CloseBracket if plain(m) => show.nudge_notes(NOTES_STEP), Num0 | Num1 | Num2 | Num3 | Num4 | Num5 | Num6 | Num7 | Num8 | Num9 => { - show.goto.push_str(k.name()); + if let Some(d) = k.name().chars().next() { + show.push_slide_number(d, now); + } } _ => {} } } + show.expire_goto(now); let on_slide = !show.grid && !show.pen && !show.ended && show.blank.is_none() && show.zoom.is_none() && !show.zoom_arm && !show.reading_bar_hit(&ctx); let gif_click = if clicked && on_slide { gif_hit(&ctx, ui, &show, &doc, app) } else { None }; @@ -640,6 +653,14 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { if let Some(c) = show.blank { painter.rect_filled(full, CornerRadius::ZERO, c); } + if !show.goto.is_empty() { + let label = format!("Go to slide {}", show.goto); + let w = (28.0 + label.len() as f32 * 10.0).clamp(160.0, full.width() - 32.0); + let r = Rect::from_min_size(pos2(full.min.x + 16.0, full.max.y - 96.0), vec2(w, 36.0)); + painter.rect_filled(r, CornerRadius::same(6), Color32::from_black_alpha(190)); + painter.text(r.center(), Align2::CENTER_CENTER, label, theme::font(18.0), Color32::WHITE); + ctx.request_repaint_after(std::time::Duration::from_millis(200)); + } // Pen. let resp = ui.interact(full, ui.id().with("show_pen"), if show.pen { Sense::drag() } else { Sense::hover() }); if show.pen { @@ -917,6 +938,39 @@ impl Show { self.zoom.map(|z| (z.nx, z.ny, z.scale)) } + fn push_slide_number(&mut self, digit: char, now: f64) { + if !digit.is_ascii_digit() || self.goto.len() >= GOTO_DIGITS { + return; + } + self.goto.push(digit); + self.goto_at = if now.is_finite() { now } else { 0.0 }; + } + + /// 1-based slide number, or `None` for 0 or a non-number. The buffer is cleared either way. + fn take_goto(&mut self) -> Option { + let raw = std::mem::take(&mut self.goto); + raw.parse::().ok().and_then(|n| n.checked_sub(1)) + } + + fn expire_goto(&mut self, now: f64) { + if self.goto.is_empty() || !now.is_finite() || !self.goto_at.is_finite() { + return; + } + if now - self.goto_at > GOTO_HOLD { + self.goto.clear(); + } + } + + /// Esc drops a number in progress. True when it did. + fn clear_goto(&mut self) -> bool { + if self.goto.is_empty() { + false + } else { + self.goto.clear(); + true + } + } + fn nudge_notes(&mut self, delta: f32) { let next = self.notes_px + delta; if next.is_finite() { @@ -937,6 +991,7 @@ impl Show { "presenterSecs": self.timer.elapsed(now), "notesPx": self.notes_px, "custom": self.state.custom_name(), + "goto": if self.goto.is_empty() { None } else { Some(self.goto.clone()) }, }); if self.grid { let slides = grid_indices(n_slides); @@ -956,6 +1011,8 @@ impl Show { } } +const GOTO_HOLD: f64 = 3.0; +const GOTO_DIGITS: usize = 4; const ZOOM_SCALE: f32 = 2.0; const GRID_CAP: usize = 48; const TOOL_BAR_W: f32 = 520.0; @@ -1510,4 +1567,33 @@ mod tests { assert_eq!(v["presenterPaused"], json!(false)); assert_eq!(v["notesPx"], json!(NOTES_MIN)); } + + #[test] + fn a_typed_slide_number_jumps_and_then_expires() { + let mut s = deckcraft_engine::Session::with_new(); + s.execute("slide.new", &json!({})).unwrap(); + let doc = s.active().unwrap().doc.clone(); + let mut show = Show::new(&doc, 0, false, false); + show.push_slide_number('1', 10.0); + show.push_slide_number('2', 10.2); + assert_eq!(show.goto, "12"); + assert_eq!(show.take_goto(), Some(11)); + assert!(show.goto.is_empty()); + show.push_slide_number('0', 1.0); + assert_eq!(show.take_goto(), None, "slide 0 is not a slide"); + show.push_slide_number('4', 5.0); + show.expire_goto(7.0); + assert_eq!(show.goto, "4"); + show.expire_goto(8.1); + assert!(show.goto.is_empty()); + show.push_slide_number('9', 0.0); + assert!(show.clear_goto()); + assert!(!show.clear_goto()); + for _ in 0..6 { + show.push_slide_number('9', 1.0); + } + assert_eq!(show.goto.len(), GOTO_DIGITS); + show.push_slide_number('a', 1.0); + assert!(show.goto.chars().all(|c| c.is_ascii_digit())); + } } diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 9d06bf91..75739419 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -15,10 +15,10 @@ Coordinates are slide points (1/72 inch; a 16:9 slide is 960 × 540) unless note | `engine.execute` / `ui.menu.invoke` / `command` | `{command, params}` | Run any command (see `engine.commands`; `deckcraft-cli describe ID` documents one) | | `engine.commands` | — | Every command: id, label, menu path, shortcut, params doc, enabled / disabled reason | | `document.inspect` | — | Slides, sections, layouts, shapes, selection | -| `ui.inspect` | — | UI state, active tool, window/canvas/slide rects, dialog, `home` (the File tab open over a presentation), `backstage` (its page, also the start screen), slide show state (`grid`, `zoomArm`, `zoom`, `blank`, `presenterPaused`, `presenterSecs`, `notesPx`, `custom`; `cells` while See All Slides is open), documents, perf (fps, render ms) | +| `ui.inspect` | — | UI state, active tool, window/canvas/slide rects, dialog, `home` (the File tab open over a presentation), `backstage` (its page, also the start screen), slide show state (`grid`, `zoomArm`, `zoom`, `blank`, `presenterPaused`, `presenterSecs`, `notesPx`, `custom`, `goto`; `cells` while See All Slides is open), documents, perf (fps, render ms) | | `ui.tool.select` | `{tool, preset?}` | `select`, `text`, `shape` + preset, any preset name, `curve`, `freeform`, `scribble`, `pen`, `highlighter`, `eraser` | | `ui.pointer` | `{events: [{kind: down\|drag\|up\|move\|doubleclick\|tripleclick, x, y, mods?}], mods?}` | Drive the active tool in slide points through the same code path as the mouse | -| `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | Keyboard input (also drives a running slide show: G is See All Slides, Z arms zoom, T pauses the presenter timer, R resets it, `[` and `]` change notes size, Esc closes the grid and zoom before it leaves the show) | +| `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | Keyboard input (also drives a running slide show: digits then Enter go to that slide, G is See All Slides, Z arms zoom, T pauses the presenter timer, R resets it, `[` and `]` change notes size, Esc clears a slide number, the grid and zoom before it leaves the show) | | `ui.ime` | `{preedit, active?: [start, end]}` / `{commit}` | An input method's composition through egui, as a real one arrives: shown in place on the slide or in a focused field; `{"preedit": ""}` cancels | | `ui.move` / `ui.click` / `ui.drag` | screen points | Real egui pointer input — reaches every widget, menu and pane | | `ui.set` | `{tab?, view?, pane?, notes?, zoom?, brightness?, ruler?, gridlines?, guides?, …}` | UI state (returns the full state) | diff --git a/docs/roadmap.md b/docs/roadmap.md index 786f1b65..8faed8cb 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # DeckCraft roadmap: milestones and what's next -> **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) 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 @@ -17,7 +17,7 @@ points and lead the beta plan. |---|---|---|---| | Build a deck from scratch (theme, layouts, titles and bullets, shapes, pictures from file), save, close and reopen it | yes | Commands for all of it; `.deckcraft` and `.pptx` round-trip tests (`crates/pptx/tests/roundtrip.rs`, `crates/format`); AutoRecover. Close discards edits without asking (#60, PR #75); image *paste* fails on Windows/Linux (#53), though insert-from-file works | 1–3 | | Open a colleague's `.pptx`, edit it, save it back as `.pptx` that PowerPoint opens | partial (not blocking) | The reader is lenient and keeps unknown objects. Files open in PowerPoint without repair (checked 2026-10-06). But only generated decks have been tested; text inside EMF/WMF pictures is missing; comment threads, media options and rewind are lost (#70, #74, #77) | 31–55 | -| Present: full-screen show with transitions, animations, media, presenter view with notes, next slide and timer | yes | 48/48 transitions, 64 effects, triggers, builds, media playback, presenter window, See All Slides and zoom. The presenter timer pauses and resets, and notes have a size control. A custom show plays only its slides. Presenter view isn't placed on the second display automatically (drag it there; #433) | 2–4 | +| Present: full-screen show with transitions, animations, media, presenter view with notes, next slide and timer | yes | 48/48 transitions, 64 effects, triggers, builds, media playback, presenter window, See All Slides and zoom. The presenter timer pauses and resets, and notes have a size control. A custom show plays only its slides. Typing a slide number during the show shows it, and Enter goes there. Presenter view isn't placed on the second display automatically (drag it there; #433) | 2–4 | | Add business content: tables, charts with their data, simple diagrams | partial (not blocking) | Tables with styles, merge/split, all D or P. Charts insert with the common types and edit data in the Chart Data dialog (P0 partial); SmartArt is basic (pre-drawn shapes) | 12–20 | | Share the deck: PDF of slides, notes or handouts; images | yes | `file.export` PDF with text layer, links, bookmarks, handouts 1–9; PNG/JPEG. Artwork is raster in the PDF; no direct print (print the PDF) | 10–16 | | Review: comments with replies, resolve, spelling | partial (not blocking) | Comments, replies and resolve work and survive `.deckcraft`; threads are flattened on `.pptx` save (#70); spelling is a small built-in list | 4–8 | @@ -109,6 +109,7 @@ plan above. | Date | Change | Summary | |---|---|---| +| 2026-10-14 | trivial | Present workflow notes go to slide number entry | | 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 | diff --git a/docs/slideshow-parity.md b/docs/slideshow-parity.md index de5ad1ec..5b4c2bfd 100644 --- a/docs/slideshow-parity.md +++ b/docs/slideshow-parity.md @@ -1,6 +1,6 @@ # Slide show and presenter view: parity with PowerPoint -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (custom show playback; previous: minor, presenter timer pause/reset) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (go to slide shows the number while it is typed; previous: minor, custom show playback) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Presenting is the job PowerPoint is hired for. This doc covers the show window, presenter view, rehearsal and recording. Transition and animation playback are in @@ -14,7 +14,7 @@ ready** (estimated), **10–16 h** to parity. |---|---|---|---| | Play from Start / from Current Slide (F5, Shift+F5, Cmd+Enter) | yes | yes | | | Next/previous by click, arrows, Space, Page Up/Down, Enter, Backspace | yes | yes | #17 reported arrows not working in v0.3.0 AppImage; recheck | -| Go to slide (number + Enter), first/last | yes | yes | | +| Go to slide (number + Enter), first/last | yes | yes: the digits show as "Go to slide …" until Enter, Backspace, Esc, or 3 seconds | Slide numbers are 1-based; 0 and a number past the end do not move | | Black / white screen (B / W) | yes | yes | | | End screen, Esc to leave | yes | yes | | | Step clock: a click after a long pause plays its step (#11) | yes | yes | | @@ -72,6 +72,7 @@ Reading View in a window with navigation: present (checklist row done). | Date | Change | Summary | |---|---|---| +| 2026-10-14 | minor | Typing a slide number during the show shows it, and Enter goes there. Esc clears the number before it leaves the show. Checklist breadth stays 66% | | 2026-10-14 | minor | A custom show plays only its slides, from the Custom Shows dialog, from Set Up Slide Show, or from a click action. Checklist breadth stays 66% | | 2026-10-14 | minor | Presenter view pauses and resets its timer (T, R) and changes notes size (A-/A+, brackets). The clock shows local time. Second-display placement and swap stay with #433. Checklist breadth stays 66% | | 2026-10-14 | minor | See All Slides (G) and click-to-zoom (Z) during the show. Presenter view opens that grid and sets black, white and zoom. Checklist breadth stays 66% | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 7f6540a7..de1b129e 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-14 · **Last updated:** 2026-10-14 · **Change:** trivial (slide show plays a custom show; breadth stays 79%; 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; breadth stays 79%; previous: trivial, custom show playback) · **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; @@ -227,7 +227,7 @@ Carried from the 2026-10-07 ROADMAP status and updated with what has landed sinc - **Transitions and animations:** 48 transitions including Morph (objects, words, characters, geometry outlines), 64 animation effects (entrance, emphasis, exit, motion paths, media), triggers, by-paragraph builds, Animation Painter, on a shared timeline engine. -- **Slide show:** full screen, keyboard/mouse navigation, blank screens, go-to-slide, See All +- **Slide show:** full screen, keyboard/mouse navigation, blank screens, go-to-slide (the typed number is shown), See All Slides, click-to-zoom, pen, presenter window (black, white, zoom, the slide grid, a timer that pauses and resets, a local clock, and notes size), rehearse timings, custom shows played on their own, Set Up Show, Reading View; links and shape actions followed on click. @@ -277,6 +277,7 @@ scored done (16 open issues). | Date | Change | Summary | |---|---|---| +| 2026-10-14 | trivial | Slide show evidence includes a visible go-to-slide number. Breadth stays 79% | | 2026-10-14 | trivial | Slide show evidence includes custom show playback. Breadth stays 79% | | 2026-10-14 | trivial | Slide show evidence includes presenter timer pause/reset, a local clock and notes size. Breadth stays 79% | | 2026-10-14 | trivial | Slide show evidence includes See All Slides and zoom. Breadth stays 79% |