1use std::path::{Path, PathBuf};
2use std::sync::{mpsc, Arc};
3use std::time::SystemTime;
4use std::{io, thread};
5
6use egui::mutex::Mutex;
7
8use crate::config::{FileDialogConfig, FileFilter};
9use crate::FileSystem;
10
11#[derive(Clone, Debug)]
12pub struct DirectoryFilter {
13 pub show_files: bool,
15 pub show_hidden: bool,
17 pub show_system_files: bool,
19 pub file_filter: Option<FileFilter>,
21 pub filter_extension: Option<String>,
23}
24
25#[derive(Debug, Default, Clone)]
27#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
28pub struct Metadata {
29 pub(crate) size: Option<u64>,
30 pub(crate) last_modified: Option<SystemTime>,
31 pub(crate) created: Option<SystemTime>,
32 pub(crate) file_type: Option<String>,
33}
34
35impl Metadata {
36 pub const fn new(
38 size: Option<u64>,
39 last_modified: Option<SystemTime>,
40 created: Option<SystemTime>,
41 file_type: Option<String>,
42 ) -> Self {
43 Self {
44 size,
45 last_modified,
46 created,
47 file_type,
48 }
49 }
50}
51
52#[derive(Debug, Default, Clone)]
57#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
58pub struct DirectoryEntry {
59 path: PathBuf,
60 metadata: Metadata,
61 is_directory: bool,
62 is_system_file: bool,
63 is_hidden: bool,
64 icon: String,
65 pub selected: bool,
67}
68
69impl DirectoryEntry {
70 pub fn from_path(config: &FileDialogConfig, path: &Path, file_system: &dyn FileSystem) -> Self {
72 Self {
73 path: path.to_path_buf(),
74 metadata: file_system.metadata(path).unwrap_or_default(),
75 is_directory: file_system.is_dir(path),
76 is_system_file: !file_system.is_dir(path) && !file_system.is_file(path),
77 icon: gen_path_icon(config, path, file_system),
78 is_hidden: file_system.is_path_hidden(path),
79 selected: false,
80 }
81 }
82
83 pub const fn metadata(&self) -> &Metadata {
85 &self.metadata
86 }
87
88 pub fn path_eq(&self, other: &Self) -> bool {
90 other.as_path() == self.as_path()
91 }
92
93 pub const fn is_dir(&self) -> bool {
97 self.is_directory
98 }
99
100 pub const fn is_file(&self) -> bool {
104 !self.is_directory
105 }
106
107 pub const fn is_system_file(&self) -> bool {
109 self.is_system_file
110 }
111
112 pub fn icon(&self) -> &str {
114 &self.icon
115 }
116
117 pub fn as_path(&self) -> &Path {
119 &self.path
120 }
121
122 pub fn to_path_buf(&self) -> PathBuf {
124 self.path.clone()
125 }
126
127 pub fn file_name(&self) -> &str {
129 self.path
130 .file_name()
131 .and_then(|name| name.to_str())
132 .unwrap_or_else(|| {
133 #[cfg(windows)]
136 if self.path.components().count() == 2 {
137 let path = self
138 .path
139 .iter()
140 .nth(0)
141 .and_then(|seg| seg.to_str())
142 .unwrap_or_default();
143
144 if path.contains(r"\\?\") {
146 return path.get(4..).unwrap_or(path);
147 }
148
149 return path;
150 }
151
152 #[cfg(not(windows))]
154 if self.path.iter().count() == 1 {
155 return self.path.to_str().unwrap_or_default();
156 }
157
158 ""
159 })
160 }
161
162 pub const fn is_hidden(&self) -> bool {
164 self.is_hidden
165 }
166}
167
168#[derive(Debug, PartialEq, Eq)]
170pub enum DirectoryContentState {
171 Pending(SystemTime),
174 Finished,
177 Success,
179 Errored(String),
182}
183
184type DirectoryContentReceiver =
185 Option<Arc<Mutex<mpsc::Receiver<Result<Vec<DirectoryEntry>, std::io::Error>>>>>;
186
187pub struct DirectoryContent {
189 state: DirectoryContentState,
191 content: Vec<DirectoryEntry>,
193 content_recv: DirectoryContentReceiver,
195}
196
197impl Default for DirectoryContent {
198 fn default() -> Self {
199 Self {
200 state: DirectoryContentState::Success,
201 content: Vec::new(),
202 content_recv: None,
203 }
204 }
205}
206
207impl std::fmt::Debug for DirectoryContent {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 f.debug_struct("DirectoryContent")
210 .field("state", &self.state)
211 .field("content", &self.content)
212 .field(
213 "content_recv",
214 if self.content_recv.is_some() {
215 &"<Receiver>"
216 } else {
217 &"None"
218 },
219 )
220 .finish()
221 }
222}
223
224impl DirectoryContent {
225 pub fn from_path(
228 config: &FileDialogConfig,
229 path: &Path,
230 file_system: Arc<dyn FileSystem + Sync + Send + 'static>,
231 filter: DirectoryFilter,
232 ) -> Self {
233 if config.load_via_thread {
234 Self::with_thread(config, path, file_system, filter)
235 } else {
236 Self::without_thread(config, path, &*file_system, &filter)
237 }
238 }
239
240 fn with_thread(
241 config: &FileDialogConfig,
242 path: &Path,
243 file_system: Arc<dyn FileSystem + Send + Sync + 'static>,
244 filter: DirectoryFilter,
245 ) -> Self {
246 let (tx, rx) = mpsc::channel();
247
248 let c = config.clone();
249 let p = path.to_path_buf();
250 thread::spawn(move || {
251 let _ = tx.send(load_directory(&c, &p, &*file_system, &filter));
252 });
253
254 Self {
255 state: DirectoryContentState::Pending(SystemTime::now()),
256 content: Vec::new(),
257 content_recv: Some(Arc::new(Mutex::new(rx))),
258 }
259 }
260
261 fn without_thread(
262 config: &FileDialogConfig,
263 path: &Path,
264 file_system: &dyn FileSystem,
265 filter: &DirectoryFilter,
266 ) -> Self {
267 match load_directory(config, path, file_system, filter) {
268 Ok(c) => Self {
269 state: DirectoryContentState::Success,
270 content: c,
271 content_recv: None,
272 },
273 Err(err) => Self {
274 state: DirectoryContentState::Errored(err.to_string()),
275 content: Vec::new(),
276 content_recv: None,
277 },
278 }
279 }
280
281 pub fn update(&mut self) -> &DirectoryContentState {
282 if self.state == DirectoryContentState::Finished {
283 self.state = DirectoryContentState::Success;
284 }
285
286 if !matches!(self.state, DirectoryContentState::Pending(_)) {
287 return &self.state;
288 }
289
290 self.update_pending_state()
291 }
292
293 fn update_pending_state(&mut self) -> &DirectoryContentState {
294 let rx = std::mem::take(&mut self.content_recv);
295 let mut update_content_recv = true;
296
297 if let Some(recv) = &rx {
298 let value = recv.lock().try_recv();
299 match value {
300 Ok(result) => match result {
301 Ok(content) => {
302 self.state = DirectoryContentState::Finished;
303 self.content = content;
304 update_content_recv = false;
305 }
306 Err(err) => {
307 self.state = DirectoryContentState::Errored(err.to_string());
308 update_content_recv = false;
309 }
310 },
311 Err(err) => {
312 if mpsc::TryRecvError::Disconnected == err {
313 self.state =
314 DirectoryContentState::Errored("thread ended unexpectedly".to_owned());
315 update_content_recv = false;
316 }
317 }
318 }
319 }
320
321 if update_content_recv {
322 self.content_recv = rx;
323 }
324
325 &self.state
326 }
327
328 pub fn iter_range_mut(
331 &mut self,
332 range: std::ops::Range<usize>,
333 ) -> impl Iterator<Item = &mut DirectoryEntry> {
334 self.content[range].iter_mut()
335 }
336
337 pub fn filtered_iter<'s>(
338 &'s self,
339 search_value: &'s str,
340 ) -> impl Iterator<Item = &'s DirectoryEntry> + 's {
341 self.content
342 .iter()
343 .filter(|p| apply_search_value(p, search_value))
344 }
345
346 pub fn filtered_iter_mut<'s>(
347 &'s mut self,
348 search_value: &'s str,
349 ) -> impl Iterator<Item = &'s mut DirectoryEntry> + 's {
350 self.content
351 .iter_mut()
352 .filter(|p| apply_search_value(p, search_value))
353 }
354
355 pub fn reset_multi_selection(&mut self) {
357 for item in &mut self.content {
358 item.selected = false;
359 }
360 }
361
362 pub fn len(&self) -> usize {
364 self.content.len()
365 }
366
367 pub fn push(&mut self, item: DirectoryEntry) {
369 self.content.push(item);
370 }
371}
372
373fn apply_search_value(entry: &DirectoryEntry, value: &str) -> bool {
374 value.is_empty()
375 || entry
376 .file_name()
377 .to_lowercase()
378 .contains(&value.to_lowercase())
379}
380
381fn load_directory(
383 config: &FileDialogConfig,
384 path: &Path,
385 file_system: &dyn FileSystem,
386 filter: &DirectoryFilter,
387) -> io::Result<Vec<DirectoryEntry>> {
388 let mut result: Vec<DirectoryEntry> = Vec::new();
389 for path in file_system.read_dir(path)? {
390 let entry = DirectoryEntry::from_path(config, &path, file_system);
391
392 if !filter.show_system_files && entry.is_system_file() {
393 continue;
394 }
395
396 if !filter.show_files && entry.is_file() {
397 continue;
398 }
399
400 if !filter.show_hidden && entry.is_hidden() {
401 continue;
402 }
403
404 if let Some(file_filter) = &filter.file_filter {
405 if entry.is_file() && !file_filter.filter.matches(entry.as_path()) {
406 continue;
407 }
408 }
409
410 if let Some(ex) = &filter.filter_extension {
411 if entry.is_file()
412 && path
413 .extension()
414 .unwrap_or_default()
415 .to_str()
416 .unwrap_or_default()
417 != ex
418 {
419 continue;
420 }
421 }
422
423 result.push(entry);
424 }
425
426 result.sort_by(|a, b| {
427 if a.is_dir() == b.is_dir() {
428 a.file_name().cmp(b.file_name())
429 } else if a.is_dir() {
430 std::cmp::Ordering::Less
431 } else {
432 std::cmp::Ordering::Greater
433 }
434 });
435
436 Ok(result)
437}
438
439fn gen_path_icon(config: &FileDialogConfig, path: &Path, file_system: &dyn FileSystem) -> String {
443 for def in &config.file_icon_filters {
444 if def.filter.matches(path) {
445 return def.icon.clone();
446 }
447 }
448
449 if file_system.is_dir(path) {
450 config.default_folder_icon.clone()
451 } else {
452 config.default_file_icon.clone()
453 }
454}