Show: Presenter View only with a second display (#1156)

* Show: Presenter View only with a second display

With one display the presenter window opened on the same screen as the show. Under Hyprland's
focus-follows-mouse, whichever window the pointer was over took full screen, and the hidden one
stalled every frame: the show lagged, dropped key presses or froze.

One display now shows only the slides, as in PowerPoint. The host counts displays through winit's
monitor list when a show starts; hosts that can't tell keep Presenter View.

* Show: count displays by MonitorHandle

Wayland lists one output twice under the same wl_output id, so comparing handles counts it once, and two separate displays with the same name and geometry still count as two.

* Show: docs for Presenter View with a second display

slideshow-parity and hardware-parity note that on Linux one display shows only the slides, with status lines and revision history; ROADMAP progress log. display_count collects into a BTreeSet (MonitorHandle's Ord matches its Eq).

* Show: docs state the one-display rule exactly

Presenter View stays off when winit lists exactly one display; if it lists none, the setting is kept. The host callback covers Unix other than macOS, so the docs say Linux and the BSDs.

* Apply review fixes

- docs/slideshow-parity.md

---------

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
This commit is contained in:
Michael Müller
2026-11-05 12:51:16 +01:00
committed by GitHub
co-authored by aiongg
parent 2b6d27d525
commit 73de9900a7
7 changed files with 49 additions and 5 deletions
+1
View File
@@ -156,6 +156,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|---|---|
| 2026-11-04 | Ogg Opus recovery keeps trailing packets without granules in the total duration, matching their recovered positions; valid end trimming stays covered |
| 2026-11-03 | Web: the `?host=parent` bridge (as in GridCraft, WordCraft and PdfCraft) lets a same-origin page that frames DeckCraft send a presentation and store its saves; Save writes an opened .pptx back as .pptx under its own name, as on the desktop |
| 2026-11-02 | Slide show on Linux and the BSDs: Presenter View stays off when winit lists exactly one display, as in PowerPoint; if it lists none, the setting is kept. On one display the presenter window covered the show, and the hidden window stalled every frame: slide changes took up to ~1.7 s under Hyprland (#1156) |
| 2026-11-02 | Native and PPTX saves retain optional table right-to-left direction, including explicit false and omission; two-save round-trip tests keep labeled columns in order |
| 2026-11-02 | Text runs preserve alternate-language metadata independently from the main language through native and PPTX saves |
| 2026-11-02 | Picture compression measures displayed dimensions through nested group transforms; checklist score unchanged |
+13
View File
@@ -143,6 +143,14 @@ fn services(native_wayland: bool) -> Services {
}
}
/// Displays winit lists for `window`, counting an output Wayland lists twice once. `None` when it
/// lists none: Presenter View then stays as it was.
#[cfg(all(unix, not(target_os = "macos")))]
fn display_count(window: &winit::window::Window) -> Option<usize> {
let n = window.available_monitors().collect::<std::collections::BTreeSet<_>>().len();
(n > 0).then_some(n)
}
const APP_ID: &str = "ai.storyteller.deckcraft";
const USAGE: &str = "\
@@ -280,6 +288,11 @@ fn main() -> eframe::Result {
Box::new(move |cc| {
deckcraft_ui_egui::install_ime_pass_hooks(&cc.egui_ctx);
let mut app = SlideApp::new(Session::new(), services(native_wayland));
// Presenter View asks winit how many displays there are when a show starts.
#[cfg(all(unix, not(target_os = "macos")))]
if let Some(window) = cc.winit_window().cloned() {
app.services.displays = Some(Box::new(move || display_count(&window)));
}
load_prefs(&mut app);
app.enable_update_checks();
// AutoRecover: reopen what a previous run left unsaved, then keep it current.
+10 -1
View File
@@ -32,15 +32,24 @@ impl SlideApp {
pub fn start_show(&mut self, from: usize, reading: bool) {
self.anim_preview = None;
self.media.stop_owned(media::Owner::Editor);
let presenter = self.ui.presenter_view && !reading && self.second_display();
if let Some(d) = self.session.active() {
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);
let mut show = show::Show::new(&d.doc, from, reading, presenter);
show.instance = self.show_instance;
show.keep_cursor = self.ui.show_keep_cursor;
self.show = Some(show);
}
}
/// Whether a second display can take Presenter View. Like PowerPoint, one display shows only
/// the slides: the presenter window would open on the same screen, and whichever of the two
/// windows ends up behind the other stalls the show (the compositor stops pacing a hidden
/// window). Hosts that can't count displays keep Presenter View.
fn second_display(&mut self) -> bool {
self.services.displays.as_mut().and_then(|f| f()).is_none_or(|n| n >= 2)
}
/// Play custom show `name` from its first slide. Set Up Slide Show is left as it was.
pub fn start_custom_show(&mut self, name: &str, reading: bool) {
let state = self.session.active().and_then(|d| deckcraft_anim::ShowState::play_custom(&d.doc, name));
+17
View File
@@ -16,3 +16,20 @@ fn show_presenter_answers_on_and_window() {
let doc = crate::UI_COMMANDS.iter().find(|c| c.0 == "show.presenter").unwrap().3;
assert!(doc.contains("separate window"), "{doc}");
}
#[test]
fn presenter_view_needs_a_second_display() {
// One display: the presenter window covered the show (or the show covered it) and the
// hidden window stalled every frame.
let presenter = |displays: Option<usize>, setting: bool| {
let services = Services { displays: Some(Box::new(move || displays)), ..Default::default() };
let mut app = SlideApp::new(Session::with_new(), services);
app.ui.presenter_view = setting;
app.start_show(0, false);
app.show.as_ref().map(|s| s.presenter)
};
assert_eq!(presenter(Some(1), true), Some(false));
assert_eq!(presenter(Some(2), true), Some(true));
assert_eq!(presenter(None, true), Some(true), "a host that can't count displays keeps Presenter View");
assert_eq!(presenter(Some(2), false), Some(false), "the setting still turns it off");
}
+2
View File
@@ -71,4 +71,6 @@ pub struct Services {
pub clipboard_set_html: Option<Box<dyn FnMut(&str, &str) -> bool>>,
/// Audio output for media playback (cpal on desktop); without one media plays silently.
pub audio_out: Option<Box<dyn deckcraft_media::AudioOut>>,
/// How many displays are connected, when the host can tell: Presenter View needs a second.
pub displays: Option<Box<dyn FnMut() -> Option<usize>>>,
}
+3 -2
View File
@@ -1,6 +1,6 @@
# Hardware parity with PowerPoint
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (community PR wave 2026-10-14: picture export DPI, Retina pixel grid (#184), twist to rotate, swipe to turn the page, pen pressure; duplicated rows and headers folded; previous: minor, IME input, #101) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365); PowerPoint for Windows where noted
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-11-02 · **Change:** minor (Presenter View only with a second display on Linux and the BSDs, #1156; previous: minor, community PR wave 2026-10-14: picture export DPI, Retina pixel grid (#184), twist to rotate, swipe to turn the page, pen pressure; duplicated rows and headers folded; previous: minor, IME input, #101) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365); PowerPoint for Windows where noted
What hardware PowerPoint uses and what DeckCraft uses, per platform. Dimension score: **~40%
ready** (estimated), **25–45 h** to parity. Performance (unmeasured, ~55%) is tracked here too
@@ -34,7 +34,7 @@ because it mostly follows from hardware use.
| Feature | PowerPoint | DeckCraft |
|---|---|---|
| Detect the projector, show on it, presenter view on the laptop | yes | **no**: the show goes full screen on the current display; presenter view opens as a normal window (egui viewport) |
| Detect the projector, show on it, presenter view on the laptop | yes | **no**: the show goes full screen on the current display; presenter view opens as a normal window (egui viewport). On Linux and the BSDs, when winit lists exactly one display, the show opens without Presenter View, as in PowerPoint; if it lists none, the setting is kept (#1156) |
| Swap displays, choose the monitor in Set Up Show | yes | no |
| Mirroring-aware behaviour | yes | no |
@@ -73,6 +73,7 @@ alignment runs once per transition (#67), and the decoders run in software.
| Date | Change | Summary |
|---|---|---|
| 2026-11-02 | minor | On Linux and the BSDs, Presenter View stays off when winit lists exactly one display: the presenter window covered the show and stalled it. If winit lists none, the setting is kept (#1156) |
| 2026-10-14 | minor | PNG and JPEG slide export record 144 dpi at the default 2 pixels per point, so a Retina picture opens at the slide's size |
| 2026-10-13 | trivial | Editor slide rasters sit on the physical pixel grid (#184) |
| 2026-10-14 | trivial | Community PR wave folded: one row each for pinch/two-finger gestures and pen pressure |
+3 -2
View File
@@ -1,6 +1,6 @@
# Slide show and presenter view: parity with PowerPoint
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-11-02 · **Change:** minor (deleting slides removes them from custom shows; previous: 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)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-11-02 · **Change:** minor (deleting slides removes them from custom shows; previous: minor, Presenter View only with a second display on Linux and the BSDs, #1156; previous: 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
@@ -41,7 +41,7 @@ ready** (estimated), **10–16 h** to parity.
| Feature | PowerPoint | DeckCraft | Notes |
|---|---|---|---|
| Separate presenter window with current slide, next slide, notes | yes | yes (an egui viewport, 1100×700); the notes keep their bullets, numbers, indents and bold/italic, as in the notes pane | |
| Separate presenter window with current slide, next slide, notes | yes | yes (an egui viewport, 1100×700); the notes keep their bullets, numbers, indents and bold/italic, as in the notes pane | On Linux and the BSDs it stays off if winit lists exactly one display as the show starts, as in PowerPoint: on one display it covered the show, and the hidden window stalled every frame (#1156). If winit lists none, the setting is kept. There is not yet a way to switch to Presenter View by hand during a one-display show. macOS, Windows and the browser are unchanged |
| Elapsed timer | yes | yes | |
| Pause / reset timer, clock | yes | yes: Pause/Resume and Reset in presenter view (T pauses, R resets); the clock is local time, `--:--` where there is no clock | Pausing the timer does not pause the show, animations or rehearsal |
| Slide counter, previous/next/end buttons | yes | yes | |
@@ -79,6 +79,7 @@ Reading View in a window with navigation: present (checklist row done).
| Date | Change | Summary |
|---|---|---|
| 2026-11-02 | minor | On Linux and the BSDs, Presenter View stays off when winit lists exactly one display; if it lists none, the setting is kept (#1156) |
| 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% |