mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Merge pull request #1191 from MarekSpodBiedry/issue-1130
This commit is contained in:
+109
-16
@@ -38,6 +38,8 @@ pub struct Show {
|
||||
blank: Option<Color32>,
|
||||
/// See All Slides covers the show until a slide is chosen.
|
||||
pub grid: bool,
|
||||
/// The Presenter View's own See All Slides: the grid replaces the slide and notes in that window only; the audience keeps the slide.
|
||||
pub presenter_grid: bool,
|
||||
/// The next click on the slide zooms in there.
|
||||
pub zoom_arm: bool,
|
||||
zoom: Option<Zoom>,
|
||||
@@ -289,6 +291,7 @@ impl Show {
|
||||
backdrop: None,
|
||||
blank: None,
|
||||
grid: false,
|
||||
presenter_grid: false,
|
||||
zoom_arm: false,
|
||||
zoom: None,
|
||||
thumbs: vec![],
|
||||
@@ -560,7 +563,8 @@ fn part_textures(ctx: &egui::Context, doc: &Presentation, from: Option<usize>, t
|
||||
|
||||
/// Read the current viewport's show keys before its widgets can consume them. Both the
|
||||
/// audience window and Presenter View use this path, including animation steps and end-of-show.
|
||||
fn keyboard(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64) -> bool {
|
||||
/// `presenter` is true for Presenter View, where G opens that window's own grid (#1130).
|
||||
fn keyboard(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64, presenter: bool) -> bool {
|
||||
let keys: Vec<(egui::Key, egui::Modifiers)> = ctx.input(|i| {
|
||||
i.events
|
||||
.iter()
|
||||
@@ -571,10 +575,16 @@ fn keyboard(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64)
|
||||
let mut actions = vec![];
|
||||
for (k, m) in keys {
|
||||
use egui::Key::*;
|
||||
if show.grid && !matches!(k, Escape | Minus) {
|
||||
let presenter_grid = presenter && show.presenter_grid;
|
||||
let grid_open = if presenter { show.presenter_grid } else { show.grid };
|
||||
if grid_open && !matches!(k, Escape | Minus) {
|
||||
// See All Slides takes no other keys until a slide is chosen.
|
||||
if k == G && plain(m) {
|
||||
show.grid = false;
|
||||
if presenter_grid {
|
||||
show.presenter_grid = false;
|
||||
} else {
|
||||
show.grid = false;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
@@ -582,7 +592,7 @@ fn keyboard(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64)
|
||||
match k {
|
||||
Escape | Minus => {
|
||||
let cancelled = k == Escape && show.clear_goto();
|
||||
if !cancelled && !show.back_out() {
|
||||
if !cancelled && !show.back_out(presenter) {
|
||||
keep = false;
|
||||
}
|
||||
}
|
||||
@@ -629,7 +639,7 @@ fn keyboard(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64)
|
||||
B | Period => toggle_blank(show, Color32::BLACK),
|
||||
W | Comma => toggle_blank(show, Color32::WHITE),
|
||||
E => show.strokes.clear(),
|
||||
G if plain(m) => show.grid = true,
|
||||
G if plain(m) => show.open_grid(presenter),
|
||||
Z if plain(m) => show.zoom_key(),
|
||||
T if plain(m) => show.timer.toggle(now),
|
||||
R if plain(m) => show.timer.reset(now),
|
||||
@@ -676,7 +686,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
show.after_enter(&doc, now);
|
||||
}
|
||||
let mut keep = keyboard(&ctx, &mut show, &doc, now);
|
||||
let mut keep = keyboard(&ctx, &mut show, &doc, now, false);
|
||||
let (clicked, secondary, moved) = ctx.input(|i| (i.pointer.primary_clicked(), i.pointer.secondary_clicked(), i.pointer.delta().length() > 0.0));
|
||||
if moved {
|
||||
show.last_move = now;
|
||||
@@ -794,7 +804,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
|
||||
let cur_step = show.state.step;
|
||||
let n_steps = show.timeline(&doc).steps();
|
||||
let step_due = show.autoplay && cur_step < n_steps && idle > 0.3;
|
||||
let holding = show.grid || show.zoom.is_some() || show.zoom_arm;
|
||||
let holding = show.grid || show.presenter_grid || show.zoom.is_some() || show.zoom_arm;
|
||||
if !holding && (step_due || (!show.rehearse && show.state.auto_advance_due(&doc, idle) && !show.ended)) {
|
||||
actions.push(show.state.next(&doc));
|
||||
} else if show.preview_only && cur_step >= n_steps && idle > 0.8 {
|
||||
@@ -1495,6 +1505,15 @@ impl Show {
|
||||
}
|
||||
}
|
||||
|
||||
/// G opens See All Slides in the window it was pressed in: Presenter View's grid stays off the audience screen (#1130).
|
||||
fn open_grid(&mut self, presenter: bool) {
|
||||
if presenter {
|
||||
self.presenter_grid = true;
|
||||
} else {
|
||||
self.grid = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Z, or the Zoom button: arm a click-to-zoom, or leave a zoom that is already on.
|
||||
fn zoom_key(&mut self) {
|
||||
if self.zoom.is_some() || self.zoom_arm {
|
||||
@@ -1505,9 +1524,12 @@ impl Show {
|
||||
}
|
||||
}
|
||||
|
||||
/// Esc closes See All Slides, then a zoom, and only then leaves the show.
|
||||
fn back_out(&mut self) -> bool {
|
||||
if self.grid {
|
||||
/// Esc closes the See All Slides of the window it was pressed in, then a zoom, and only then leaves the show.
|
||||
fn back_out(&mut self, presenter: bool) -> bool {
|
||||
if presenter && self.presenter_grid {
|
||||
self.presenter_grid = false;
|
||||
true
|
||||
} else if !presenter && self.grid {
|
||||
self.grid = false;
|
||||
true
|
||||
} else {
|
||||
@@ -1932,7 +1954,7 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no
|
||||
egui::ViewportId::from_hash_of("deckcraft_presenter"),
|
||||
egui::ViewportBuilder::default().with_title("Presenter View — DeckCraft").with_inner_size([1100.0, 700.0]),
|
||||
|ui, _| {
|
||||
if !keyboard(ui.ctx(), show, doc, now) {
|
||||
if !keyboard(ui.ctx(), show, doc, now, true) {
|
||||
*keep = false;
|
||||
}
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(500));
|
||||
@@ -1974,8 +1996,8 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no
|
||||
if button(ui, if show.blank == Some(Color32::WHITE) { "White on" } else { "White" }).clicked() {
|
||||
toggle_blank(show, Color32::WHITE);
|
||||
}
|
||||
if button(ui, if show.grid { "All slides on" } else { "All slides" }).clicked() {
|
||||
show.grid = !show.grid;
|
||||
if button(ui, if show.presenter_grid { "All slides on" } else { "All slides" }).clicked() {
|
||||
show.presenter_grid = !show.presenter_grid;
|
||||
}
|
||||
if button(ui, show.zoom_label()).clicked() {
|
||||
show.zoom_key();
|
||||
@@ -1983,6 +2005,23 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
let avail = ui.available_rect_before_wrap();
|
||||
if show.presenter_grid {
|
||||
// The grid fills this window; the audience keeps seeing the current slide.
|
||||
paint_grid(ui.painter(), ui.ctx(), show, doc, avail);
|
||||
let hit = ui.interact(avail, ui.id().with("presenter_grid"), Sense::click());
|
||||
if hit.clicked()
|
||||
&& let Some(p) = hit.interact_pointer_pos()
|
||||
&& let Some(n) = see_all_hit(avail, grid_indices(doc.slides.len()).len(), slide_aspect(doc), p)
|
||||
&& let Some(&i) = grid_indices(doc.slides.len()).get(n)
|
||||
{
|
||||
show.presenter_grid = false;
|
||||
show.blank = None;
|
||||
show.zoom = None;
|
||||
show.zoom_arm = false;
|
||||
actions.push(show.state.goto(doc, i));
|
||||
}
|
||||
return;
|
||||
}
|
||||
let left = Rect::from_min_max(avail.min, pos2(avail.min.x + avail.width() * 0.62, avail.max.y));
|
||||
let right = Rect::from_min_max(pos2(left.max.x + 16.0, avail.min.y), avail.max);
|
||||
let cr = fit(Rect::from_min_max(left.min, pos2(left.max.x, left.min.y + left.height() * 0.8)), doc);
|
||||
@@ -2039,6 +2078,8 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no
|
||||
});
|
||||
if ui.input(|i| i.viewport().close_requested()) {
|
||||
show.presenter = false;
|
||||
// A grid left open in the closed window would hold auto-advance for the rest of the show.
|
||||
show.presenter_grid = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -2115,6 +2156,40 @@ mod tests {
|
||||
assert_eq!(harness.output().platform_output.cursor_icon, egui::CursorIcon::None, "still for 3 s: hidden");
|
||||
}
|
||||
|
||||
/// #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() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
|
||||
let mut session = deckcraft_engine::Session::with_new();
|
||||
session.execute("slide.new", &json!({})).unwrap();
|
||||
let doc = session.active().unwrap().doc.clone();
|
||||
let show = Show::new(&doc, 0, false, true);
|
||||
let mut harness = egui_kittest::Harness::builder().with_size(vec2(1100.0, 700.0)).build_ui_state(
|
||||
|ui, (doc, show, keep): &mut (std::sync::Arc<Presentation>, Show, bool)| {
|
||||
let now = ui.input(|i| i.time);
|
||||
presenter_window(ui.ctx(), show, doc, now, keep);
|
||||
},
|
||||
(doc, show, true),
|
||||
);
|
||||
harness.run_steps(2);
|
||||
harness.get_by_label("All slides").click();
|
||||
harness.run_steps(2);
|
||||
assert!(harness.state().1.presenter_grid, "the grid opens in the Presenter View");
|
||||
assert!(!harness.state().1.grid, "the audience screen keeps showing the slide");
|
||||
harness.key_press(egui::Key::Escape);
|
||||
harness.run_steps(2);
|
||||
assert!(!harness.state().1.presenter_grid, "Esc closes the Presenter View grid");
|
||||
assert!(harness.state().2, "and does not end the show");
|
||||
harness.key_press(egui::Key::G);
|
||||
harness.run_steps(2);
|
||||
assert!(harness.state().1.presenter_grid, "G in Presenter View opens its own grid");
|
||||
assert!(!harness.state().1.grid, "G in Presenter View leaves the audience screen alone");
|
||||
harness.key_press(egui::Key::G);
|
||||
harness.run_steps(2);
|
||||
assert!(!harness.state().1.presenter_grid, "G again closes it");
|
||||
}
|
||||
|
||||
/// Send real egui key events to the presenter UI, without the audience window's handler.
|
||||
#[test]
|
||||
fn presenter_navigation_uses_its_own_keyboard_events() {
|
||||
@@ -2412,6 +2487,24 @@ mod tests {
|
||||
assert!((back.0 - 0.5).abs() < 1e-3 && (back.1 - 0.5).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_only_closes_the_grid_of_its_own_window() {
|
||||
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, true);
|
||||
show.presenter_grid = true;
|
||||
assert!(!show.back_out(false), "audience Esc leaves the show, the Presenter View grid stays");
|
||||
assert!(show.presenter_grid);
|
||||
assert!(show.back_out(true));
|
||||
assert!(!show.presenter_grid);
|
||||
show.grid = true;
|
||||
assert!(!show.back_out(true), "Presenter View Esc does not close the audience grid");
|
||||
assert!(show.grid);
|
||||
assert!(show.back_out(false));
|
||||
assert!(!show.grid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_leaves_the_grid_then_the_zoom_and_blank_toggles() {
|
||||
let mut s = deckcraft_engine::Session::with_new();
|
||||
@@ -2420,12 +2513,12 @@ mod tests {
|
||||
let mut show = Show::new(&doc, 0, false, true);
|
||||
show.grid = true;
|
||||
show.zoom = Some(Zoom::at(0.2, 0.3));
|
||||
assert!(show.back_out());
|
||||
assert!(show.back_out(false));
|
||||
assert!(!show.grid);
|
||||
assert!(show.zoom.is_some(), "closing the grid leaves the zoom");
|
||||
assert!(show.back_out());
|
||||
assert!(show.back_out(false));
|
||||
assert!(show.zoom.is_none());
|
||||
assert!(!show.back_out());
|
||||
assert!(!show.back_out(false));
|
||||
show.zoom_key();
|
||||
assert!(show.zoom_arm);
|
||||
show.zoom_key();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Slide show and presenter view: parity with PowerPoint
|
||||
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** 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-22 · **Change:** 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
|
||||
@@ -28,7 +28,7 @@ ready** (estimated), **10–16 h** to parity.
|
||||
| Hyperlinks and action settings followed (slides, web, mail, End Show, Last Viewed, Custom Show) | yes | yes | Only `http(s)` and `mailto` open outside the show, by design (`safe_url`). A Custom Show action plays that show |
|
||||
| Media playback with controls, play across slides, full-screen video, Play/Pause/Stop in the click sequence | yes | yes | WMA/WMV not decodable. Click sequence since #116. Slide Show ▸ Show Media Controls (on by default) hides the playback bar. Seeks and loops preserve new-generation video frames and end-of-stream; queue regression and repeated seeks across H.264/HEVC/VP9/AV1 in `crates/media` |
|
||||
| Animated GIFs play; pause from their corner button | yes | yes (#39) | The corner button hides with Show Media Controls |
|
||||
| See All Slides grid during the show | yes | yes: G, or All on the show bar and in presenter view; click a thumbnail, Esc closes the grid | Hidden slides are labelled; the first 48 slides are shown |
|
||||
| See All Slides grid during the show | yes | yes: G, or All on the show bar; click a thumbnail, Esc closes the grid | Hidden slides are labelled; the first 48 slides are shown |
|
||||
| Zoom into part of a slide during the show | yes | yes: Z, then click the slide (or the presenter slide); 2×, Esc or another click leaves it | The presenter keeps the whole slide and draws a frame on the zoomed part |
|
||||
| Pen and highlighter ink | yes | pen only (Cmd+P pen, Cmd+A arrow, E erases all ink) | No highlighter, per-stroke eraser, ink colours, or keep/discard annotations prompt |
|
||||
| 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 |
|
||||
@@ -50,7 +50,7 @@ ready** (estimated), **10–16 h** to parity.
|
||||
| Placed on the presenter's display automatically, show on the projector | yes | **no**: opens wherever the OS puts it | Beta blocker, 3–5 h. Open PR #433 places the show on a second display when one is attached. This change does not |
|
||||
| Swap displays | yes | no | Still open on #433 |
|
||||
| Pen, laser, black screen, zoom from presenter view | yes | Black, White and Zoom buttons; the audience zoom is framed on the presenter slide | Pen and laser are driven from the audience window only; the presenter window does not move the laser dot |
|
||||
| See all slides from presenter view | yes | yes: All slides opens the grid on the show | |
|
||||
| See all slides from presenter view | yes | yes: All slides (or G) opens the grid in presenter view only, the audience keeps the slide; click a thumbnail to go there, Esc closes it | |
|
||||
| Rehearse with Coach (Speaker Coach) | yes (cloud) | no | AI dimension |
|
||||
|
||||
## Rehearse and record
|
||||
@@ -79,6 +79,7 @@ Reading View in a window with navigation: present (checklist row done).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| 2026-10-13 | minor | Video seek handoff no longer discards freshly decoded frames or end-of-stream (upstream #416); deterministic queue and four-codec seek regression tests |
|
||||
|
||||
Reference in New Issue
Block a user