//! 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) -> 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, glob: impl Into) -> 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, Box> { #[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()) } } }