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}