Show All slides in the Presenter View window, not on the audience screen (#1130)

This commit is contained in:
MarekSpodBiedry
2026-10-22 21:10:16 +02:00
parent a2435383b1
commit bde35fab11
2 changed files with 84 additions and 11 deletions
+81 -9
View File
@@ -34,6 +34,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>,
@@ -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<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()
@@ -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<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() {
+3 -2
View File
@@ -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 |