1use crate::request_channel::{RequestChannel, RequestReceiver};
3
4use crate::tasks::RuntimeWake;
5use incular_platform::{
6 CapabilitySupport, DocumentDescriptor, FileContentTypeKind, FileDialogCapabilities,
7 FileDialogError, FileDialogKind, FileDialogOption, FileDialogOptions, FileDialogOutcome,
8 FileDialogRequest as PortableRequest, FileDialogRequestId, FileDialogSelection,
9 PlatformCapabilities, WindowId,
10};
11use std::{
12 collections::VecDeque,
13 future::Future,
14 marker::PhantomData,
15 path::PathBuf,
16 pin::Pin,
17 sync::{
18 Arc, RwLock,
19 atomic::{AtomicBool, Ordering},
20 mpsc,
21 },
22 task::{Context, Poll},
23};
24use tokio::sync::oneshot;
25
26type FileDialogResult = Result<FileDialogOutcome, FileDialogError>;
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct NativeFileDialogRequest {
31 pub request_id: FileDialogRequestId,
32 pub window_id: WindowId,
33 pub request: PortableRequest,
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct NativeFileDialogCompletion {
40 pub request_id: FileDialogRequestId,
41 pub window_id: WindowId,
42 pub result: FileDialogResult,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum FileDialogCompletionStatus {
47 Completed,
48 InvalidResult,
49 UnknownRequest,
50 TargetMismatch,
51}
52
53#[derive(Clone)]
57pub struct FileDialogService {
58 window_id: WindowId,
59 bridge: Arc<FileDialogBridge>,
60 capabilities: Arc<RwLock<PlatformCapabilities>>,
61}
62
63impl std::fmt::Debug for FileDialogService {
64 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 formatter
66 .debug_tuple("FileDialogService")
67 .field(&self.window_id)
68 .finish()
69 }
70}
71
72impl FileDialogService {
73 pub(crate) fn new(
74 window_id: WindowId,
75 bridge: Arc<FileDialogBridge>,
76 capabilities: Arc<RwLock<PlatformCapabilities>>,
77 ) -> Self {
78 Self {
79 window_id,
80 bridge,
81 capabilities,
82 }
83 }
84
85 #[must_use]
86 pub const fn window_id(&self) -> WindowId {
87 self.window_id
88 }
89
90 #[must_use]
91 pub fn capabilities(&self) -> FileDialogCapabilities {
92 self.capabilities
93 .read()
94 .expect("window capability snapshot lock")
95 .application_services
96 .file_dialogs
97 }
98
99 pub async fn open_file(
102 &self,
103 options: FileDialogOptions,
104 ) -> Result<Option<DocumentDescriptor>, FileDialogError> {
105 self.request_open_file(options)?.await
106 }
107
108 pub async fn open_files(
111 &self,
112 options: FileDialogOptions,
113 ) -> Result<Option<Vec<DocumentDescriptor>>, FileDialogError> {
114 self.request_open_files(options)?.await
115 }
116
117 pub async fn save_file(
120 &self,
121 options: FileDialogOptions,
122 ) -> Result<Option<DocumentDescriptor>, FileDialogError> {
123 self.request_save_file(options)?.await
124 }
125
126 pub async fn select_folder(
128 &self,
129 options: FileDialogOptions,
130 ) -> Result<Option<PathBuf>, FileDialogError> {
131 self.request_select_folder(options)?.await
132 }
133
134 pub fn request_open_file(
137 &self,
138 options: FileDialogOptions,
139 ) -> Result<FileDialogRequest<Option<DocumentDescriptor>>, FileDialogError> {
140 self.request(FileDialogKind::OpenFile, options, map_single_document)
141 }
142
143 pub fn request_open_files(
145 &self,
146 options: FileDialogOptions,
147 ) -> Result<FileDialogRequest<Option<Vec<DocumentDescriptor>>>, FileDialogError> {
148 self.request(FileDialogKind::OpenFiles, options, map_multiple_documents)
149 }
150
151 pub fn request_save_file(
153 &self,
154 options: FileDialogOptions,
155 ) -> Result<FileDialogRequest<Option<DocumentDescriptor>>, FileDialogError> {
156 self.request(FileDialogKind::SaveFile, options, map_single_document)
157 }
158
159 pub fn request_select_folder(
161 &self,
162 options: FileDialogOptions,
163 ) -> Result<FileDialogRequest<Option<PathBuf>>, FileDialogError> {
164 self.request(FileDialogKind::SelectFolder, options, map_folder)
165 }
166
167 fn request<T>(
168 &self,
169 kind: FileDialogKind,
170 options: FileDialogOptions,
171 map: fn(FileDialogResult) -> Result<T, FileDialogError>,
172 ) -> Result<FileDialogRequest<T>, FileDialogError> {
173 let request = PortableRequest::new(kind, options).map_err(FileDialogError::from)?;
174 let raw = self.bridge.request(self.window_id, request)?;
175 Ok(FileDialogRequest {
176 raw,
177 map,
178 marker: PhantomData,
179 })
180 }
181}
182
183fn map_single_document(
184 result: FileDialogResult,
185) -> Result<Option<DocumentDescriptor>, FileDialogError> {
186 match result? {
187 FileDialogOutcome::Cancelled => Ok(None),
188 FileDialogOutcome::Selected(FileDialogSelection::Documents(activation))
189 if activation.documents().len() == 1 =>
190 {
191 Ok(activation.documents().first().cloned())
192 }
193 FileDialogOutcome::Selected(_) => Err(FileDialogError::InvalidResult),
194 }
195}
196
197fn map_multiple_documents(
198 result: FileDialogResult,
199) -> Result<Option<Vec<DocumentDescriptor>>, FileDialogError> {
200 match result? {
201 FileDialogOutcome::Cancelled => Ok(None),
202 FileDialogOutcome::Selected(FileDialogSelection::Documents(activation))
203 if !activation.is_empty() =>
204 {
205 Ok(Some(activation.documents().to_vec()))
206 }
207 FileDialogOutcome::Selected(_) => Err(FileDialogError::InvalidResult),
208 }
209}
210
211fn map_folder(result: FileDialogResult) -> Result<Option<PathBuf>, FileDialogError> {
212 match result? {
213 FileDialogOutcome::Cancelled => Ok(None),
214 FileDialogOutcome::Selected(FileDialogSelection::Folder(path)) => Ok(Some(path)),
215 FileDialogOutcome::Selected(_) => Err(FileDialogError::InvalidResult),
216 }
217}
218
219#[must_use = "file-dialog requests must be awaited, inspected, or explicitly dropped"]
225pub struct FileDialogRequest<T> {
226 raw: RawFileDialogRequest,
227 map: fn(FileDialogResult) -> Result<T, FileDialogError>,
228 marker: PhantomData<T>,
229}
230
231impl<T> Unpin for FileDialogRequest<T> {}
232
233impl<T> FileDialogRequest<T> {
234 #[must_use]
235 pub const fn id(&self) -> FileDialogRequestId {
236 self.raw.id
237 }
238
239 pub fn try_result(&mut self) -> Option<Result<T, FileDialogError>> {
240 self.raw.try_result().map(self.map)
241 }
242}
243
244impl<T> Future for FileDialogRequest<T> {
245 type Output = Result<T, FileDialogError>;
246
247 fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
248 match Pin::new(&mut self.as_mut().get_mut().raw).poll(context) {
249 Poll::Ready(result) => Poll::Ready((self.map)(result)),
250 Poll::Pending => Poll::Pending,
251 }
252 }
253}
254
255struct RawFileDialogRequest {
256 id: FileDialogRequestId,
257 receiver: RequestReceiver<FileDialogResult>,
258}
259impl RawFileDialogRequest {
260 fn try_result(&mut self) -> Option<FileDialogResult> {
261 self.receiver
262 .try_result(|| Err(FileDialogError::ApplicationStopped))
263 }
264}
265impl Future for RawFileDialogRequest {
266 type Output = FileDialogResult;
267 fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
268 self.receiver
269 .poll_result(context, || Err(FileDialogError::ApplicationStopped))
270 }
271}
272
273pub(crate) struct QueuedFileDialogRequest {
274 pub(crate) request_id: FileDialogRequestId,
275 pub(crate) window_id: WindowId,
276 pub(crate) request: PortableRequest,
277 pub(crate) sender: oneshot::Sender<FileDialogResult>,
278 pub(crate) cancelled: Arc<AtomicBool>,
279}
280
281pub(crate) struct FileDialogBridge {
282 channel: RequestChannel<QueuedFileDialogRequest>,
283 cancellation_sender: mpsc::Sender<FileDialogRequestId>,
284}
285impl FileDialogBridge {
286 pub(crate) fn new(
287 sender: mpsc::Sender<QueuedFileDialogRequest>,
288 cancellation_sender: mpsc::Sender<FileDialogRequestId>,
289 ) -> Self {
290 Self {
291 channel: RequestChannel::new(sender),
292 cancellation_sender,
293 }
294 }
295 fn request(
296 self: &Arc<Self>,
297 window_id: WindowId,
298 request: PortableRequest,
299 ) -> Result<RawFileDialogRequest, FileDialogError> {
300 self.channel
301 .request(|id| {
302 let request_id = FileDialogRequestId::new(id);
303 let (sender, receiver) = oneshot::channel();
304 let cancelled = Arc::new(AtomicBool::new(false));
305 let queued = QueuedFileDialogRequest {
306 request_id,
307 window_id,
308 request,
309 sender,
310 cancelled: cancelled.clone(),
311 };
312 let bridge = self.clone();
313 let receiver = RequestReceiver::new(receiver, move || {
314 cancelled.store(true, Ordering::Release);
315 bridge.cancel(request_id);
316 });
317 (
318 queued,
319 RawFileDialogRequest {
320 id: request_id,
321 receiver,
322 },
323 )
324 })
325 .map_err(|_| FileDialogError::ApplicationStopped)
326 }
327 fn cancel(&self, id: FileDialogRequestId) {
328 self.channel.cancel(&self.cancellation_sender, id);
329 }
330 pub(crate) fn set_wake(&self, wake: Arc<dyn RuntimeWake>) {
331 self.channel.set_wake(wake);
332 }
333 pub(crate) fn stop(&self) {
334 self.channel.stop();
335 }
336}
337
338#[derive(Debug)]
342pub struct MemoryFileDialogAdapter {
343 capabilities: FileDialogCapabilities,
344 scripted: VecDeque<FileDialogResult>,
345 observed: Vec<NativeFileDialogRequest>,
346}
347
348impl Default for MemoryFileDialogAdapter {
349 fn default() -> Self {
350 Self {
351 capabilities: FileDialogCapabilities::all(CapabilitySupport::Supported),
352 scripted: VecDeque::new(),
353 observed: Vec::new(),
354 }
355 }
356}
357
358impl MemoryFileDialogAdapter {
359 #[must_use]
360 pub const fn capabilities(&self) -> FileDialogCapabilities {
361 self.capabilities
362 }
363
364 #[must_use]
365 pub fn with_capabilities(mut self, capabilities: FileDialogCapabilities) -> Self {
366 self.capabilities = capabilities;
367 self
368 }
369
370 pub fn push_result(&mut self, result: FileDialogResult) {
371 self.scripted.push_back(result);
372 }
373
374 pub fn respond(&mut self, request: NativeFileDialogRequest) -> NativeFileDialogCompletion {
375 let result = self
376 .capabilities
377 .validate_request(&request.request)
378 .map(|()| {
379 self.scripted
380 .pop_front()
381 .unwrap_or(Ok(FileDialogOutcome::Cancelled))
382 })
383 .unwrap_or_else(Err);
384 self.observed.push(request.clone());
385 NativeFileDialogCompletion {
386 request_id: request.request_id,
387 window_id: request.window_id,
388 result,
389 }
390 }
391
392 #[must_use]
393 pub fn observed(&self) -> &[NativeFileDialogRequest] {
394 &self.observed
395 }
396}
397
398pub(crate) fn explicitly_unsupported_request(
399 capabilities: FileDialogCapabilities,
400 request: &PortableRequest,
401) -> Option<FileDialogError> {
402 if capabilities.operation(request.kind) == CapabilitySupport::Unsupported {
403 return Some(FileDialogError::UnsupportedOperation(request.kind));
404 }
405 if capabilities.parent_window == CapabilitySupport::Unsupported {
406 return Some(FileDialogError::UnsupportedOption(
407 FileDialogOption::ParentWindow,
408 ));
409 }
410 if request
411 .options
412 .filters_value()
413 .iter()
414 .any(|filter| !filter.extensions_list().is_empty())
415 && capabilities.extension_filters == CapabilitySupport::Unsupported
416 {
417 return Some(FileDialogError::UnsupportedOption(
418 FileDialogOption::ExtensionFilters,
419 ));
420 }
421 for content_type in request
422 .options
423 .filters_value()
424 .iter()
425 .flat_map(|filter| filter.content_types_list())
426 {
427 let (support, option) = match content_type.kind() {
428 FileContentTypeKind::Mime => (
429 capabilities.mime_type_filters,
430 FileDialogOption::MimeTypeFilters,
431 ),
432 FileContentTypeKind::TypeIdentifier => (
433 capabilities.type_identifier_filters,
434 FileDialogOption::TypeIdentifierFilters,
435 ),
436 };
437 if support == CapabilitySupport::Unsupported {
438 return Some(FileDialogError::UnsupportedOption(option));
439 }
440 }
441 if request.options.suggested_file_name_value().is_some()
442 && capabilities.suggested_file_name == CapabilitySupport::Unsupported
443 {
444 return Some(FileDialogError::UnsupportedOption(
445 FileDialogOption::SuggestedFileName,
446 ));
447 }
448 if request.options.suggested_directory_value().is_some()
449 && capabilities.suggested_directory == CapabilitySupport::Unsupported
450 {
451 return Some(FileDialogError::UnsupportedOption(
452 FileDialogOption::SuggestedDirectory,
453 ));
454 }
455 None
456}
457
458pub(crate) fn result_matches_request(
459 request: &PortableRequest,
460 outcome: &FileDialogOutcome,
461) -> bool {
462 match (request.kind, outcome) {
463 (_, FileDialogOutcome::Cancelled) => true,
464 (
465 FileDialogKind::OpenFile | FileDialogKind::SaveFile,
466 FileDialogOutcome::Selected(FileDialogSelection::Documents(activation)),
467 ) => activation.documents().len() == 1,
468 (
469 FileDialogKind::OpenFiles,
470 FileDialogOutcome::Selected(FileDialogSelection::Documents(activation)),
471 ) => !activation.is_empty(),
472 (
473 FileDialogKind::SelectFolder,
474 FileDialogOutcome::Selected(FileDialogSelection::Folder(_)),
475 ) => true,
476 _ => false,
477 }
478}