Skip to main content

egui_file_dialog/data/
directory_content.rs

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    /// If files should be included.
14    pub show_files: bool,
15    /// If hidden files and folders should be included.
16    pub show_hidden: bool,
17    /// If system files should be included.
18    pub show_system_files: bool,
19    /// Optional filter to further filter files.
20    pub file_filter: Option<FileFilter>,
21    /// Optional file extension to filter by.
22    pub filter_extension: Option<String>,
23}
24
25/// Contains the metadata of a directory item.
26#[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    /// Create a new custom metadata
37    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/// Contains the information of a directory item.
53///
54/// This struct is mainly there so that the information and metadata can be loaded once and not that
55/// a request has to be sent to the OS every frame using, for example, `path.is_file()`.
56#[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    /// If the item is marked as selected as part of a multi selection.
66    pub selected: bool,
67}
68
69impl DirectoryEntry {
70    /// Creates a new directory entry from a path
71    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    /// Returns the metadata of the directory entry.
84    pub const fn metadata(&self) -> &Metadata {
85        &self.metadata
86    }
87
88    /// Checks if the path of the current directory entry matches the other directory entry.
89    pub fn path_eq(&self, other: &Self) -> bool {
90        other.as_path() == self.as_path()
91    }
92
93    /// Returns true if the item is a directory.
94    /// False is returned if the item is a file or the path did not exist when the
95    /// `DirectoryEntry` object was created.
96    pub const fn is_dir(&self) -> bool {
97        self.is_directory
98    }
99
100    /// Returns true if the item is a file.
101    /// False is returned if the item is a directory or the path did not exist when the
102    /// `DirectoryEntry` object was created.
103    pub const fn is_file(&self) -> bool {
104        !self.is_directory
105    }
106
107    /// Returns true if the item is a system file.
108    pub const fn is_system_file(&self) -> bool {
109        self.is_system_file
110    }
111
112    /// Returns the icon of the directory item.
113    pub fn icon(&self) -> &str {
114        &self.icon
115    }
116
117    /// Returns the path of the directory item.
118    pub fn as_path(&self) -> &Path {
119        &self.path
120    }
121
122    /// Clones the path of the directory item.
123    pub fn to_path_buf(&self) -> PathBuf {
124        self.path.clone()
125    }
126
127    /// Returns the file name of the directory item.
128    pub fn file_name(&self) -> &str {
129        self.path
130            .file_name()
131            .and_then(|name| name.to_str())
132            .unwrap_or_else(|| {
133                // Make sure the root directories like ["C:", "\"] and ["\\?\C:", "\"] are
134                // displayed correctly
135                #[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                    // Skip path namespace prefix if present, for example: "\\?\C:"
145                    if path.contains(r"\\?\") {
146                        return path.get(4..).unwrap_or(path);
147                    }
148
149                    return path;
150                }
151
152                // Make sure the root directory "/" is displayed correctly
153                #[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    /// Returns whether the path this `DirectoryEntry` points to is considered hidden.
163    pub const fn is_hidden(&self) -> bool {
164        self.is_hidden
165    }
166}
167
168/// Contains the state of the directory content.
169#[derive(Debug, PartialEq, Eq)]
170pub enum DirectoryContentState {
171    /// If we are currently waiting for the loading process on another thread.
172    /// The value is the timestamp when the loading process started.
173    Pending(SystemTime),
174    /// If loading the directory content finished since the last update call.
175    /// This is only returned once.
176    Finished,
177    /// If loading the directory content was successful.
178    Success,
179    /// If there was an error loading the directory content.
180    /// The value contains the error message.
181    Errored(String),
182}
183
184type DirectoryContentReceiver =
185    Option<Arc<Mutex<mpsc::Receiver<Result<Vec<DirectoryEntry>, std::io::Error>>>>>;
186
187/// Contains the content of a directory.
188pub struct DirectoryContent {
189    /// Current state of the directory content.
190    state: DirectoryContentState,
191    /// The loaded directory contents.
192    content: Vec<DirectoryEntry>,
193    /// Receiver when the content is loaded on a different thread.
194    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    /// Create a new `DirectoryContent` object and loads the contents of the given path.
226    /// Use `include_files` to include or exclude files in the content list.
227    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    /// Returns an iterator in the given range of the directory contents.
329    /// No filters are applied using this iterator.
330    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    /// Marks each element in the content as unselected.
356    pub fn reset_multi_selection(&mut self) {
357        for item in &mut self.content {
358            item.selected = false;
359        }
360    }
361
362    /// Returns the number of elements inside the directory.
363    pub fn len(&self) -> usize {
364        self.content.len()
365    }
366
367    /// Pushes a new item to the content.
368    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
381/// Loads the contents of the given directory.
382fn 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
439/// Generates the icon for the specific path.
440/// The default icon configuration is taken into account, as well as any configured
441/// file icon filters.
442fn 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}