use std::path::PathBuf;
use std::sync::Mutex;
use winit::raw_window_handle::{
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
RawWindowHandle, WindowHandle,
};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FileDialog {
title: Option<String>,
directory: Option<PathBuf>,
file_name: Option<String>,
filters: Vec<(String, Vec<String>)>,
}
impl FileDialog {
pub fn new() -> Self {
Self::default()
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn directory(mut self, directory: impl Into<PathBuf>) -> Self {
self.directory = Some(directory.into());
self
}
pub fn file_name(mut self, name: impl Into<String>) -> Self {
self.file_name = Some(name.into());
self
}
pub fn filter(mut self, name: impl Into<String>, extensions: &[&str]) -> Self {
let extensions = extensions
.iter()
.map(|e| e.trim_start_matches('*').trim_start_matches('.').to_owned())
.filter(|e| !e.is_empty())
.collect();
self.filters.push((name.into(), extensions));
self
}
pub fn pick_file(self) -> Option<PathBuf> {
let dialog = self.native("pick_file")?;
pollster::block_on(dialog.pick_file()).map(|f| f.path().to_owned())
}
pub fn pick_files(self) -> Vec<PathBuf> {
let Some(dialog) = self.native("pick_files") else {
return Vec::new();
};
pollster::block_on(dialog.pick_files())
.unwrap_or_default()
.iter()
.map(|f| f.path().to_owned())
.collect()
}
pub fn pick_folder(self) -> Option<PathBuf> {
let dialog = self.native("pick_folder")?;
pollster::block_on(dialog.pick_folder()).map(|f| f.path().to_owned())
}
pub fn save_file(self) -> Option<PathBuf> {
let dialog = self.native("save_file")?;
pollster::block_on(dialog.save_file()).map(|f| f.path().to_owned())
}
fn native(self, what: &str) -> Option<rfd::AsyncFileDialog> {
if crate::interface_thread::current() {
log::error!(
"FileDialog::{what} was called on the interface's thread, which it would stop: \
call it from Command::perform, or from spawn in a component"
);
return None;
}
let mut dialog = rfd::AsyncFileDialog::new();
if let Some(title) = self.title {
dialog = dialog.set_title(title);
}
if let Some(directory) = self.directory {
dialog = dialog.set_directory(directory);
}
if let Some(name) = self.file_name {
dialog = dialog.set_file_name(name);
}
for (name, extensions) in &self.filters {
dialog = dialog.add_filter(name, extensions.as_slice());
}
if let Some(parent) = PARENT.lock().ok().and_then(|parent| *parent) {
dialog = dialog.set_parent(&parent);
}
Some(dialog)
}
}
#[derive(Clone, Copy)]
struct Parent {
window: RawWindowHandle,
display: RawDisplayHandle,
}
unsafe impl Send for Parent {}
impl HasWindowHandle for Parent {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
Ok(unsafe { WindowHandle::borrow_raw(self.window) })
}
}
impl HasDisplayHandle for Parent {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
Ok(unsafe { DisplayHandle::borrow_raw(self.display) })
}
}
static PARENT: Mutex<Option<Parent>> = Mutex::new(None);
pub(crate) fn window_made(window: &winit::window::Window) {
let parent = match (window.window_handle(), window.display_handle()) {
(Ok(w), Ok(d)) => Some(Parent {
window: w.as_raw(),
display: d.as_raw(),
}),
_ => None,
};
if let Ok(mut slot) = PARENT.lock() {
*slot = parent;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_filter_takes_the_dot_off() {
let dialog = FileDialog::new()
.filter("Images", &["png", ".jpg", "*.jpeg", "", "*"])
.filter("Text", &["txt"]);
assert_eq!(
dialog.filters,
vec![
(
"Images".to_owned(),
vec!["png".into(), "jpg".into(), "jpeg".into()]
),
("Text".to_owned(), vec!["txt".into()]),
]
);
}
#[test]
fn the_description_is_kept() {
let dialog = FileDialog::new()
.title("Export")
.directory("/tmp")
.file_name("notes.txt");
assert_eq!(dialog.title.as_deref(), Some("Export"));
assert_eq!(dialog.directory, Some(PathBuf::from("/tmp")));
assert_eq!(dialog.file_name.as_deref(), Some("notes.txt"));
}
#[test]
fn the_interface_thread_is_refused() {
if !crate::interface_thread::claim_for_test() {
return; }
assert!(crate::interface_thread::current());
assert_eq!(FileDialog::new().pick_file(), None);
assert!(FileDialog::new().pick_files().is_empty());
assert_eq!(FileDialog::new().pick_folder(), None);
assert_eq!(FileDialog::new().save_file(), None);
let elsewhere = std::thread::spawn(crate::interface_thread::current)
.join()
.unwrap();
assert!(!elsewhere, "another thread is not the interface's");
}
}