From b9842d0b8278e22983d9710571d7407c424be817 Mon Sep 17 00:00:00 2001 From: Faabyy <68284158+Faabyy@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:50:26 +0000 Subject: [PATCH 1/2] Slide show: Set Up Slide Show can keep the mouse cursor visible (#1445) A full-screen show hides a still pointer after 2.5 s, which makes decks with clickable, branching choices hard to use. Set Up Slide Show now has Keep mouse cursor visible (`show.keepCursor {on?}`): the show then keeps the normal pointer, and the pointing hand over links and actions, while links still follow. It is a user preference kept in ui.json, since PPTX has no place for it; off by default, Reading View unchanged. Co-Authored-By: Claude --- ROADMAP.md | 1 + crates/ui-egui/src/app/commands.rs | 6 +++ crates/ui-egui/src/app/playback.rs | 1 + crates/ui-egui/src/dialogs/body.rs | 2 + crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/src/show.rs | 9 ++++ crates/ui-egui/src/show/tests.rs | 73 ++++++++++++++++++++++++++++++ crates/ui-egui/src/show/ui.rs | 2 +- crates/ui-egui/src/state.rs | 4 ++ docs/slideshow-parity.md | 5 +- 10 files changed, 101 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b04c4b69..9599f55e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -137,6 +137,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-27 | Set Up Slide Show ▸ Keep mouse cursor visible: a full-screen show no longer hides a still pointer, so the choices of a branching deck stay visible and clickable; the setting is kept with the user's preferences (`show.keepCursor`), off by default (#1445) | | 2026-10-25 | Windows installer: the Start menu shortcut takes the icon embedded in `deckcraft.exe` instead of an MSI ``, so a taskbar pin keeps its icon after an update (#1114); a pin that already lost it has to be unpinned and pinned again once | | 2026-10-24 | AutoFormat as you type: a paragraph that starts with `*`, `-`, `--`, `>`, `<>`, `1.`, `1)`, `I.`, `i.`, `A.`, `a.` or `a)` and a space or tab becomes a bulleted or numbered list in that style; Backspace right after puts the typed text back, and Tools ▸ AutoCorrect and `text.autocorrect {lists}` turn it off | | 2026-10-24 | Tables: while the caret is in a cell, dragging into another cell selects that range of cells (shaded, merged cells taken in whole), and Shift+arrow grows or shrinks it; formatting applies to the selected cells only, and Delete clears their text and keeps the table | diff --git a/crates/ui-egui/src/app/commands.rs b/crates/ui-egui/src/app/commands.rs index 8f0e462f..4592d6f1 100644 --- a/crates/ui-egui/src/app/commands.rs +++ b/crates/ui-egui/src/app/commands.rs @@ -163,6 +163,12 @@ impl SlideApp { } } "show.presenter" => self.ui.presenter_view = self.flag(p, self.ui.presenter_view), + "show.keepCursor" => { + self.ui.show_keep_cursor = self.flag(p, self.ui.show_keep_cursor); + if let Some(s) = self.show.as_mut() { + s.keep_cursor = self.ui.show_keep_cursor; + } + } "show.end" => { self.show = None; self.media.stop_owned(media::Owner::Show); diff --git a/crates/ui-egui/src/app/playback.rs b/crates/ui-egui/src/app/playback.rs index 33b173d3..b2b8145f 100644 --- a/crates/ui-egui/src/app/playback.rs +++ b/crates/ui-egui/src/app/playback.rs @@ -36,6 +36,7 @@ impl SlideApp { self.show_instance = self.show_instance.wrapping_add(1).max(1); let mut show = show::Show::new(&d.doc, from, reading, self.ui.presenter_view && !reading); show.instance = self.show_instance; + show.keep_cursor = self.ui.show_keep_cursor; self.show = Some(show); } } diff --git a/crates/ui-egui/src/dialogs/body.rs b/crates/ui-egui/src/dialogs/body.rs index 071e042a..f6efcac4 100644 --- a/crates/ui-egui/src/dialogs/body.rs +++ b/crates/ui-egui/src/dialogs/body.rs @@ -206,6 +206,7 @@ pub(super) fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let nn = check(ui, d, "Show without narration", "nn", sh.without_narration); let na = check(ui, d, "Show without animation", "na", sh.without_animation); let ut = check(ui, d, "Use timings, if present", "ut", sh.use_timings); + let kc = check(ui, d, "Keep mouse cursor visible", "kc", app.ui.show_keep_cursor); let names: Vec = app.session.active().map(|s| s.doc.custom_shows.iter().map(|c| c.name.clone()).collect()).unwrap_or_default(); let chosen = d.fields.entry("custom".into()).or_insert_with(|| sh.custom_show.clone().unwrap_or_default()).clone(); let field = ui.label("Slides"); @@ -226,6 +227,7 @@ pub(super) fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let ty = d.get("type", "speaker"); let custom = d.fields.get("custom").cloned().unwrap_or_default(); run(app, "show.setup", json!({"type": ty, "loop": lp, "noNarration": nn, "noAnimation": na, "useTimings": ut, "custom": custom})); + run(app, "show.keepCursor", json!({"on": kc})); } ok || cancel } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 30b5a0cd..0c28f3fe 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -110,6 +110,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("view.newWindow", "New Window", None, "{on?: bool, slide?: index, delta?: int}"), ("show.start", "Slide Show", Some("F5"), "{from?: index, custom?: name}"), ("show.presenter", "Presenter View", None, "{on?: bool}"), + ("show.keepCursor", "Keep Mouse Cursor Visible", None, "{on?: bool}"), ("show.end", "End Show", None, "{}"), ("animation.preview", "Preview Animations", None, "{on?: bool, from?: effect index}"), ("app.home", "Home", None, "{on?}"), diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs index 342dfd2c..503a3628 100644 --- a/crates/ui-egui/src/show.rs +++ b/crates/ui-egui/src/show.rs @@ -44,6 +44,8 @@ pub struct Show { /// Play every step automatically (animation preview). pub autoplay: bool, pub rehearse: bool, + /// Set Up Slide Show ▸ Keep mouse cursor visible: a still pointer is not hidden (#1445). + pub keep_cursor: bool, timeline: Option<(usize, Timeline)>, step_start: f64, slide_start: f64, @@ -206,6 +208,7 @@ impl Show { preview_only: false, autoplay: false, rehearse: false, + keep_cursor: false, timeline: None, step_start: 0.0, slide_start: 0.0, @@ -362,6 +365,12 @@ impl Show { now - self.last_move < 2.5 } + /// Whether a still pointer is hidden: after 2.5 s in a full-screen show, unless Keep mouse + /// cursor visible is on. Reading View always keeps it. + fn hides_idle_cursor(&self, now: f64) -> bool { + !self.pointer_awake(now) && !self.reading && !self.keep_cursor + } + /// Pen and laser are different pointers: turning one on puts the other away. fn set_pen(&mut self, on: bool) { self.pen = on; diff --git a/crates/ui-egui/src/show/tests.rs b/crates/ui-egui/src/show/tests.rs index 8c515363..2395171f 100644 --- a/crates/ui-egui/src/show/tests.rs +++ b/crates/ui-egui/src/show/tests.rs @@ -52,6 +52,79 @@ fn a_still_cursor_hides_over_a_clickable_shape() { assert_eq!(harness.output().platform_output.cursor_icon, egui::CursorIcon::None, "still for 3 s: hidden"); } +/// #1445: Keep mouse cursor visible leaves a still pointer showing over a link, so a branching +/// deck's choices stay targetable; the setting outlives the show and the link still follows. +#[test] +fn keep_cursor_visible_leaves_a_still_cursor_over_a_link() { + let mut session = deckcraft_engine::Session::with_new(); + let size = session.active().unwrap().doc.slide_size; + session.execute("slide.new", &json!({})).unwrap(); + session.execute("slide.go", &json!({"index": 0})).unwrap(); + let r = session.execute("shape.insert", &json!({"preset": "rect", "rect": [0.0, 0.0, size.width, size.height]})).unwrap(); + session.execute("insert.hyperlink", &json!({"slide": 1, "ids": [r["id"]]})).unwrap(); + let mut app = SlideApp::new(session, crate::Services::default()); + app.ui.presenter_view = false; + app.run("show.keepCursor", json!({"on": true})).unwrap(); + let mut harness = egui_kittest::Harness::builder().with_size(vec2(1000.0, 700.0)).with_step_dt(0.25).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + app, + ); + harness.run_steps(3); + harness.state_mut().start_show(0, false); + harness.run_steps(2); + harness.hover_at(pos2(500.0, 350.0)); + harness.run_steps(14); + assert_eq!(harness.output().platform_output.cursor_icon, egui::CursorIcon::PointingHand, "still for 3 s: kept"); + click_at(&mut harness, pos2(500.0, 350.0)); + harness.run_steps(2); + assert_eq!(harness.state().show.as_ref().map(|s| s.state.slide), Some(1), "the link still follows"); + assert!(harness.state().ui.show_keep_cursor, "the setting is kept for the next show"); +} + +/// #1445: Set Up Slide Show turns Keep mouse cursor visible on and off. +#[test] +fn set_up_slide_show_keeps_the_cursor_visible() { + use egui_kittest::kittest::Queryable; + + let app = SlideApp::new(deckcraft_engine::Session::with_new(), crate::Services::default()); + let mut harness = egui_kittest::Harness::builder().with_size(vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + app, + ); + for on in [true, false] { + harness.state_mut().run("app.dialog", json!({"id": "setupShow"})).unwrap(); + harness.run_steps(6); + harness.get_by_label("Keep mouse cursor visible").click(); + harness.run_steps(3); + harness.get_by_label("OK").click(); + harness.run_steps(2); + assert!(harness.state().dialog.is_none()); + assert_eq!(harness.state().ui.show_keep_cursor, on); + } +} + +/// #1445: the idle hide follows the pointer's last move, Reading View and Keep mouse cursor visible. +#[test] +fn the_idle_cursor_hides_only_when_asked() { + let session = deckcraft_engine::Session::with_new(); + let doc = session.active().unwrap().doc.clone(); + let mut show = Show::new(&doc, 0, false, false); + show.last_move = 10.0; + assert!(!show.hides_idle_cursor(12.0), "moved 2 s ago"); + assert!(show.hides_idle_cursor(13.0), "still for 3 s"); + show.keep_cursor = true; + assert!(!show.hides_idle_cursor(13.0), "kept visible"); + show.keep_cursor = false; + show.reading = true; + assert!(!show.hides_idle_cursor(13.0), "Reading View never hides it"); +} + /// #1130: the Presenter View's All slides button opens the grid in that window, not on the audience screen. #[test] fn presenter_all_slides_does_not_cover_the_audience_screen() { diff --git a/crates/ui-egui/src/show/ui.rs b/crates/ui-egui/src/show/ui.rs index e769d66f..80f9bd86 100644 --- a/crates/ui-egui/src/show/ui.rs +++ b/crates/ui-egui/src/show/ui.rs @@ -469,7 +469,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { ctx.set_cursor_icon(egui::CursorIcon::None); } else if show.zoom_arm || show.grid { ctx.set_cursor_icon(egui::CursorIcon::Crosshair); - } else if !show.pointer_awake(now) && !show.reading { + } else if show.hides_idle_cursor(now) { // Idle first: a still pointer hides also over a clickable shape, such as a playing clip. ctx.set_cursor_icon(egui::CursorIcon::None); ctx.request_repaint_after(std::time::Duration::from_millis(500)); diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 616769bc..4f36d8d1 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -76,6 +76,9 @@ pub struct UiState { pub sorter_zoom: f32, pub recent: Vec, pub presenter_view: bool, + /// Set Up Slide Show ▸ Keep mouse cursor visible: full-screen shows never hide a still pointer, + /// for decks with clickable choices (#1445). + pub show_keep_cursor: bool, pub ui_scale: f32, /// Format Shape pane: tab (`fill`, `effects`, `size`, `picture`, `text`). pub format_tab: String, @@ -115,6 +118,7 @@ impl Default for UiState { sorter_zoom: 1.0, recent: vec![], presenter_view: true, + show_keep_cursor: false, ui_scale: 1.0, format_tab: "fill".into(), smartart_row: 0, diff --git a/docs/slideshow-parity.md b/docs/slideshow-parity.md index e843ee13..d6853560 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-24 · **Change:** minor (Ctrl+wheel zooms the show around the pointer, #1286; previous: major, community PR wave 2026-10-14: interactive decks, laser pointer, See All Slides and zoom, go-to-slide number, custom show playback, presenter timer, clock, notes size and keyboard, Show Media Controls, swipe; breadth 68%; duplicated rows and headers folded; previous: major, created) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-27 · **Change:** minor (Set Up Slide Show ▸ Keep mouse cursor visible, #1445; previous: minor, Ctrl+wheel zooms the show around the pointer, #1286; previous: major, community PR wave 2026-10-14: interactive decks, laser pointer, See All Slides and zoom, go-to-slide number, custom show playback, presenter timer, clock, notes size and keyboard, Show Media Controls, swipe; breadth 68%; duplicated rows and headers folded; previous: major, created) · **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 @@ -34,7 +34,7 @@ ready** (estimated), **10–16 h** to parity. | Laser pointer | yes | yes in the show | Cmd/Ctrl+L and the on-screen button draw the show's laser colour and leave no ink. The presenter window does not move that dot | | Subtitles and live captions | yes (cloud speech) | no | AI dimension | | Custom shows | yes | yes: Show plays that list in order and does not change Set Up; Set Up Slide Show can keep one for Play from Start | Hidden slides in the list are skipped | -| Set Up Show: loop until Esc, kiosk (browsed at a kiosk), show without animation/narration, pen colour, slides range | yes | yes, most options | | +| Set Up Show: loop until Esc, kiosk (browsed at a kiosk), show without animation/narration, pen colour, slides range | yes | yes, most options | Keep mouse cursor visible (DeckCraft's own, #1445) stops the show hiding a still pointer after 2.5 s, for decks with clickable choices; it is a user preference (`ui.json`, `show.keepCursor`), since PPTX has no place for it | | Use rehearsed timings | yes | yes | | ## Presenter view @@ -79,6 +79,7 @@ Reading View in a window with navigation: present (checklist row done). | Date | Change | Summary | |---|---|---| +| 2026-10-27 | minor | Set Up Slide Show ▸ Keep mouse cursor visible: full-screen shows keep a still pointer, so clickable choices stay targetable; Reading View unchanged (#1445) | | 2026-10-22 | minor | See All Slides from presenter view opens in presenter view, not on the audience screen (#1130) | | 2026-10-14 | trivial | Community PR wave folded: one media playback row, the presenter pen/laser row matches the laser that exists, estimates keep only the open work (~9–16 h); checklist breadth 68% | | 2026-10-13 | minor | Click handling for interactive decks: triggers, per-slide state kept across visits, visible-shape hit testing with hand cursor, advance on click | From 9388884d202c3f95386fbe3301acc79dc9569742 Mon Sep 17 00:00:00 2001 From: Faabyy <68284158+Faabyy@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:17:11 +0000 Subject: [PATCH 2/2] Docs: the keep-cursor preference is `showKeepCursor` in ui.json; roadmap header for #1445 Co-Authored-By: Claude --- ROADMAP.md | 3 ++- docs/slideshow-parity.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9599f55e..3da7c452 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,6 +2,7 @@ **Stage: alpha** · next: beta, ~20 points and ~120–210 h away +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-27 · **Change:** trivial (progress log: Set Up Slide Show ▸ Keep mouse cursor visible, #1445; stage and scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-24 · **Change:** trivial (progress log: Edit Points handle drags with Shift, Ctrl/Cmd or Alt/Option set the point type; previous: trivial, progress log: scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; 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-24 · **Change:** trivial (progress log: Ctrl+wheel zoom during the slide show, #1286; previous: trivial, scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; 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-24 · **Change:** trivial (progress log: picture and embedded object fills; previous: trivial, scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) @@ -137,7 +138,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| -| 2026-10-27 | Set Up Slide Show ▸ Keep mouse cursor visible: a full-screen show no longer hides a still pointer, so the choices of a branching deck stay visible and clickable; the setting is kept with the user's preferences (`show.keepCursor`), off by default (#1445) | +| 2026-10-27 | Set Up Slide Show ▸ Keep mouse cursor visible: a full-screen show no longer hides a still pointer, so the choices of a branching deck stay visible and clickable; the setting is kept with the user's preferences (`ui.json`, `showKeepCursor`), off by default (#1445) | | 2026-10-25 | Windows installer: the Start menu shortcut takes the icon embedded in `deckcraft.exe` instead of an MSI ``, so a taskbar pin keeps its icon after an update (#1114); a pin that already lost it has to be unpinned and pinned again once | | 2026-10-24 | AutoFormat as you type: a paragraph that starts with `*`, `-`, `--`, `>`, `<>`, `1.`, `1)`, `I.`, `i.`, `A.`, `a.` or `a)` and a space or tab becomes a bulleted or numbered list in that style; Backspace right after puts the typed text back, and Tools ▸ AutoCorrect and `text.autocorrect {lists}` turn it off | | 2026-10-24 | Tables: while the caret is in a cell, dragging into another cell selects that range of cells (shaded, merged cells taken in whole), and Shift+arrow grows or shrinks it; formatting applies to the selected cells only, and Delete clears their text and keeps the table | diff --git a/docs/slideshow-parity.md b/docs/slideshow-parity.md index d6853560..5b10c202 100644 --- a/docs/slideshow-parity.md +++ b/docs/slideshow-parity.md @@ -34,7 +34,7 @@ ready** (estimated), **10–16 h** to parity. | Laser pointer | yes | yes in the show | Cmd/Ctrl+L and the on-screen button draw the show's laser colour and leave no ink. The presenter window does not move that dot | | Subtitles and live captions | yes (cloud speech) | no | AI dimension | | Custom shows | yes | yes: Show plays that list in order and does not change Set Up; Set Up Slide Show can keep one for Play from Start | Hidden slides in the list are skipped | -| Set Up Show: loop until Esc, kiosk (browsed at a kiosk), show without animation/narration, pen colour, slides range | yes | yes, most options | Keep mouse cursor visible (DeckCraft's own, #1445) stops the show hiding a still pointer after 2.5 s, for decks with clickable choices; it is a user preference (`ui.json`, `show.keepCursor`), since PPTX has no place for it | +| Set Up Show: loop until Esc, kiosk (browsed at a kiosk), show without animation/narration, pen colour, slides range | yes | yes, most options | Keep mouse cursor visible (DeckCraft's own, #1445) stops the show hiding a still pointer after 2.5 s, for decks with clickable choices; it is a user preference (`ui.json`, `showKeepCursor`), since PPTX has no place for it | | Use rehearsed timings | yes | yes | | ## Presenter view