use crate::schema::{FileDialogKind, FileDialogStatus};
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Mutex, MutexGuard};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum FileDialogResult {
Chosen {
paths: Vec<String>,
filter: Option<usize>,
},
Cancelled,
Unavailable,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileDialog {
kind: FileDialogKind,
window: u32,
title: String,
directory: String,
name: String,
filters: Vec<(String, Vec<String>)>,
default_filter: usize,
}
impl FileDialog {
fn new(kind: FileDialogKind) -> Self {
Self {
kind,
window: 0,
title: String::new(),
directory: String::new(),
name: String::new(),
filters: Vec::new(),
default_filter: 0,
}
}
pub fn open_file() -> Self {
Self::new(FileDialogKind::OpenFile)
}
pub fn open_files() -> Self {
Self::new(FileDialogKind::OpenFiles)
}
pub fn open_folder() -> Self {
Self::new(FileDialogKind::OpenFolder)
}
pub fn open_folders() -> Self {
Self::new(FileDialogKind::OpenFolders)
}
pub fn save() -> Self {
Self::new(FileDialogKind::Save)
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn directory(mut self, directory: impl Into<String>) -> Self {
self.directory = directory.into();
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn filter(mut self, label: impl Into<String>, extensions: &[&str]) -> Self {
self.filters.push((
label.into(),
extensions
.iter()
.map(|extension| (*extension).to_owned())
.collect(),
));
self
}
pub fn default_filter(mut self, position: usize) -> Self {
self.default_filter = position;
self
}
pub fn window(mut self, node_id: u32) -> Self {
self.window = node_id;
self
}
pub fn in_window(self, window: crate::windows::WindowHandle) -> Self {
self.window(window.id())
}
pub fn show(self, on_result: impl FnOnce(FileDialogResult) + 'static) {
let request = next_request();
let callback: LocalCallback = Box::new(on_result);
LOCAL.with_borrow_mut(|callbacks| {
callbacks.insert(request, callback);
});
submit(self.into_request(request));
}
fn into_request(self, id: u32) -> Request {
Request {
id,
window: self.window,
kind: self.kind,
default_filter: u16::try_from(self.default_filter).unwrap_or(u16::MAX),
title: self.title,
directory: self.directory,
name: self.name,
filters: encode_filters(&self.filters),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FileDialogSender {
_private: (),
}
impl FileDialogSender {
pub const fn new() -> Self {
Self { _private: () }
}
pub fn show(
&self,
dialog: FileDialog,
on_result: impl FnOnce(FileDialogResult) + Send + 'static,
) {
let request = next_request();
let callback: RemoteCallback = Box::new(on_result);
remote().push((request, callback));
submit(dialog.into_request(request));
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Request {
pub(crate) id: u32,
pub(crate) window: u32,
pub(crate) kind: FileDialogKind,
pub(crate) default_filter: u16,
pub(crate) title: String,
pub(crate) directory: String,
pub(crate) name: String,
pub(crate) filters: String,
}
fn encode_filters(filters: &[(String, Vec<String>)]) -> String {
let mut out = String::new();
for (label, extensions) in filters {
if !out.is_empty() {
out.push('\0');
}
out.extend(label.chars().filter(|c| *c != '\0'));
out.push('\0');
let mut first = true;
for extension in extensions {
if !first {
out.push(',');
}
first = false;
out.extend(extension.chars().filter(|c| *c != '\0' && *c != ','));
}
}
out
}
type LocalCallback = Box<dyn FnOnce(FileDialogResult)>;
type RemoteCallback = Box<dyn FnOnce(FileDialogResult) + Send>;
thread_local! {
static LOCAL: RefCell<HashMap<u32, LocalCallback>> = RefCell::new(HashMap::new());
}
static REMOTE: Mutex<Vec<(u32, RemoteCallback)>> = Mutex::new(Vec::new());
static QUEUE: Mutex<Vec<Request>> = Mutex::new(Vec::new());
static PENDING: AtomicBool = AtomicBool::new(false);
static NEXT_REQUEST: AtomicU32 = AtomicU32::new(1);
fn next_request() -> u32 {
loop {
let id = NEXT_REQUEST.fetch_add(1, Ordering::Relaxed);
if id != 0 {
return id;
}
}
}
fn queue() -> MutexGuard<'static, Vec<Request>> {
QUEUE.lock().unwrap_or_else(|poison| poison.into_inner())
}
fn remote() -> MutexGuard<'static, Vec<(u32, RemoteCallback)>> {
REMOTE.lock().unwrap_or_else(|poison| poison.into_inner())
}
fn submit(request: Request) {
queue().push(request);
PENDING.store(true, Ordering::Release);
if !crate::boundary::in_host_call() {
crate::boundary::request_frame_from_worker();
}
}
pub(crate) fn drain(mut emit: impl FnMut(&Request)) {
if !PENDING.swap(false, Ordering::AcqRel) {
return;
}
let mut taken = std::mem::take(&mut *queue());
for request in &taken {
emit(request);
}
taken.clear();
let mut guard = queue();
if guard.is_empty() {
*guard = taken;
} else {
PENDING.store(true, Ordering::Release);
}
}
pub(crate) fn deliver(request: u32, status: FileDialogStatus, filter: u32, paths: &str) -> bool {
let result = match status {
FileDialogStatus::Chosen => {
let paths: Vec<String> = paths
.split('\0')
.filter(|path| !path.is_empty())
.map(str::to_owned)
.collect();
if paths.is_empty() {
FileDialogResult::Unavailable
} else {
FileDialogResult::Chosen {
paths,
filter: (filter != u32::MAX).then_some(filter as usize),
}
}
}
FileDialogStatus::Cancelled => FileDialogResult::Cancelled,
FileDialogStatus::Unavailable => FileDialogResult::Unavailable,
};
if let Some(callback) = LOCAL.with_borrow_mut(|callbacks| callbacks.remove(&request)) {
callback(result);
return true;
}
let callback = {
let mut waiting = remote();
waiting
.iter()
.position(|(id, _)| *id == request)
.map(|at| waiting.swap_remove(at).1)
};
match callback {
Some(callback) => {
callback(result);
true
}
None => false,
}
}
#[doc(hidden)]
pub fn reset_file_dialogs() {
queue().clear();
PENDING.store(false, Ordering::Release);
remote().clear();
LOCAL.with_borrow_mut(HashMap::clear);
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::rc::Rc;
#[test]
fn fr72_filters_are_one_nul_separated_string() {
assert_eq!(encode_filters(&[]), "");
let filters = vec![
(
"Images".to_owned(),
vec!["png".to_owned(), "jpg".to_owned()],
),
("Docs".to_owned(), vec!["pdf".to_owned(), "txt".to_owned()]),
];
assert_eq!(encode_filters(&filters), "Images\0png,jpg\0Docs\0pdf,txt");
let hostile = vec![("A\0B".to_owned(), vec!["p,n\0g".to_owned()])];
assert_eq!(encode_filters(&hostile), "AB\0png");
}
#[test]
fn fr72_paths_are_split_on_nul_and_a_stray_answer_is_ignored() {
let seen = Rc::new(RefCell::new(Vec::new()));
let sink = Rc::clone(&seen);
let request = next_request();
let callback: LocalCallback =
Box::new(move |result: FileDialogResult| sink.borrow_mut().push(result));
LOCAL.with_borrow_mut(|callbacks| {
callbacks.insert(request, callback);
});
assert!(deliver(
request,
FileDialogStatus::Chosen,
1,
"/home/한글/문서.txt\0/tmp/a b.png"
));
assert_eq!(
*seen.borrow(),
vec![FileDialogResult::Chosen {
paths: vec!["/home/한글/문서.txt".to_owned(), "/tmp/a b.png".to_owned()],
filter: Some(1),
}]
);
assert!(!deliver(request, FileDialogStatus::Cancelled, u32::MAX, ""));
assert!(!deliver(
u32::MAX,
FileDialogStatus::Cancelled,
u32::MAX,
""
));
assert_eq!(seen.borrow().len(), 1);
}
#[test]
fn fr72_a_pick_with_no_paths_is_unavailable_not_chosen() {
let seen = Rc::new(Cell::new(None));
let sink = Rc::clone(&seen);
let request = next_request();
let callback: LocalCallback =
Box::new(move |result: FileDialogResult| sink.set(Some(result)));
LOCAL.with_borrow_mut(|callbacks| {
callbacks.insert(request, callback);
});
deliver(request, FileDialogStatus::Chosen, u32::MAX, "");
assert_eq!(seen.take(), Some(FileDialogResult::Unavailable));
}
#[test]
fn fr72_a_worker_callback_is_called_once_with_its_own_answer() {
let seen = std::sync::Arc::new(Mutex::new(Vec::new()));
let sink = std::sync::Arc::clone(&seen);
let request = next_request();
let callback: RemoteCallback =
Box::new(move |result: FileDialogResult| sink.lock().unwrap().push(result));
remote().push((request, callback));
assert!(deliver(
request,
FileDialogStatus::Unavailable,
u32::MAX,
""
));
assert!(!deliver(
request,
FileDialogStatus::Unavailable,
u32::MAX,
""
));
assert_eq!(*seen.lock().unwrap(), vec![FileDialogResult::Unavailable]);
}
}