Browse / github.com/storytold/deckcraft / patch
Show: Presenter View only with a second display
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; Presenter View needs a second display. Will be rebased onto #433 once it lands, using its `SlideApp::displays` instead of the separate count below (see the discussion in the comments). - `apps/deckcraft/src/displays.rs` counts displays through Wayland outputs (`wl_output`) or X11 RandR monitors, on
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- melandur (imported from pull request #1156, open)
- Base
128cb0b883c1210a8aeda30c713b889f5d92a984- Size
- +95 −1 in 5 files
- Review
- pending
- Shared
- 2026-10-28
- Id
47230d01fc7fe0df1117c984
How it went
No other operator has reported using it yet.
Diff
raw · Cargo.lock apps/deckcraft/Cargo.toml apps/deckcraft/src/displays.rs apps/deckcraft/src/main.rs crates/ui-egui/src/lib.rs
@@ -906,8 +906,10 @@ dependencies = [ "log", "rfd", "serde_json",+ "wayland-client", "winit", "winresource",+ "x11rb", ] [[package]]@@ -38,3 +38,6 @@ fmv-macos-events = "=0.1.0" [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] winit = { version = "0.30", default-features = false, features = ["x11"] }+# Counting displays for Presenter View (src/displays.rs); both already come with winit.+wayland-client = "0.31"+x11rb = { version = "0.13", features = ["randr"] }new file mode 100644@@ -0,0 +1,51 @@+//! How many displays are connected (Presenter View needs a second one), asked of the display+//! server the window uses: Wayland's outputs or X11's RandR monitors. `None` when it can't tell.++/// The connected displays, through X11 when `x11` (the window's backend) else Wayland.+pub fn count(x11: bool) -> Option<usize> {+ let n = if x11 { x11_monitors() } else { wayland_outputs() };+ n.filter(|n| *n > 0)+}++fn wayland_outputs() -> Option<usize> {+ use wayland_client::protocol::wl_registry;+ use wayland_client::{Connection, Dispatch, QueueHandle};++ struct Outputs(usize);+ impl Dispatch<wl_registry::WlRegistry, ()> for Outputs {+ fn event(state: &mut Self, _: &wl_registry::WlRegistry, event: wl_registry::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {+ if let wl_registry::Event::Global { interface, .. } = event+ && interface == "wl_output"+ {+ state.0 += 1;+ }+ }+ }++ let conn = Connection::connect_to_env().ok()?;+ let mut queue = conn.new_event_queue();+ let _registry = conn.display().get_registry(&queue.handle(), ());+ let mut outputs = Outputs(0);+ queue.roundtrip(&mut outputs).ok()?;+ Some(outputs.0)+}++fn x11_monitors() -> Option<usize> {+ use x11rb::connection::Connection;+ use x11rb::protocol::randr::ConnectionExt;++ let (conn, screen) = x11rb::connect(None).ok()?;+ let root = conn.setup().roots.get(screen)?.root;+ let reply = conn.randr_get_monitors(root, true).ok()?.reply().ok()?;+ Some(reply.monitors.len())+}++#[cfg(test)]+mod tests {+ #[test]+ fn counting_never_fails_without_a_display_server() {+ // CI has no display: the count is unknown, never a crash.+ let _ = super::count(true);+ let _ = super::count(false);+ }+}@@ -20,6 +20,8 @@ mod audio; mod control_server; #[cfg(all(unix, not(target_os = "macos")))] mod display;+#[cfg(all(unix, not(target_os = "macos")))]+mod displays; #[cfg(any(target_os = "windows", test))] mod graphics; mod logging;@@ -256,6 +258,11 @@ fn main() -> eframe::Result { options, Box::new(move |cc| { let mut app = SlideApp::new(Session::new(), services());+ // Presenter View asks the display server the window is on how many displays there are.+ #[cfg(all(unix, not(target_os = "macos")))]+ {+ app.services.displays = Some(Box::new(move || displays::count(x11)));+ } load_prefs(&mut app); // AutoRecover: reopen what a previous run left unsaved, then keep it current. if std::env::var_os("DECKCRAFT_NO_RECOVERY").is_none() {@@ -220,6 +220,8 @@ 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>>>, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]@@ -1075,8 +1077,9 @@ 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 = Some(show::Show::new(&d.doc, from, reading, self.ui.presenter_view && !reading));+ self.show = Some(show::Show::new(&d.doc, from, reading, presenter)); } } @@ -1169,6 +1172,14 @@ impl SlideApp { Ok(json!({"playing": true, "slide": slide, "duration": duration})) } + /// 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)+ }+ fn pick_and_open(&mut self) { if let Some(pick) = self.services.pick_open.as_mut() { if let Some(path) = pick("open") {@@ -2203,6 +2214,26 @@ mod tests { assert_eq!(old.qat.len(), 6); } + #[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>| {+ let services = Services { displays: Some(Box::new(move || displays)), ..Default::default() };+ let mut app = SlideApp::new(Session::with_new(), services);+ app.ui.presenter_view = true;+ app.start_show(0, false);+ app.show.as_ref().map(|s| s.presenter)+ };+ assert_eq!(presenter(Some(1)), Some(false));+ assert_eq!(presenter(Some(2)), Some(true));+ assert_eq!(presenter(None), Some(true), "a host that can't count displays keeps Presenter View");+ let mut app = SlideApp::new(Session::with_new(), Services { displays: Some(Box::new(|| Some(2))), ..Default::default() });+ app.ui.presenter_view = false;+ app.start_show(0, false);+ assert_eq!(app.show.as_ref().map(|s| s.presenter), Some(false), "the setting still turns it off");+ }+ #[test] fn opening_a_file_with_missing_fonts_shows_a_toast_once() { use egui_kittest::kittest::Queryable;