Skip to main content

dear_file_browser/
native.rs

1//! Native (rfd) backend.
2//!
3//! This module implements the OS-native file dialogs via the `rfd` crate.
4//! On desktop platforms it opens the system dialog; on `wasm32` targets it
5//! uses the Web File Picker. Both blocking and async flows are exposed via the
6//! `FileDialog` builder:
7//!
8//! - `open_blocking()` opens a modal, OS-native dialog and returns on close.
9//! - `open_async()` awaits the selection (desktop and wasm32 supported).
10//!
11//! Notes
12//! - Filters map to `rfd::FileDialog::add_filter` and accept lowercase
13//!   extensions without dots (e.g. "png").
14//! - When `start_dir` is provided it is forwarded to `rfd`.
15//! - On the Web (wasm32), the ImGui in-UI browser cannot enumerate the local
16//!   filesystem – prefer the native backend to access user files.
17use crate::core::{Backend, DialogMode, FileDialog, FileDialogError, Selection};
18
19#[cfg(feature = "tracing")]
20use tracing::trace;
21
22impl FileDialog {
23    fn to_rfd(&self) -> rfd::FileDialog {
24        let mut d = rfd::FileDialog::new();
25        if let Some(dir) = &self.start_dir {
26            d = d.set_directory(dir);
27        }
28        if let Some(name) = &self.default_name {
29            d = d.set_file_name(name);
30        }
31        for f in &self.filters {
32            let exts_owned: Vec<String> = f
33                .extensions
34                .iter()
35                .filter_map(|s| plain_extension_for_native(s))
36                .collect();
37            let exts: Vec<&str> = exts_owned.iter().map(|s| s.as_str()).collect();
38            if !exts.is_empty() {
39                d = d.add_filter(&f.name, &exts);
40            }
41        }
42        d
43    }
44
45    fn to_rfd_async(&self) -> rfd::AsyncFileDialog {
46        let mut a = rfd::AsyncFileDialog::new();
47        if let Some(dir) = self.start_dir.as_deref() {
48            a = a.set_directory(dir);
49        }
50        if let Some(name) = self.default_name.as_deref() {
51            a = a.set_file_name(name);
52        }
53        for f in &self.filters {
54            let exts_owned: Vec<String> = f
55                .extensions
56                .iter()
57                .filter_map(|s| plain_extension_for_native(s))
58                .collect();
59            if !exts_owned.is_empty() {
60                a = a.add_filter(&f.name, &exts_owned);
61            }
62        }
63        a
64    }
65
66    /// Open a dialog synchronously (blocking).
67    pub fn open_blocking(self) -> Result<Selection, FileDialogError> {
68        match self.effective_backend() {
69            Backend::Native => self.open_blocking_native(),
70            Backend::ImGui => Err(FileDialogError::Unsupported),
71            Backend::Auto => unreachable!("resolved in effective_backend"),
72        }
73    }
74
75    fn open_blocking_native(self) -> Result<Selection, FileDialogError> {
76        #[cfg(feature = "tracing")]
77        trace!(?self.mode, "rfd blocking open");
78        let mut sel = Selection::default();
79        match self.mode {
80            DialogMode::OpenFile => {
81                if let Some(p) = self.to_rfd().pick_file() {
82                    sel.paths.push(p);
83                }
84            }
85            DialogMode::OpenFiles => {
86                if !self.allow_multi {
87                    if let Some(p) = self.to_rfd().pick_file() {
88                        sel.paths.push(p);
89                    }
90                } else if let Some(v) = self.to_rfd().pick_files() {
91                    sel.paths.extend(v);
92                }
93            }
94            DialogMode::PickFolder => {
95                if let Some(p) = self.to_rfd().pick_folder() {
96                    sel.paths.push(p);
97                }
98            }
99            DialogMode::SaveFile => {
100                if let Some(p) = self.to_rfd().save_file() {
101                    sel.paths.push(p);
102                }
103            }
104        }
105        if let Some(max) = self.max_selection.filter(|&m| m > 0) {
106            sel.paths.truncate(max);
107        }
108        if sel.paths.is_empty() {
109            Err(FileDialogError::Cancelled)
110        } else {
111            Ok(sel)
112        }
113    }
114
115    /// Open a dialog asynchronously via `rfd::AsyncFileDialog`.
116    pub async fn open_async(self) -> Result<Selection, FileDialogError> {
117        use rfd::AsyncFileDialog as A;
118        #[cfg(feature = "tracing")]
119        trace!(?self.mode, "rfd async open");
120        let mut sel = Selection::default();
121        match self.mode {
122            DialogMode::OpenFile => {
123                let a = self.to_rfd_async();
124                let f = a.pick_file().await;
125                if let Some(h) = f {
126                    sel.paths.push(h.path().to_path_buf());
127                }
128            }
129            DialogMode::OpenFiles => {
130                let a = self.to_rfd_async();
131                if !self.allow_multi {
132                    let f = a.pick_file().await;
133                    if let Some(h) = f {
134                        sel.paths.push(h.path().to_path_buf());
135                    }
136                } else {
137                    let v = a.pick_files().await;
138                    if let Some(v) = v {
139                        sel.paths
140                            .extend(v.into_iter().map(|h| h.path().to_path_buf()));
141                    }
142                }
143            }
144            DialogMode::PickFolder => {
145                let mut a = A::new();
146                if let Some(dir) = self.start_dir.as_deref() {
147                    a = a.set_directory(dir);
148                }
149                let f = a.pick_folder().await;
150                if let Some(h) = f {
151                    sel.paths.push(h.path().to_path_buf());
152                }
153            }
154            DialogMode::SaveFile => {
155                let a = self.to_rfd_async();
156                let f = a.save_file().await;
157                if let Some(h) = f {
158                    sel.paths.push(h.path().to_path_buf());
159                }
160            }
161        }
162        if let Some(max) = self.max_selection.filter(|&m| m > 0) {
163            sel.paths.truncate(max);
164        }
165        if sel.paths.is_empty() {
166            Err(FileDialogError::Cancelled)
167        } else {
168            Ok(sel)
169        }
170    }
171}
172
173fn is_plain_extension_token(token: &str) -> bool {
174    let t = token.trim();
175    if t.is_empty() {
176        return false;
177    }
178    if t.starts_with("((") && t.ends_with("))") {
179        return false;
180    }
181    !(t.contains('*') || t.contains('?'))
182}
183
184fn plain_extension_for_native(token: &str) -> Option<String> {
185    if !is_plain_extension_token(token) {
186        return None;
187    }
188    let t = token.trim().trim_start_matches('.');
189    if t.is_empty() {
190        return None;
191    }
192    Some(t.to_lowercase())
193}