Files
deckcraft/crates/ui-egui/src/services.rs
T
Michael Müllerandaiongg 73de9900a7 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>
2026-11-05 12:51:16 +01:00

77 lines
3.8 KiB
Rust

pub type ReadFn = Box<dyn Fn(&str) -> Result<Vec<u8>, String>>;
pub type WriteFn = Box<dyn FnMut(&str, &[u8]) -> Result<(), String>>;
pub type PickFn = Box<dyn FnMut(&str) -> Option<String>>;
/// `(purpose, request id)`. The host shows a picker and later pushes an [`InboxEvent`]
/// carrying that same id. Completing a cancelled or unknown id does nothing: the file is
/// not applied to whichever document happens to be active.
pub type OpenAsyncFn = Box<dyn FnMut(&str, u64)>;
pub type DownloadFn = Box<dyn FnMut(&str, &[u8])>;
/// [`Services::host_save`]: `(file name, bytes, save_as)`. An error means the page didn't get
/// the file, and the save failed.
pub type HostSaveFn = Box<dyn FnMut(&str, &[u8], bool) -> Result<(), String>>;
/// A file the host finished reading for the request `id` from [`SlideApp::begin_external_file`].
#[derive(Clone, Debug)]
pub struct InboxFile {
/// Cancellation identity. A file whose id was cancelled, or never started, is ignored.
pub id: u64,
pub name: String,
pub bytes: Vec<u8>,
}
/// What the host delivers through [`Inbox`] after an asynchronous read.
#[derive(Clone, Debug)]
pub enum InboxEvent {
File(InboxFile),
/// The picker was dismissed or the file could not be read. `id` will not be applied.
Cancelled {
id: u64,
},
}
/// Files delivered asynchronously by the host (web file picker, dropped files).
pub type Inbox = std::sync::Arc<std::sync::Mutex<Vec<InboxEvent>>>;
/// Platform services injected by the host (desktop or web).
#[derive(Default)]
pub struct Services {
/// Browser-owned presenter popup; absent on native hosts.
pub presenter: Option<Box<dyn crate::presenter::Host>>,
/// Open-file dialog for a purpose (`open`, `picture`, `audio`, `video`) → path.
pub pick_open: Option<PickFn>,
/// Save dialog with a suggested name → path.
pub pick_save: Option<PickFn>,
pub read: Option<ReadFn>,
pub write: Option<WriteFn>,
/// Asynchronous open dialog (web). The callback receives the purpose and the request id
/// from [`SlideApp::begin_external_file`]; the chosen file arrives through `inbox`.
pub open_async: Option<OpenAsyncFn>,
/// Hand bytes to the user as a named file (browser download).
pub download: Option<DownloadFn>,
/// Web, framed by a page that keeps the files (`?host=parent`): Save and Save As hand the
/// presentation to that page instead of downloading it, with whether it is a new file
/// ([`HostSaveFn`]). Save writes a presentation the page sent back under its own name and
/// format, as the desktop app writes back to the file it opened; a presentation the page
/// never sent (an untitled one, a repaired copy) goes as a new `.deckcraft` file, as Save
/// suggests on the desktop. Save As (.pptx or .deckcraft) is always a new file. Either way the
/// presentation is then that file, and the next Save goes to it. Exports and Print still go
/// through `download`, which the host also points at the page: they are copies.
pub host_save: Option<HostSaveFn>,
pub inbox: Option<Inbox>,
/// Image on the system clipboard (PNG bytes), when the host can read one.
pub clipboard_image: Option<Box<dyn FnMut() -> Option<Vec<u8>>>>,
/// HTML on the system clipboard (Pages, Keynote, and other apps that write `public.html`).
pub clipboard_html: Option<Box<dyn FnMut() -> Option<String>>>,
/// Put HTML and its plain-text alternative on the system clipboard. `true` when it landed.
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>>>,
}