diff --git a/ROADMAP.md b/ROADMAP.md index 2227e6602..edd316276 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 | diff --git a/apps/deckcraft/src/main.rs b/apps/deckcraft/src/main.rs index 2798c2d71..3f872c214 100644 --- a/apps/deckcraft/src/main.rs +++ b/apps/deckcraft/src/main.rs @@ -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 { + let n = window.available_monitors().collect::>().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. diff --git a/crates/ui-egui/src/app/playback.rs b/crates/ui-egui/src/app/playback.rs index c959829cd..c46286807 100644 --- a/crates/ui-egui/src/app/playback.rs +++ b/crates/ui-egui/src/app/playback.rs @@ -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)); diff --git a/crates/ui-egui/src/presenter_tests.rs b/crates/ui-egui/src/presenter_tests.rs index c422ad183..6dbc9a9d5 100644 --- a/crates/ui-egui/src/presenter_tests.rs +++ b/crates/ui-egui/src/presenter_tests.rs @@ -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, 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"); +} diff --git a/crates/ui-egui/src/services.rs b/crates/ui-egui/src/services.rs index b6f8a0f50..0ca335a84 100644 --- a/crates/ui-egui/src/services.rs +++ b/crates/ui-egui/src/services.rs @@ -71,4 +71,6 @@ pub struct Services { pub clipboard_set_html: Option bool>>, /// Audio output for media playback (cpal on desktop); without one media plays silently. pub audio_out: Option>, + /// How many displays are connected, when the host can tell: Presenter View needs a second. + pub displays: Option Option>>, } diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md index a0b94ac0c..485cf0866 100644 --- a/docs/hardware-parity.md +++ b/docs/hardware-parity.md @@ -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 | diff --git a/docs/slideshow-parity.md b/docs/slideshow-parity.md index 137960531..08fff1b5f 100644 --- a/docs/slideshow-parity.md +++ b/docs/slideshow-parity.md @@ -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% |