file_dialog.rs
raw
//! Native file dialogs.
//!
//! An **integration, not a widget**: the dialog is a window we do not own, in
//! a process we do not own, and it hands back paths. There is nothing to draw
//! and nothing to write in a `.gdc`. It is also the one place we can put a
//! *real*, compositor-managed dialog on screen,
//! because the platform owns the toplevel and the parent association that
//! winit will not give us (see `guiduck_core::widget::dialog`).
//!
//! [`FileDialog`] is the portable surface. Each platform's chooser sits behind
//! it: xdg-desktop-portal on freedesktop systems, and — when someone needs
//! them — the native APIs on Windows and macOS. Nothing above this line knows
//! which, and a platform without a backend yet says so and cancels rather than
//! failing to build: an application should not have to care where it runs to
//! compile.
//!
//! There will never be an in-toolkit file browser. The desktop's own chooser
//! honours the user's bookmarks, settings, and sandbox, and is the same dialog
//! every other application shows. We would be worse at all three.
//!
//! The call blocks — a person may take a minute to choose — so it runs
//! through [`background`](crate::background): the chooser on its own thread,
//! the callback on the UI thread, and the application sees a plain handler
//! call rather than a stalled frame.
use guiduck_core::WidgetTree;
#[cfg(unix)]
mod xdg;
/// What a file dialog asks for.
pub struct FileDialog {
title: String,
multiple: bool,
filters: Vec<(String, String)>,
}
impl FileDialog {
pub fn open(title: impl Into<String>) -> Self {
Self {
title: title.into(),
multiple: false,
filters: Vec::new(),
}
}
pub fn multiple(mut self, multiple: bool) -> Self {
self.multiple = multiple;
self
}
/// A named glob filter, e.g. `("Images", "*.png")`.
pub fn filter(mut self, label: impl Into<String>, glob: impl Into<String>) -> Self {
self.filters.push((label.into(), glob.into()));
self
}
/// Show the dialog, delivering the chosen files to `then` on the UI
/// thread once the user is done.
///
/// Returns immediately. `then` gets an empty slice if the user cancelled —
/// changing your mind is not an error — and likewise if no chooser is
/// available, with a warning: an application should not die because the
/// desktop could not open a file picker.
pub fn show(self, then: impl FnOnce(&mut WidgetTree, &[String]) + 'static) {
crate::background::run(
move || match self.run() {
Ok(files) => files,
Err(why) => {
eprintln!("guiduck: no file chooser: {why}");
Vec::new()
}
},
move |tree, files| then(tree, &files),
);
}
/// The blocking half, on its own thread. One of these per platform.
fn run(self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
#[cfg(unix)]
{
xdg::open_file(&self.title, self.multiple, &self.filters)
}
#[cfg(not(unix))]
{
// Windows (IFileOpenDialog) and macOS (NSOpenPanel) belong here.
// Until someone needs one, saying so beats pretending.
let _ = (&self.title, self.multiple, &self.filters);
Err("this platform has no file-chooser backend yet".into())
}
}
}