mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-03 01:00:48 +00:00
Document dialog outcomes and lifecycle contracts
This commit is contained in:
1 parent
ff19b46588
commit
d6517a8f7f
2 files changed
+30
No files matched your search
@@ -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<String>) -> 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<T> {
|
||||
/// 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<T> Outcome<T> {
|
||||
/// Preserve a selection; an empty rfd answer is indeterminate, never proven cancellation.
|
||||
pub fn from_rfd(value: Option<T>) -> Self {
|
||||
value.map_or(Self::Indeterminate, Self::Selected)
|
||||
}
|
||||
/// Transform only a selected value, preserving all other outcomes unchanged.
|
||||
pub fn map<U>(self, map: impl FnOnce(T) -> U) -> Outcome<U> {
|
||||
match self {
|
||||
Self::Selected(value) => Outcome::Selected(map(value)),
|
||||
@@ -48,11 +65,13 @@ impl<T> Outcome<T> {
|
||||
Self::Indeterminate => Outcome::Indeterminate,
|
||||
}
|
||||
}
|
||||
/// Construct a backend failure with the supplied diagnostic.
|
||||
pub fn failed(message: impl Into<String>) -> Self {
|
||||
Self::Failed(DialogError::new(ErrorKind::Backend, message))
|
||||
}
|
||||
}
|
||||
impl<T> Outcome<Vec<T>> {
|
||||
/// Treat missing and empty multi-selection answers as indeterminate.
|
||||
pub fn from_rfd_files(value: Option<Vec<T>>) -> Self {
|
||||
Self::from_rfd(value.filter(|paths| !paths.is_empty()))
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use std::{
|
||||
},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
/// An already-created native picker future that a reserved worker can wait on.
|
||||
pub type DialogFuture<T> = Pin<Box<dyn Future<Output = Outcome<T>> + Send>>;
|
||||
|
||||
/// Wait for a future without polling an idle native dialog.
|
||||
@@ -76,6 +77,8 @@ impl<R, T> Clone for DialogClient<R, T> {
|
||||
}
|
||||
}
|
||||
impl<R, T> DialogClient<R, T> {
|
||||
/// 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<Outcome<T>> {
|
||||
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<R, T> {
|
||||
check: Arc<dyn Fn() -> Result<(), DialogError> + Send + Sync>,
|
||||
}
|
||||
impl<R, T: Send + 'static> NativeDialogs<R, T> {
|
||||
/// Create a client and its event-loop-owned service without a backend preflight check.
|
||||
pub fn new(wake: impl Fn() + Send + Sync + 'static) -> (DialogClient<R, T>, Self) {
|
||||
Self::with_preflight(wake, || Ok(()))
|
||||
}
|
||||
@@ -140,9 +144,13 @@ impl<R, T: Send + 'static> NativeDialogs<R, T> {
|
||||
let wake: Arc<dyn Fn() + Send + Sync> = 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<T>) {
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user