Skip to main content

telar_services_core/
dialogs.rs

1use std::path::PathBuf;
2use std::sync::{Arc, OnceLock};
3
4/// One entry of a dialog's type filter: a name the user reads and the extensions it accepts.
5#[derive(Clone, Debug, Default)]
6pub struct FileFilter {
7    pub name: String,
8    pub extensions: Vec<String>,
9}
10
11impl FileFilter {
12    pub fn new(name: impl Into<String>, extensions: &[&str]) -> Self {
13        Self {
14            name: name.into(),
15            extensions: extensions.iter().map(|e| (*e).to_string()).collect(),
16        }
17    }
18}
19
20/// What to ask the OS for. Every field is optional: an empty request opens the platform's default
21/// "any file, last directory" dialog.
22#[derive(Clone, Debug, Default)]
23pub struct FileDialog {
24    pub title: Option<String>,
25    pub directory: Option<PathBuf>,
26    /// Pre-filled name, for a save dialog.
27    pub file_name: Option<String>,
28    pub filters: Vec<FileFilter>,
29}
30
31impl FileDialog {
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    pub fn title(mut self, title: impl Into<String>) -> Self {
37        self.title = Some(title.into());
38        self
39    }
40
41    pub fn directory(mut self, dir: impl Into<PathBuf>) -> Self {
42        self.directory = Some(dir.into());
43        self
44    }
45
46    pub fn file_name(mut self, name: impl Into<String>) -> Self {
47        self.file_name = Some(name.into());
48        self
49    }
50
51    pub fn filter(mut self, name: impl Into<String>, extensions: &[&str]) -> Self {
52        self.filters.push(FileFilter::new(name, extensions));
53        self
54    }
55}
56
57/// The OS file-chooser, as the vocabulary crate sees it.
58///
59/// Every method BLOCKS until the user answers, which is why the whole trait is `Send + Sync`: the caller
60/// runs it on a worker thread and takes the answer back on the UI thread. Nothing here touches the event
61/// loop, so a backend is free to be a portal call, a native panel, or a stub in a test.
62pub trait FileDialogs: Send + Sync + 'static {
63    fn open_file(&self, request: FileDialog) -> Option<PathBuf>;
64    fn open_files(&self, request: FileDialog) -> Vec<PathBuf>;
65    fn save_file(&self, request: FileDialog) -> Option<PathBuf>;
66    fn pick_folder(&self, request: FileDialog) -> Option<PathBuf>;
67}
68
69static DIALOGS: OnceLock<Arc<dyn FileDialogs>> = OnceLock::new();
70
71/// Installs the backend the app's dialogs go through. The desktop runner calls this at startup; a test or
72/// a headless build can install a stub instead. The first call wins.
73pub fn set_file_dialogs(provider: Arc<dyn FileDialogs>) {
74    let _ = DIALOGS.set(provider);
75}
76
77/// The installed backend, or `None` on a platform with no file chooser (headless, Android today).
78pub fn file_dialogs() -> Option<Arc<dyn FileDialogs>> {
79    DIALOGS.get().cloned()
80}