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