mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-11 16:37:06 -06:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<Icon>`, 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 |
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> = 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
|
||||
}
|
||||
|
||||
@@ -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?}"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -76,6 +76,9 @@ pub struct UiState {
|
||||
pub sorter_zoom: f32,
|
||||
pub recent: Vec<String>,
|
||||
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,
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user