egui_file_dialog/data/
directory_content.rs

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