From d6517a8f7fdb7585a539d796b04a3cabb4fbefd3 Mon Sep 17 00:00:00 2001 From: skylartaylor Date: Thu, 22 Oct 2026 00:38:07 +0200 Subject: [PATCH] Document dialog outcomes and lifecycle contracts --- crates/platform/src/dialog_outcome.rs | 19 +++++++++++++++++++ crates/platform/src/dialogs.rs | 11 +++++++++++ 2 files changed, 30 insertions(+) diff --git a/crates/platform/src/dialog_outcome.rs b/crates/platform/src/dialog_outcome.rs index fb7f5d64..7f675002 100644 --- a/crates/platform/src/dialog_outcome.rs +++ b/crates/platform/src/dialog_outcome.rs @@ -1,22 +1,34 @@ //! Dialog results with evidence: an empty rfd answer does not establish user cancellation. +/// Host assessment of whether a native backend is available; unknown is not proven absence. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Capability { + /// The host has confirmed that a backend is available. Available, + /// Proven backend absence, with a diagnostic suitable for the host. Unavailable(String), + /// Inconclusive capability evidence; the host should still permit a picker attempt. Unknown(String), } +/// The layer that prevented a dialog request from completing. #[derive(Clone, Debug, PartialEq, Eq)] pub enum ErrorKind { + /// The host proved that no backend is available. Unavailable, + /// Request transport or background execution failed. Worker, + /// The picker backend reported or encountered a failure. Backend, } +/// A classified dialog failure with a host-facing diagnostic. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DialogError { + /// The failing layer. pub kind: ErrorKind, + /// Diagnostic text for the host to present or log. pub message: String, } impl DialogError { + /// Construct a failure without inferring cancellation from the diagnostic. pub fn new(kind: ErrorKind, message: impl Into) -> Self { Self { kind, message: message.into() } } @@ -27,19 +39,24 @@ impl std::fmt::Display for DialogError { } } impl std::error::Error for DialogError {} +/// Selection, explicit cancellation, failure, or an answer whose cause is unknown. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Outcome { + /// A value returned by the picker. Selected(T), /// Explicit app/backend cancellation; never inferred from rfd's empty answer. Cancelled, + /// A transport, capability or backend failure. Failed(DialogError), /// No selection; rfd cannot say whether the user cancelled or its backend failed. Indeterminate, } impl Outcome { + /// Preserve a selection; an empty rfd answer is indeterminate, never proven cancellation. pub fn from_rfd(value: Option) -> Self { value.map_or(Self::Indeterminate, Self::Selected) } + /// Transform only a selected value, preserving all other outcomes unchanged. pub fn map(self, map: impl FnOnce(T) -> U) -> Outcome { match self { Self::Selected(value) => Outcome::Selected(map(value)), @@ -48,11 +65,13 @@ impl Outcome { Self::Indeterminate => Outcome::Indeterminate, } } + /// Construct a backend failure with the supplied diagnostic. pub fn failed(message: impl Into) -> Self { Self::Failed(DialogError::new(ErrorKind::Backend, message)) } } impl Outcome> { + /// Treat missing and empty multi-selection answers as indeterminate. pub fn from_rfd_files(value: Option>) -> Self { Self::from_rfd(value.filter(|paths| !paths.is_empty())) } diff --git a/crates/platform/src/dialogs.rs b/crates/platform/src/dialogs.rs index a9ca9a16..b09c66be 100644 --- a/crates/platform/src/dialogs.rs +++ b/crates/platform/src/dialogs.rs @@ -14,6 +14,7 @@ use std::{ }, thread::JoinHandle, }; +/// An already-created native picker future that a reserved worker can wait on. pub type DialogFuture = Pin> + Send>>; /// Wait for a future without polling an idle native dialog. @@ -76,6 +77,8 @@ impl Clone for DialogClient { } } impl DialogClient { + /// Queue a request without blocking and wake the host to pump it. + /// A busy or unavailable host answers with a worker failure; no second picker is opened. pub fn request(&self, request: R) -> Receiver> { let (answer, receive) = mpsc::channel(); if self.busy.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() { @@ -125,6 +128,7 @@ pub struct NativeDialogs { check: Arc Result<(), DialogError> + Send + Sync>, } impl NativeDialogs { + /// Create a client and its event-loop-owned service without a backend preflight check. pub fn new(wake: impl Fn() + Send + Sync + 'static) -> (DialogClient, Self) { Self::with_preflight(wake, || Ok(())) } @@ -140,9 +144,13 @@ impl NativeDialogs { let wake: Arc = Arc::new(wake); (DialogClient { sender, busy: busy.clone(), wake: wake.clone() }, Self { queue, busy, state: None, wake, check: Arc::new(check) }) } + /// Whether a queued, preparing or live picker still owns the transport reservation. pub fn pending(&self) -> bool { self.busy.load(Ordering::SeqCst) } + /// Advance requests on the event-loop thread and publish completed replies. + /// Call before consuming replies and after enqueuing work. `create` must eagerly open + /// the native picker on this thread and return its already-created future. pub fn pump(&mut self, mut create: impl FnMut(R) -> DialogFuture) { if let Some(state) = self.state.take() { match state { @@ -216,6 +224,9 @@ pub struct DeferredClose { } #[cfg(feature = "egui")] impl DeferredClose { + /// Hold actual close requests while a picker or app continuation is pending. + /// Return `true` once after settlement so the host retries its unsaved-work policy; + /// this never grants permission to discard changes or closes the viewport itself. pub fn update(&mut self, ctx: &egui::Context, pending: bool) -> bool { let outgoing = ctx.viewport(|v| { v.commands