Skip to main content

fret_platform_native/
file_dialog.rs

1#[cfg(not(target_arch = "wasm32"))]
2use fret_core::{ExternalDropFileData, ExternalDropReadError};
3use fret_core::{FileDialogDataEvent, FileDialogOptions, FileDialogSelection, FileDialogToken};
4use fret_platform::external_drop::ExternalDropReadLimits;
5use fret_platform::file_dialog::{FileDialogError, FileDialogProvider};
6
7#[cfg(all(
8    not(target_arch = "wasm32"),
9    any(target_os = "windows", target_os = "macos", target_os = "linux")
10))]
11use fret_core::ExternalDragFile;
12
13#[cfg(not(target_arch = "wasm32"))]
14use std::path::PathBuf;
15
16#[cfg(all(
17    not(target_arch = "wasm32"),
18    any(target_os = "windows", target_os = "macos", target_os = "linux")
19))]
20use std::collections::HashMap;
21
22#[cfg(all(
23    not(target_arch = "wasm32"),
24    any(target_os = "windows", target_os = "macos", target_os = "linux")
25))]
26#[derive(Debug)]
27pub struct NativeFileDialog {
28    next_token: u64,
29    selections: HashMap<FileDialogToken, Vec<PathBuf>>,
30}
31
32#[cfg(not(all(
33    not(target_arch = "wasm32"),
34    any(target_os = "windows", target_os = "macos", target_os = "linux")
35)))]
36#[derive(Debug)]
37pub struct NativeFileDialog;
38
39pub type DesktopFileDialog = NativeFileDialog;
40
41impl NativeFileDialog {
42    pub fn new() -> Self {
43        #[cfg(all(
44            not(target_arch = "wasm32"),
45            any(target_os = "windows", target_os = "macos", target_os = "linux")
46        ))]
47        {
48            Self {
49                next_token: 1,
50                selections: HashMap::new(),
51            }
52        }
53
54        #[cfg(not(all(
55            not(target_arch = "wasm32"),
56            any(target_os = "windows", target_os = "macos", target_os = "linux")
57        )))]
58        {
59            Self
60        }
61    }
62}
63
64impl Default for NativeFileDialog {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70#[cfg(all(
71    not(target_arch = "wasm32"),
72    any(target_os = "windows", target_os = "macos", target_os = "linux")
73))]
74impl NativeFileDialog {
75    fn allocate_token(&mut self) -> FileDialogToken {
76        let token = FileDialogToken(self.next_token);
77        self.next_token = self.next_token.saturating_add(1);
78        token
79    }
80}
81
82#[cfg(not(target_arch = "wasm32"))]
83impl NativeFileDialog {
84    pub fn paths(&self, token: FileDialogToken) -> Option<&[PathBuf]> {
85        #[cfg(all(
86            any(target_os = "windows", target_os = "macos", target_os = "linux"),
87            not(target_arch = "wasm32")
88        ))]
89        {
90            self.selections.get(&token).map(|v| v.as_slice())
91        }
92
93        #[cfg(not(all(
94            any(target_os = "windows", target_os = "macos", target_os = "linux"),
95            not(target_arch = "wasm32")
96        )))]
97        {
98            let _ = token;
99            None
100        }
101    }
102
103    pub fn read_paths(
104        token: FileDialogToken,
105        paths: Vec<PathBuf>,
106        limits: ExternalDropReadLimits,
107    ) -> FileDialogDataEvent {
108        let mut files: Vec<ExternalDropFileData> = Vec::new();
109        let mut errors: Vec<ExternalDropReadError> = Vec::new();
110        let mut total: u64 = 0;
111
112        for path in paths.into_iter().take(limits.max_files) {
113            let name = path
114                .file_name()
115                .map(|n| n.to_string_lossy().to_string())
116                .unwrap_or_else(|| path.to_string_lossy().to_string());
117
118            let meta_len = match std::fs::metadata(&path) {
119                Ok(m) => Some(m.len()),
120                Err(err) => {
121                    errors.push(ExternalDropReadError {
122                        name,
123                        message: format!("metadata failed: {err}"),
124                    });
125                    continue;
126                }
127            };
128
129            if let Some(len) = meta_len
130                && len > limits.max_file_bytes
131            {
132                errors.push(ExternalDropReadError {
133                    name,
134                    message: format!(
135                        "file too large ({} bytes > max_file_bytes {})",
136                        len, limits.max_file_bytes
137                    ),
138                });
139                continue;
140            }
141
142            if total >= limits.max_total_bytes {
143                errors.push(ExternalDropReadError {
144                    name,
145                    message: format!(
146                        "selection too large (total {} >= max_total_bytes {})",
147                        total, limits.max_total_bytes
148                    ),
149                });
150                break;
151            }
152
153            let bytes = match std::fs::read(&path) {
154                Ok(bytes) => bytes,
155                Err(err) => {
156                    errors.push(ExternalDropReadError {
157                        name,
158                        message: format!("read failed: {err}"),
159                    });
160                    continue;
161                }
162            };
163
164            if bytes.len() as u64 > limits.max_file_bytes {
165                errors.push(ExternalDropReadError {
166                    name,
167                    message: format!(
168                        "file too large ({} bytes > max_file_bytes {})",
169                        bytes.len(),
170                        limits.max_file_bytes
171                    ),
172                });
173                continue;
174            }
175
176            let next_total = total.saturating_add(bytes.len() as u64);
177            if next_total > limits.max_total_bytes {
178                errors.push(ExternalDropReadError {
179                    name,
180                    message: format!(
181                        "selection too large (next_total {} > max_total_bytes {})",
182                        next_total, limits.max_total_bytes
183                    ),
184                });
185                break;
186            }
187
188            total = next_total;
189            files.push(ExternalDropFileData { name, bytes });
190        }
191
192        FileDialogDataEvent {
193            token,
194            files,
195            errors,
196        }
197    }
198}
199
200impl FileDialogProvider for NativeFileDialog {
201    fn open_files(
202        &mut self,
203        options: &FileDialogOptions,
204    ) -> Result<Option<FileDialogSelection>, FileDialogError> {
205        #[cfg(all(
206            not(target_arch = "wasm32"),
207            any(target_os = "windows", target_os = "macos", target_os = "linux")
208        ))]
209        {
210            let mut dialog = rfd::FileDialog::new();
211
212            if let Some(title) = &options.title {
213                dialog = dialog.set_title(title);
214            }
215
216            for filter in &options.filters {
217                dialog = dialog.add_filter(&filter.name, &filter.extensions);
218            }
219
220            let selected: Option<Vec<PathBuf>> = if options.multiple {
221                dialog.pick_files()
222            } else {
223                dialog.pick_file().map(|p| vec![p])
224            };
225
226            let Some(paths) = selected else {
227                return Ok(None);
228            };
229
230            let token = self.allocate_token();
231            let files = paths
232                .iter()
233                .map(|path| ExternalDragFile {
234                    name: path
235                        .file_name()
236                        .map(|n| n.to_string_lossy().to_string())
237                        .unwrap_or_else(|| path.to_string_lossy().to_string()),
238                    size_bytes: std::fs::metadata(path).ok().map(|m| m.len()),
239                    media_type: None,
240                })
241                .collect::<Vec<_>>();
242
243            self.selections.insert(token, paths);
244
245            Ok(Some(FileDialogSelection { token, files }))
246        }
247
248        #[cfg(not(all(
249            not(target_arch = "wasm32"),
250            any(target_os = "windows", target_os = "macos", target_os = "linux")
251        )))]
252        {
253            let _ = options;
254            Err(FileDialogError {
255                kind: fret_platform::file_dialog::FileDialogErrorKind::Unsupported,
256            })
257        }
258    }
259
260    fn read_all(
261        &mut self,
262        token: FileDialogToken,
263        limits: ExternalDropReadLimits,
264    ) -> Option<FileDialogDataEvent> {
265        #[cfg(all(
266            not(target_arch = "wasm32"),
267            any(target_os = "windows", target_os = "macos", target_os = "linux")
268        ))]
269        {
270            let paths = self.selections.get(&token)?.clone();
271            Some(Self::read_paths(token, paths, limits))
272        }
273
274        #[cfg(not(all(
275            not(target_arch = "wasm32"),
276            any(target_os = "windows", target_os = "macos", target_os = "linux")
277        )))]
278        {
279            let _ = token;
280            let _ = limits;
281            None
282        }
283    }
284
285    fn release(&mut self, token: FileDialogToken) {
286        #[cfg(all(
287            not(target_arch = "wasm32"),
288            any(target_os = "windows", target_os = "macos", target_os = "linux")
289        ))]
290        {
291            self.selections.remove(&token);
292        }
293
294        #[cfg(not(all(
295            not(target_arch = "wasm32"),
296            any(target_os = "windows", target_os = "macos", target_os = "linux")
297        )))]
298        {
299            let _ = token;
300        }
301    }
302}