use std::path::PathBuf;
#[derive(Clone, Debug, Default)]
pub struct FileDialog {
pub title: String,
pub file_name: String,
pub accept: String,
}
impl FileDialog {
pub fn new() -> Self {
Self::default()
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn with_file_name(mut self, name: impl Into<String>) -> Self {
self.file_name = name.into();
self
}
pub fn with_accept(mut self, accept: impl Into<String>) -> Self {
self.accept = accept.into();
self
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FileContents {
pub name: String,
pub bytes: Vec<u8>,
}
pub fn open_file_contents(opts: &FileDialog, done: impl FnOnce(Option<FileContents>) + 'static) {
#[cfg(target_arch = "wasm32")]
{
web::open(&opts.accept, Box::new(done));
}
#[cfg(not(target_arch = "wasm32"))]
{
let contents = open_file(opts).and_then(|path| {
let bytes = std::fs::read(&path).ok()?;
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
Some(FileContents { name, bytes })
});
done(contents);
}
}
pub fn save_file_contents(opts: &FileDialog, bytes: &[u8]) -> bool {
#[cfg(target_arch = "wasm32")]
{
let name = if opts.file_name.is_empty() {
"download"
} else {
&opts.file_name
};
crate::platform::backend::web::save_file(name, bytes);
true
}
#[cfg(not(target_arch = "wasm32"))]
{
save_file(opts).is_some_and(|path| std::fs::write(path, bytes).is_ok())
}
}
#[cfg(target_arch = "wasm32")]
mod web {
use super::FileContents;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
type Done = Box<dyn FnOnce(Option<FileContents>)>;
thread_local! {
static PENDING: RefCell<HashMap<u32, Done>> = RefCell::new(HashMap::new());
static NEXT: Cell<u32> = const { Cell::new(1) };
}
pub(super) fn open(accept: &str, done: Done) {
let id = NEXT.with(|n| n.replace(n.get().wrapping_add(1)));
PENDING.with(|p| p.borrow_mut().insert(id, done));
crate::platform::backend::web::open_file(id, accept);
}
pub(crate) fn picked(id: u32, file: Option<(String, Vec<u8>)>) {
if let Some(done) = PENDING.with(|p| p.borrow_mut().remove(&id)) {
done(file.map(|(name, bytes)| FileContents { name, bytes }));
}
}
}
#[cfg(target_arch = "wasm32")]
pub(crate) use web::picked as web_picked;
pub fn open_file(opts: &FileDialog) -> Option<PathBuf> {
let title = title_or(opts, "Open File");
backend::file_chooser(Op::Open, &title)
}
pub fn save_file(opts: &FileDialog) -> Option<PathBuf> {
let title = title_or(opts, "Save File");
backend::file_chooser(Op::Save, &title)
}
pub fn pick_folder(opts: &FileDialog) -> Option<PathBuf> {
let title = title_or(opts, "Select Folder");
backend::file_chooser(Op::Folder, &title)
}
fn title_or(opts: &FileDialog, default: &str) -> String {
if opts.title.is_empty() {
default.to_string()
} else {
opts.title.clone()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Op {
Open,
Save,
Folder,
}
#[cfg(target_os = "linux")]
mod backend {
use super::{Op, PathBuf};
pub(super) fn file_chooser(op: Op, title: &str) -> Option<PathBuf> {
let (member, directory) = match op {
Op::Open => ("OpenFile", false),
Op::Save => ("SaveFile", false),
Op::Folder => ("OpenFile", true),
};
let mut bus = crate::platform::a11y::DBus::connect_session().ok()?;
bus.portal_file_chooser(member, title, directory)
.ok()
.flatten()
}
}
#[cfg(target_os = "windows")]
#[allow(unsafe_code)]
#[path = "windows.rs"]
mod backend;
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
#[path = "macos.rs"]
mod backend;
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
mod backend {
use super::{Op, PathBuf};
pub(super) fn file_chooser(_op: Op, _title: &str) -> Option<PathBuf> {
None
}
}