1use crate::schema::{FileDialogKind, FileDialogStatus};
31use std::cell::RefCell;
32use std::collections::HashMap;
33use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
34use std::sync::{Mutex, MutexGuard};
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38pub enum FileDialogResult {
39 Chosen {
46 paths: Vec<String>,
47 filter: Option<usize>,
48 },
49 Cancelled,
51 Unavailable,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct FileDialog {
59 kind: FileDialogKind,
60 window: u32,
61 title: String,
62 directory: String,
63 name: String,
64 filters: Vec<(String, Vec<String>)>,
65 default_filter: usize,
66}
67
68impl FileDialog {
69 fn new(kind: FileDialogKind) -> Self {
70 Self {
71 kind,
72 window: 0,
73 title: String::new(),
74 directory: String::new(),
75 name: String::new(),
76 filters: Vec::new(),
77 default_filter: 0,
78 }
79 }
80
81 pub fn open_file() -> Self {
83 Self::new(FileDialogKind::OpenFile)
84 }
85
86 pub fn open_files() -> Self {
88 Self::new(FileDialogKind::OpenFiles)
89 }
90
91 pub fn open_folder() -> Self {
93 Self::new(FileDialogKind::OpenFolder)
94 }
95
96 pub fn open_folders() -> Self {
98 Self::new(FileDialogKind::OpenFolders)
99 }
100
101 pub fn save() -> Self {
104 Self::new(FileDialogKind::Save)
105 }
106
107 pub fn title(mut self, title: impl Into<String>) -> Self {
109 self.title = title.into();
110 self
111 }
112
113 pub fn directory(mut self, directory: impl Into<String>) -> Self {
116 self.directory = directory.into();
117 self
118 }
119
120 pub fn name(mut self, name: impl Into<String>) -> Self {
123 self.name = name.into();
124 self
125 }
126
127 pub fn filter(mut self, label: impl Into<String>, extensions: &[&str]) -> Self {
131 self.filters.push((
132 label.into(),
133 extensions
134 .iter()
135 .map(|extension| (*extension).to_owned())
136 .collect(),
137 ));
138 self
139 }
140
141 pub fn default_filter(mut self, position: usize) -> Self {
144 self.default_filter = position;
145 self
146 }
147
148 pub fn window(mut self, node_id: u32) -> Self {
151 self.window = node_id;
152 self
153 }
154
155 pub fn in_window(self, window: crate::windows::WindowHandle) -> Self {
157 self.window(window.id())
158 }
159
160 pub fn show(self, on_result: impl FnOnce(FileDialogResult) + 'static) {
166 let request = next_request();
167 let callback: LocalCallback = Box::new(on_result);
168 LOCAL.with_borrow_mut(|callbacks| {
169 callbacks.insert(request, callback);
170 });
171 submit(self.into_request(request));
172 }
173
174 fn into_request(self, id: u32) -> Request {
175 Request {
176 id,
177 window: self.window,
178 kind: self.kind,
179 default_filter: u16::try_from(self.default_filter).unwrap_or(u16::MAX),
180 title: self.title,
181 directory: self.directory,
182 name: self.name,
183 filters: encode_filters(&self.filters),
184 }
185 }
186}
187
188#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
193pub struct FileDialogSender {
194 _private: (),
195}
196
197impl FileDialogSender {
198 pub const fn new() -> Self {
199 Self { _private: () }
200 }
201
202 pub fn show(
204 &self,
205 dialog: FileDialog,
206 on_result: impl FnOnce(FileDialogResult) + Send + 'static,
207 ) {
208 let request = next_request();
209 let callback: RemoteCallback = Box::new(on_result);
210 remote().push((request, callback));
211 submit(dialog.into_request(request));
212 }
213}
214
215#[derive(Clone, Debug, PartialEq, Eq)]
217pub(crate) struct Request {
218 pub(crate) id: u32,
219 pub(crate) window: u32,
220 pub(crate) kind: FileDialogKind,
221 pub(crate) default_filter: u16,
222 pub(crate) title: String,
223 pub(crate) directory: String,
224 pub(crate) name: String,
225 pub(crate) filters: String,
226}
227
228fn encode_filters(filters: &[(String, Vec<String>)]) -> String {
232 let mut out = String::new();
233 for (label, extensions) in filters {
234 if !out.is_empty() {
235 out.push('\0');
236 }
237 out.extend(label.chars().filter(|c| *c != '\0'));
238 out.push('\0');
239 let mut first = true;
240 for extension in extensions {
241 if !first {
242 out.push(',');
243 }
244 first = false;
245 out.extend(extension.chars().filter(|c| *c != '\0' && *c != ','));
246 }
247 }
248 out
249}
250
251type LocalCallback = Box<dyn FnOnce(FileDialogResult)>;
252type RemoteCallback = Box<dyn FnOnce(FileDialogResult) + Send>;
253
254thread_local! {
255 static LOCAL: RefCell<HashMap<u32, LocalCallback>> = RefCell::new(HashMap::new());
257}
258
259static REMOTE: Mutex<Vec<(u32, RemoteCallback)>> = Mutex::new(Vec::new());
261
262static QUEUE: Mutex<Vec<Request>> = Mutex::new(Vec::new());
264
265static PENDING: AtomicBool = AtomicBool::new(false);
268
269static NEXT_REQUEST: AtomicU32 = AtomicU32::new(1);
271
272fn next_request() -> u32 {
273 loop {
274 let id = NEXT_REQUEST.fetch_add(1, Ordering::Relaxed);
275 if id != 0 {
276 return id;
277 }
278 }
279}
280
281fn queue() -> MutexGuard<'static, Vec<Request>> {
282 QUEUE.lock().unwrap_or_else(|poison| poison.into_inner())
284}
285
286fn remote() -> MutexGuard<'static, Vec<(u32, RemoteCallback)>> {
287 REMOTE.lock().unwrap_or_else(|poison| poison.into_inner())
288}
289
290fn submit(request: Request) {
291 queue().push(request);
292 PENDING.store(true, Ordering::Release);
293 if !crate::boundary::in_host_call() {
296 crate::boundary::request_frame_from_worker();
297 }
298}
299
300pub(crate) fn drain(mut emit: impl FnMut(&Request)) {
302 if !PENDING.swap(false, Ordering::AcqRel) {
303 return;
304 }
305 let mut taken = std::mem::take(&mut *queue());
306 for request in &taken {
307 emit(request);
308 }
309 taken.clear();
310 let mut guard = queue();
311 if guard.is_empty() {
312 *guard = taken;
313 } else {
314 PENDING.store(true, Ordering::Release);
316 }
317}
318
319pub(crate) fn deliver(request: u32, status: FileDialogStatus, filter: u32, paths: &str) -> bool {
324 let result = match status {
325 FileDialogStatus::Chosen => {
326 let paths: Vec<String> = paths
327 .split('\0')
328 .filter(|path| !path.is_empty())
329 .map(str::to_owned)
330 .collect();
331 if paths.is_empty() {
334 FileDialogResult::Unavailable
335 } else {
336 FileDialogResult::Chosen {
337 paths,
338 filter: (filter != u32::MAX).then_some(filter as usize),
339 }
340 }
341 }
342 FileDialogStatus::Cancelled => FileDialogResult::Cancelled,
343 FileDialogStatus::Unavailable => FileDialogResult::Unavailable,
344 };
345 if let Some(callback) = LOCAL.with_borrow_mut(|callbacks| callbacks.remove(&request)) {
348 callback(result);
349 return true;
350 }
351 let callback = {
352 let mut waiting = remote();
353 waiting
354 .iter()
355 .position(|(id, _)| *id == request)
356 .map(|at| waiting.swap_remove(at).1)
357 };
358 match callback {
359 Some(callback) => {
360 callback(result);
361 true
362 }
363 None => false,
364 }
365}
366
367#[doc(hidden)]
369pub fn reset_file_dialogs() {
370 queue().clear();
371 PENDING.store(false, Ordering::Release);
372 remote().clear();
373 LOCAL.with_borrow_mut(HashMap::clear);
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use std::cell::Cell;
380 use std::rc::Rc;
381
382 #[test]
383 fn fr72_filters_are_one_nul_separated_string() {
384 assert_eq!(encode_filters(&[]), "");
385 let filters = vec![
386 (
387 "Images".to_owned(),
388 vec!["png".to_owned(), "jpg".to_owned()],
389 ),
390 ("Docs".to_owned(), vec!["pdf".to_owned(), "txt".to_owned()]),
391 ];
392 assert_eq!(encode_filters(&filters), "Images\0png,jpg\0Docs\0pdf,txt");
393 let hostile = vec![("A\0B".to_owned(), vec!["p,n\0g".to_owned()])];
394 assert_eq!(encode_filters(&hostile), "AB\0png");
395 }
396
397 #[test]
398 fn fr72_paths_are_split_on_nul_and_a_stray_answer_is_ignored() {
399 let seen = Rc::new(RefCell::new(Vec::new()));
400 let sink = Rc::clone(&seen);
401 let request = next_request();
402 let callback: LocalCallback =
403 Box::new(move |result: FileDialogResult| sink.borrow_mut().push(result));
404 LOCAL.with_borrow_mut(|callbacks| {
405 callbacks.insert(request, callback);
406 });
407 assert!(deliver(
408 request,
409 FileDialogStatus::Chosen,
410 1,
411 "/home/한글/문서.txt\0/tmp/a b.png"
412 ));
413 assert_eq!(
414 *seen.borrow(),
415 vec![FileDialogResult::Chosen {
416 paths: vec!["/home/한글/문서.txt".to_owned(), "/tmp/a b.png".to_owned()],
417 filter: Some(1),
418 }]
419 );
420 assert!(!deliver(request, FileDialogStatus::Cancelled, u32::MAX, ""));
422 assert!(!deliver(
423 u32::MAX,
424 FileDialogStatus::Cancelled,
425 u32::MAX,
426 ""
427 ));
428 assert_eq!(seen.borrow().len(), 1);
429 }
430
431 #[test]
432 fn fr72_a_pick_with_no_paths_is_unavailable_not_chosen() {
433 let seen = Rc::new(Cell::new(None));
434 let sink = Rc::clone(&seen);
435 let request = next_request();
436 let callback: LocalCallback =
437 Box::new(move |result: FileDialogResult| sink.set(Some(result)));
438 LOCAL.with_borrow_mut(|callbacks| {
439 callbacks.insert(request, callback);
440 });
441 deliver(request, FileDialogStatus::Chosen, u32::MAX, "");
442 assert_eq!(seen.take(), Some(FileDialogResult::Unavailable));
443 }
444
445 #[test]
446 fn fr72_a_worker_callback_is_called_once_with_its_own_answer() {
447 let seen = std::sync::Arc::new(Mutex::new(Vec::new()));
448 let sink = std::sync::Arc::clone(&seen);
449 let request = next_request();
450 let callback: RemoteCallback =
451 Box::new(move |result: FileDialogResult| sink.lock().unwrap().push(result));
452 remote().push((request, callback));
453 assert!(deliver(
454 request,
455 FileDialogStatus::Unavailable,
456 u32::MAX,
457 ""
458 ));
459 assert!(!deliver(
460 request,
461 FileDialogStatus::Unavailable,
462 u32::MAX,
463 ""
464 ));
465 assert_eq!(*seen.lock().unwrap(), vec![FileDialogResult::Unavailable]);
466 }
467}