//! Asking the platform for a folder or a file, instead of a typed path. //! //! A path typed into a field is how a destination silently becomes a new //! folder somewhere nobody meant: one wrong letter three levels down and the //! export is written, successfully, to a place the photographer will never //! look. The platform's own dialogue shows what is there, remembers where the //! user last was, and offers "New folder" — which is the other half of what //! was asked for, and which no widget here would do as well. //! //! # Which dialogue //! //! `rfd`, with the XDG desktop portal on Linux and the common item dialogue on //! Windows. The portal rather than GTK because it is the one that works inside //! the Flatpak (where a GTK dialogue would browse the sandbox, not the user's //! disk) and because it needs no GTK in a Slint application that otherwise has //! none. It also draws whichever desktop's own chooser is running, so the //! dialogue looks like the rest of the user's machine. //! //! # Not blocking the interface //! //! The dialogue is awaited on Slint's event loop with `spawn_local` rather than //! on a thread: the portal's reply arrives over D-Bus on async-io's reactor, //! which needs no executor of ours, and the window keeps drawing while the //! chooser is open. The callback therefore runs on the UI thread, where every //! caller wants to be anyway. //! //! # Android //! //! Not here. Android has no filesystem dialogue — only the Storage Access //! Framework, which hands back a document tree rather than a path — so the //! screens that need a path hide their button there (`Pickers.local-paths`), //! and album folders go through the Storage Access Framework instead. use std::path::PathBuf; #[cfg(not(target_os = "android"))] use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; #[cfg(not(target_os = "android"))] use slint::ComponentHandle; use crate::AppWindow; /// What to ask for. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Pick { /// A directory. Folder, /// One file, narrowed to these extensions (without the dot). File(&'static [&'static str]), } /// Whether this build has a platform dialogue for filesystem paths. pub const AVAILABLE: bool = cfg!(not(target_os = "android")); /// Ask for a path, and call `chosen` with it if the user picks one. /// /// Nothing is called on cancel: every caller's answer to "the user closed the /// dialogue" is to leave things as they were, so there is no second callback /// to write. `start` opens the dialogue where the current value is, when there /// is one — changing a destination usually means moving one level, not /// starting again from the home directory. #[cfg(not(target_os = "android"))] pub fn ask( window: &AppWindow, title: &str, pick: Pick, start: Option<&str>, chosen: impl FnOnce(PathBuf) + 'static, ) { let mut dialog = rfd::AsyncFileDialog::new().set_title(title); // Parented to the window, so the portal or Windows can place it over the // application and make it modal to it. Where the handle is unavailable // the dialogue still works; it just opens unparented. if let Ok(handle) = window.window().window_handle().window_handle() { if let Ok(display) = window.window().window_handle().display_handle() { dialog = dialog.set_parent(&Parent(handle, display)); } } if let Some(dir) = start.map(str::trim).filter(|s| !s.is_empty()) { let dir = PathBuf::from(dir); // The folder itself for a folder pick; for a file, where the file is. let dir = if dir.is_dir() { Some(dir) } else { dir.parent().map(PathBuf::from) }; if let Some(dir) = dir.filter(|d| d.is_dir()) { dialog = dialog.set_directory(dir); } } let result = slint::spawn_local(async move { let picked = match pick { Pick::Folder => dialog.pick_folder().await, Pick::File(extensions) => { dialog .add_filter(extensions.join(", "), extensions) .pick_file() .await } }; if let Some(handle) = picked { chosen(handle.path().to_path_buf()); } }); if let Err(e) = result { log::warn!("could not open the folder dialogue: {e}"); } } /// The window's handles, in the one shape `rfd::set_parent` takes. /// /// Slint hands out the window and the display handle separately, each /// borrowed from the window; rfd wants one value implementing both. #[cfg(not(target_os = "android"))] struct Parent<'a>( raw_window_handle::WindowHandle<'a>, raw_window_handle::DisplayHandle<'a>, ); #[cfg(not(target_os = "android"))] impl raw_window_handle::HasWindowHandle for Parent<'_> { fn window_handle( &self, ) -> Result, raw_window_handle::HandleError> { Ok(self.0) } } #[cfg(not(target_os = "android"))] impl raw_window_handle::HasDisplayHandle for Parent<'_> { fn display_handle( &self, ) -> Result, raw_window_handle::HandleError> { Ok(self.1) } } /// No filesystem dialogue on Android; see the module header. The buttons that /// would call this are hidden there, so reaching it is a wiring mistake, and /// it says so in the log rather than silently doing nothing. #[cfg(target_os = "android")] pub fn ask( _window: &AppWindow, title: &str, _pick: Pick, _start: Option<&str>, _chosen: impl FnOnce(PathBuf) + 'static, ) { log::warn!("{title}: no filesystem dialogue on Android"); }