From bde35fab110d410265d6339d2bd583277efb578f Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:05:12 +0200 Subject: [PATCH] Show All slides in the Presenter View window, not on the audience screen (#1130) --- crates/ui-egui/src/show.rs | 90 ++++++++++++++++++++++++++++++++++---- docs/slideshow-parity.md | 5 ++- 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs index 51f859af..857ab196 100644 --- a/crates/ui-egui/src/show.rs +++ b/crates/ui-egui/src/show.rs @@ -34,6 +34,8 @@ pub struct Show { blank: Option, /// 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, @@ -187,6 +189,7 @@ impl Show { prefetch: None, blank: None, grid: false, + presenter_grid: false, zoom_arm: false, zoom: None, thumbs: vec![], @@ -422,7 +425,8 @@ fn part_textures(ctx: &egui::Context, doc: &Presentation, from: Option, 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() @@ -433,10 +437,15 @@ 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; + if (show.grid || presenter_grid) && !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; } @@ -491,7 +500,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), @@ -538,7 +547,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; @@ -1322,6 +1331,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 { @@ -1334,7 +1352,10 @@ 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 { + if self.presenter_grid { + self.presenter_grid = false; + true + } else if self.grid { self.grid = false; true } else { @@ -1758,7 +1779,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)); @@ -1800,8 +1821,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(); @@ -1809,6 +1830,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); @@ -1909,6 +1947,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, 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() { diff --git a/docs/slideshow-parity.md b/docs/slideshow-parity.md index 921732a5..94f1144a 100644 --- a/docs/slideshow-parity.md +++ b/docs/slideshow-parity.md @@ -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 |