egui_file_dialog/
file_dialog.rs

1use crate::config::{
2    FileDialogConfig, FileDialogKeyBindings, FileDialogLabels, FileFilter, Filter, OpeningMode,
3    PinnedFolder, QuickAccess, SaveExtension,
4};
5use crate::create_directory_dialog::CreateDirectoryDialog;
6use crate::data::{
7    DirectoryContent, DirectoryContentState, DirectoryEntry, DirectoryFilter, Disk, Disks,
8    UserDirectories,
9};
10use crate::modals::{FileDialogModal, ModalAction, ModalState, OverwriteFileModal};
11use crate::{FileSystem, NativeFileSystem};
12use egui::text::{CCursor, CCursorRange};
13use std::any::Any;
14use std::fmt::Debug;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17
18/// Represents the mode the file dialog is currently in.
19#[derive(Debug, PartialEq, Eq, Clone, Copy)]
20pub enum DialogMode {
21    /// When the dialog is currently used to select a single file.
22    PickFile,
23
24    /// When the dialog is currently used to select a single directory.
25    PickDirectory,
26
27    /// When the dialog is currently used to select multiple files and directories.
28    PickMultiple,
29
30    /// When the dialog is currently used to save a file.
31    SaveFile,
32}
33
34/// Represents the state the file dialog is currently in.
35#[derive(Debug, PartialEq, Eq, Clone)]
36pub enum DialogState {
37    /// The dialog is currently open and the user can perform the desired actions.
38    Open,
39
40    /// The dialog is currently closed and not visible.
41    Closed,
42
43    /// The user has selected a folder or file or specified a destination path for saving a file.
44    Picked(PathBuf),
45
46    /// The user has finished selecting multiple files and folders.
47    PickedMultiple(Vec<PathBuf>),
48
49    /// The user cancelled the dialog and didn't select anything.
50    Cancelled,
51}
52
53/// Contains data of the `FileDialog` that should be stored persistently.
54#[derive(Debug, Clone)]
55#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
56pub struct FileDialogStorage {
57    /// The folders the user pinned to the left sidebar.
58    pub pinned_folders: Vec<PinnedFolder>,
59    /// If hidden files and folders should be listed inside the directory view.
60    pub show_hidden: bool,
61    /// If system files should be listed inside the directory view.
62    pub show_system_files: bool,
63    /// The last directory the user visited.
64    pub last_visited_dir: Option<PathBuf>,
65    /// The last directory from which the user picked an item.
66    pub last_picked_dir: Option<PathBuf>,
67}
68
69impl Default for FileDialogStorage {
70    /// Creates a new object with default values
71    fn default() -> Self {
72        Self {
73            pinned_folders: Vec::new(),
74            show_hidden: false,
75            show_system_files: false,
76            last_visited_dir: None,
77            last_picked_dir: None,
78        }
79    }
80}
81
82/// Represents a file dialog instance.
83///
84/// The `FileDialog` instance can be used multiple times and for different actions.
85///
86/// # Examples
87///
88/// ```
89/// use egui_file_dialog::FileDialog;
90///
91/// struct MyApp {
92///     file_dialog: FileDialog,
93/// }
94///
95/// impl MyApp {
96///     fn update(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
97///         if ui.button("Pick a file").clicked() {
98///             self.file_dialog.pick_file();
99///         }
100///
101///         if let Some(path) = self.file_dialog.update(ctx).picked() {
102///             println!("Picked file: {:?}", path);
103///         }
104///     }
105/// }
106/// ```
107#[derive(Debug)]
108pub struct FileDialog {
109    /// The configuration of the file dialog.
110    config: FileDialogConfig,
111    /// Persistent data of the file dialog.
112    storage: FileDialogStorage,
113
114    /// Stack of modal windows to be displayed.
115    /// The top element is what is currently being rendered.
116    modals: Vec<Box<dyn FileDialogModal + Send + Sync>>,
117
118    /// The mode the dialog is currently in
119    mode: DialogMode,
120    /// The state the dialog is currently in
121    state: DialogState,
122    /// If files are displayed in addition to directories.
123    /// This option will be ignored when mode == `DialogMode::SelectFile`.
124    show_files: bool,
125    /// Custom data set by the API consumer, to track things like the purpose
126    /// the file dialog was opened for.
127    user_data: Option<Box<dyn Any + Send + Sync>>,
128    /// The currently used window ID.
129    window_id: egui::Id,
130
131    /// The user directories like Home or Documents.
132    /// These are loaded once when the dialog is created or when the `refresh()` method is called.
133    user_directories: Option<UserDirectories>,
134    /// The currently mounted system disks.
135    /// These are loaded once when the dialog is created or when the `refresh()` method is called.
136    system_disks: Disks,
137
138    /// Contains the directories that the user opened. Every newly opened directory
139    /// is pushed to the vector.
140    /// Used for the navigation buttons to load the previous or next directory.
141    directory_stack: Vec<PathBuf>,
142    /// An offset from the back of `directory_stack` telling which directory is currently open.
143    /// If 0, the user is currently in the latest open directory.
144    /// If not 0, the user has used the "Previous directory" button and has
145    /// opened previously opened directories.
146    directory_offset: usize,
147    /// The content of the currently open directory
148    directory_content: DirectoryContent,
149
150    /// The dialog that is shown when the user wants to create a new directory.
151    create_directory_dialog: CreateDirectoryDialog,
152
153    /// Whether the text edit is open for editing the current path.
154    path_edit_visible: bool,
155    /// Buffer holding the text when the user edits the current path.
156    path_edit_value: String,
157    /// If the path edit should be initialized. Unlike `path_edit_request_focus`,
158    /// this also sets the cursor to the end of the text input field.
159    path_edit_activate: bool,
160    /// If the text edit of the path should request focus in the next frame.
161    path_edit_request_focus: bool,
162
163    /// The item that the user currently selected.
164    /// Can be a directory or a folder.
165    selected_item: Option<DirectoryEntry>,
166    /// Buffer for the input of the file name when the dialog is in `SaveFile` mode.
167    file_name_input: String,
168    /// This variables contains the error message if the `file_name_input` is invalid.
169    /// This can be the case, for example, if a file or folder with the name already exists.
170    file_name_input_error: Option<String>,
171    /// If the file name input text field should request focus in the next frame.
172    file_name_input_request_focus: bool,
173    /// The file filter the user selected.
174    selected_file_filter: Option<egui::Id>,
175    /// The save extension that the user selected.
176    selected_save_extension: Option<egui::Id>,
177
178    /// If we should scroll to the item selected by the user in the next frame.
179    scroll_to_selection: bool,
180    /// Buffer containing the value of the search input.
181    search_value: String,
182    /// If the search should be initialized in the next frame.
183    init_search: bool,
184
185    /// If any widget was focused in the last frame.
186    /// This is used to prevent the dialog from closing when pressing the escape key
187    /// inside a text input.
188    any_focused_last_frame: bool,
189
190    /// The current pinned folder being renamed.
191    /// None if no folder is being renamed.
192    rename_pinned_folder: Option<PinnedFolder>,
193    /// If the text input of the pinned folder being renamed should request focus in
194    /// the next frame.
195    rename_pinned_folder_request_focus: bool,
196}
197
198/// This tests if file dialog is send and sync.
199#[cfg(test)]
200const fn test_prop<T: Send + Sync>() {}
201
202#[test]
203const fn test() {
204    test_prop::<FileDialog>();
205}
206
207impl Default for FileDialog {
208    /// Creates a new file dialog instance with default values.
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214impl Debug for dyn FileDialogModal + Send + Sync {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        write!(f, "<FileDialogModal>")
217    }
218}
219
220/// Callback type to inject a custom egui ui inside the file dialog's ui.
221///
222/// Also gives access to the file dialog, since it would otherwise be inaccessible
223/// inside the closure.
224type FileDialogUiCallback<'a> = dyn FnMut(&mut egui::Ui, &mut FileDialog) + 'a;
225
226impl FileDialog {
227    // ------------------------------------------------------------------------
228    // Creation:
229
230    /// Creates a new file dialog instance with default values.
231    #[must_use]
232    pub fn new() -> Self {
233        let file_system = Arc::new(NativeFileSystem);
234
235        Self {
236            config: FileDialogConfig::default_from_filesystem(file_system.clone()),
237            storage: FileDialogStorage::default(),
238
239            modals: Vec::new(),
240
241            mode: DialogMode::PickDirectory,
242            state: DialogState::Closed,
243            show_files: true,
244            user_data: None,
245
246            window_id: egui::Id::new("file_dialog"),
247
248            user_directories: None,
249            system_disks: Disks::new_empty(),
250
251            directory_stack: Vec::new(),
252            directory_offset: 0,
253            directory_content: DirectoryContent::default(),
254
255            create_directory_dialog: CreateDirectoryDialog::from_filesystem(file_system),
256
257            path_edit_visible: false,
258            path_edit_value: String::new(),
259            path_edit_activate: false,
260            path_edit_request_focus: false,
261
262            selected_item: None,
263            file_name_input: String::new(),
264            file_name_input_error: None,
265            file_name_input_request_focus: true,
266            selected_file_filter: None,
267            selected_save_extension: None,
268
269            scroll_to_selection: false,
270            search_value: String::new(),
271            init_search: false,
272
273            any_focused_last_frame: false,
274
275            rename_pinned_folder: None,
276            rename_pinned_folder_request_focus: false,
277        }
278    }
279
280    /// Creates a new file dialog object and initializes it with the specified configuration.
281    pub fn with_config(config: FileDialogConfig) -> Self {
282        let mut obj = Self::new();
283        *obj.config_mut() = config;
284        obj.create_directory_dialog =
285            CreateDirectoryDialog::from_filesystem(obj.config.file_system.clone());
286        obj
287    }
288
289    /// Uses the given file system instead of the native file system.
290    #[must_use]
291    pub fn with_file_system(file_system: Arc<dyn FileSystem + Send + Sync>) -> Self {
292        let mut obj = Self::new();
293        obj.config.initial_directory = file_system.current_dir().unwrap_or_default();
294        obj.config.file_system = file_system;
295        obj.create_directory_dialog =
296            CreateDirectoryDialog::from_filesystem(obj.config.file_system.clone());
297        obj
298    }
299
300    // -------------------------------------------------
301    // Open, Update:
302
303    /// Opens the file dialog in the given mode with the given options.
304    /// This function resets the file dialog and takes care for the variables that need to be
305    /// set when opening the file dialog.
306    ///
307    /// Returns the result of the operation to load the initial directory.
308    ///
309    /// If you don't need to set the individual parameters, you can also use the shortcut
310    /// methods `select_directory`, `select_file` and `save_file`.
311    ///
312    /// # Arguments
313    ///
314    /// * `mode` - The mode in which the dialog should be opened
315    /// * `show_files` - If files should also be displayed to the user in addition to directories.
316    ///   This is ignored if the mode is `DialogMode::SelectFile`.
317    ///
318    /// # Examples
319    ///
320    /// ```
321    /// use std::path::PathBuf;
322    ///
323    /// use egui_file_dialog::{DialogMode, FileDialog};
324    ///
325    /// struct MyApp {
326    ///     file_dialog: FileDialog,
327    ///
328    ///     picked_file: Option<PathBuf>,
329    /// }
330    ///
331    /// impl MyApp {
332    ///     fn update(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
333    ///         if ui.button("Pick file").clicked() {
334    ///             let _ = self.file_dialog.open(DialogMode::PickFile, true);
335    ///         }
336    ///
337    ///         self.file_dialog.update(ctx);
338    ///
339    ///         if let Some(path) = self.file_dialog.picked() {
340    ///             self.picked_file = Some(path.to_path_buf());
341    ///         }
342    ///     }
343    /// }
344    /// ```
345    #[deprecated(
346        since = "0.10.0",
347        note = "Use `pick_file` / `pick_directory` / `pick_multiple` in combination with \
348                `set_user_data` instead"
349    )]
350    pub fn open(&mut self, mode: DialogMode, mut show_files: bool) {
351        self.reset();
352        self.refresh();
353
354        if mode == DialogMode::PickFile {
355            show_files = true;
356        }
357
358        if mode == DialogMode::SaveFile {
359            self.file_name_input_request_focus = true;
360            self.file_name_input
361                .clone_from(&self.config.default_file_name);
362        }
363
364        self.selected_file_filter = None;
365        self.selected_save_extension = None;
366
367        self.set_default_file_filter();
368        self.set_default_save_extension();
369
370        self.mode = mode;
371        self.state = DialogState::Open;
372        self.show_files = show_files;
373
374        self.window_id = self
375            .config
376            .id
377            .map_or_else(|| egui::Id::new(self.get_window_title()), |id| id);
378
379        self.load_directory(&self.get_initial_directory());
380    }
381
382    /// Shortcut function to open the file dialog to prompt the user to pick a directory.
383    /// If used, no files in the directories will be shown to the user.
384    /// Use the `open()` method instead, if you still want to display files to the user.
385    /// This function resets the file dialog. Configuration variables such as
386    /// `initial_directory` are retained.
387    ///
388    /// The function ignores the result of the initial directory loading operation.
389    pub fn pick_directory(&mut self) {
390        // `FileDialog::open` will only be marked as private in the future.
391        #[allow(deprecated)]
392        self.open(DialogMode::PickDirectory, false);
393    }
394
395    /// Shortcut function to open the file dialog to prompt the user to pick a file.
396    /// This function resets the file dialog. Configuration variables such as
397    /// `initial_directory` are retained.
398    ///
399    /// The function ignores the result of the initial directory loading operation.
400    pub fn pick_file(&mut self) {
401        // `FileDialog::open` will only be marked as private in the future.
402        #[allow(deprecated)]
403        self.open(DialogMode::PickFile, true);
404    }
405
406    /// Shortcut function to open the file dialog to prompt the user to pick multiple
407    /// files and folders.
408    /// This function resets the file dialog. Configuration variables such as `initial_directory`
409    /// are retained.
410    ///
411    /// The function ignores the result of the initial directory loading operation.
412    pub fn pick_multiple(&mut self) {
413        // `FileDialog::open` will only be marked as private in the future.
414        #[allow(deprecated)]
415        self.open(DialogMode::PickMultiple, true);
416    }
417
418    /// Shortcut function to open the file dialog to prompt the user to save a file.
419    /// This function resets the file dialog. Configuration variables such as
420    /// `initial_directory` are retained.
421    ///
422    /// The function ignores the result of the initial directory loading operation.
423    pub fn save_file(&mut self) {
424        // `FileDialog::open` will only be marked as private in the future.
425        #[allow(deprecated)]
426        self.open(DialogMode::SaveFile, true);
427    }
428
429    /// The main update method that should be called every frame if the dialog is to be visible.
430    ///
431    /// This function has no effect if the dialog state is currently not `DialogState::Open`.
432    pub fn update(&mut self, ctx: &egui::Context) -> &Self {
433        if self.state != DialogState::Open {
434            return self;
435        }
436
437        self.update_keybindings(ctx);
438        self.update_ui(ctx, None);
439
440        self
441    }
442
443    /// Sets the width of the right panel.
444    pub fn set_right_panel_width(&mut self, width: f32) {
445        self.config.right_panel_width = Some(width);
446    }
447
448    /// Clears the width of the right panel by setting it to None.
449    pub fn clear_right_panel_width(&mut self) {
450        self.config.right_panel_width = None;
451    }
452
453    /// Do an [update](`Self::update`) with a custom right panel ui.
454    ///
455    /// Example use cases:
456    /// - Show custom information for a file (size, MIME type, etc.)
457    /// - Embed a preview, like a thumbnail for an image
458    /// - Add controls for custom open options, like open as read-only, etc.
459    ///
460    /// See [`active_entry`](Self::active_entry) to get the active directory entry
461    /// to show the information for.
462    ///
463    /// This function has no effect if the dialog state is currently not `DialogState::Open`.
464    pub fn update_with_right_panel_ui(
465        &mut self,
466        ctx: &egui::Context,
467        f: &mut FileDialogUiCallback,
468    ) -> &Self {
469        if self.state != DialogState::Open {
470            return self;
471        }
472
473        self.update_keybindings(ctx);
474        self.update_ui(ctx, Some(f));
475
476        self
477    }
478
479    // -------------------------------------------------
480    // Setter:
481
482    /// Mutably borrow internal `config`.
483    pub fn config_mut(&mut self) -> &mut FileDialogConfig {
484        &mut self.config
485    }
486
487    /// Sets the storage used by the file dialog.
488    /// Storage includes all data that is persistently stored between multiple
489    /// file dialog instances.
490    pub fn storage(mut self, storage: FileDialogStorage) -> Self {
491        self.storage = storage;
492        self
493    }
494
495    /// Mutably borrow internal storage.
496    pub fn storage_mut(&mut self) -> &mut FileDialogStorage {
497        &mut self.storage
498    }
499
500    /// Sets the keybindings used by the file dialog.
501    pub fn keybindings(mut self, keybindings: FileDialogKeyBindings) -> Self {
502        self.config.keybindings = keybindings;
503        self
504    }
505
506    /// Sets the labels the file dialog uses.
507    ///
508    /// Used to enable multiple language support.
509    ///
510    /// See `FileDialogLabels` for more information.
511    pub fn labels(mut self, labels: FileDialogLabels) -> Self {
512        self.config.labels = labels;
513        self
514    }
515
516    /// Mutably borrow internal `config.labels`.
517    pub fn labels_mut(&mut self) -> &mut FileDialogLabels {
518        &mut self.config.labels
519    }
520
521    /// Sets which directory is loaded when opening the file dialog.
522    pub const fn opening_mode(mut self, opening_mode: OpeningMode) -> Self {
523        self.config.opening_mode = opening_mode;
524        self
525    }
526
527    /// If the file dialog window should be displayed as a modal.
528    ///
529    /// If the window is displayed as modal, the area outside the dialog can no longer be
530    /// interacted with and an overlay is displayed.
531    pub const fn as_modal(mut self, as_modal: bool) -> Self {
532        self.config.as_modal = as_modal;
533        self
534    }
535
536    /// Sets the color of the overlay when the dialog is displayed as a modal window.
537    pub const fn modal_overlay_color(mut self, modal_overlay_color: egui::Color32) -> Self {
538        self.config.modal_overlay_color = modal_overlay_color;
539        self
540    }
541
542    /// Sets the first loaded directory when the dialog opens.
543    /// If the path is a file, the file's parent directory is used. If the path then has no
544    /// parent directory or cannot be loaded, the user will receive an error.
545    /// However, the user directories and system disk allow the user to still select a file in
546    /// the event of an error.
547    ///
548    /// Since `fs::canonicalize` is used, both absolute paths and relative paths are allowed.
549    /// See `FileDialog::canonicalize_paths` for more information.
550    pub fn initial_directory(mut self, directory: PathBuf) -> Self {
551        self.config.initial_directory = directory;
552        self
553    }
554
555    /// Sets the default file name when opening the dialog in `DialogMode::SaveFile` mode.
556    pub fn default_file_name(mut self, name: &str) -> Self {
557        name.clone_into(&mut self.config.default_file_name);
558        self
559    }
560
561    /// Sets if the user is allowed to select an already existing file when the dialog is in
562    /// `DialogMode::SaveFile` mode.
563    ///
564    /// If this is enabled, the user will receive a modal asking whether the user really
565    /// wants to overwrite an existing file.
566    pub const fn allow_file_overwrite(mut self, allow_file_overwrite: bool) -> Self {
567        self.config.allow_file_overwrite = allow_file_overwrite;
568        self
569    }
570
571    /// Sets if the path edit is allowed to select the path as the file to save
572    /// if it does not have an extension.
573    ///
574    /// This can lead to confusion if the user wants to open a directory with the path edit,
575    /// types it incorrectly and the dialog tries to select the incorrectly typed folder as
576    /// the file to be saved.
577    ///
578    /// This only affects the `DialogMode::SaveFile` mode.
579    pub const fn allow_path_edit_to_save_file_without_extension(mut self, allow: bool) -> Self {
580        self.config.allow_path_edit_to_save_file_without_extension = allow;
581        self
582    }
583
584    /// Sets the separator of the directories when displaying a path.
585    /// Currently only used when the current path is displayed in the top panel.
586    pub fn directory_separator(mut self, separator: &str) -> Self {
587        self.config.directory_separator = separator.to_string();
588        self
589    }
590
591    /// Sets if the paths in the file dialog should be canonicalized before use.
592    ///
593    /// By default, all paths are canonicalized. This has the advantage that the paths are
594    /// all brought to a standard and are therefore compatible with each other.
595    ///
596    /// On Windows, however, this results in the namespace prefix `\\?\` being set in
597    /// front of the path, which may not be compatible with other applications.
598    /// In addition, canonicalizing converts all relative paths to absolute ones.
599    ///
600    /// See: [Rust docs](https://doc.rust-lang.org/std/fs/fn.canonicalize.html)
601    /// for more information.
602    ///
603    /// In general, it is only recommended to disable canonicalization if
604    /// you know what you are doing and have a reason for it.
605    /// Disabling canonicalization can lead to unexpected behavior, for example if an
606    /// already canonicalized path is then set as the initial directory.
607    pub const fn canonicalize_paths(mut self, canonicalize: bool) -> Self {
608        self.config.canonicalize_paths = canonicalize;
609        self
610    }
611
612    /// If the directory content should be loaded via a separate thread.
613    /// This prevents the application from blocking when loading large directories
614    /// or from slow hard drives.
615    pub const fn load_via_thread(mut self, load_via_thread: bool) -> Self {
616        self.config.load_via_thread = load_via_thread;
617        self
618    }
619
620    /// Sets if long filenames should be truncated in the middle.
621    /// The extension, if available, will be preserved.
622    ///
623    /// Warning! If this is disabled, the scroll-to-selection might not work correctly and have
624    /// an offset for large directories.
625    pub const fn truncate_filenames(mut self, truncate_filenames: bool) -> Self {
626        self.config.truncate_filenames = truncate_filenames;
627        self
628    }
629
630    /// Sets the icon that is used to display errors.
631    pub fn err_icon(mut self, icon: &str) -> Self {
632        self.config.err_icon = icon.to_string();
633        self
634    }
635
636    /// Sets the default icon that is used to display files.
637    pub fn default_file_icon(mut self, icon: &str) -> Self {
638        self.config.default_file_icon = icon.to_string();
639        self
640    }
641
642    /// Sets the default icon that is used to display folders.
643    pub fn default_folder_icon(mut self, icon: &str) -> Self {
644        self.config.default_folder_icon = icon.to_string();
645        self
646    }
647
648    /// Sets the icon that is used to display devices in the left panel.
649    pub fn device_icon(mut self, icon: &str) -> Self {
650        self.config.device_icon = icon.to_string();
651        self
652    }
653
654    /// Sets the icon that is used to display removable devices in the left panel.
655    pub fn removable_device_icon(mut self, icon: &str) -> Self {
656        self.config.removable_device_icon = icon.to_string();
657        self
658    }
659
660    /// Adds a new file filter the user can select from a dropdown widget.
661    ///
662    /// NOTE: The name must be unique. If a filter with the same name already exists,
663    ///       it will be overwritten.
664    ///
665    /// # Arguments
666    ///
667    /// * `name` - Display name of the filter
668    /// * `filter` - Sets a filter function that checks whether a given
669    ///   Path matches the criteria for this filter.
670    ///
671    /// # Examples
672    ///
673    /// ```
674    /// use std::sync::Arc;
675    /// use egui_file_dialog::FileDialog;
676    ///
677    /// FileDialog::new()
678    ///     .add_file_filter(
679    ///         "PNG files",
680    ///         Arc::new(|path| path.extension().unwrap_or_default() == "png"))
681    ///     .add_file_filter(
682    ///         "JPG files",
683    ///         Arc::new(|path| path.extension().unwrap_or_default() == "jpg"));
684    /// ```
685    pub fn add_file_filter(mut self, name: &str, filter: Filter<Path>) -> Self {
686        self.config = self.config.add_file_filter(name, filter);
687        self
688    }
689
690    /// Shortctut method to add a file filter that matches specific extensions.
691    ///
692    /// # Arguments
693    ///
694    /// * `name` - Display name of the filter
695    /// * `extensions` - The extensions of the files to be filtered
696    ///
697    /// # Examples
698    ///
699    /// ```
700    /// use egui_file_dialog::FileDialog;
701    ///
702    /// FileDialog::new()
703    ///     .add_file_filter_extensions("Pictures", vec!["png", "jpg", "dds"])
704    ///     .add_file_filter_extensions("Rust files", vec!["rs", "toml", "lock"]);
705    pub fn add_file_filter_extensions(mut self, name: &str, extensions: Vec<&'static str>) -> Self {
706        self.config = self.config.add_file_filter_extensions(name, extensions);
707        self
708    }
709
710    /// Name of the file filter to be selected by default.
711    ///
712    /// No file filter is selected if there is no file filter with that name.
713    pub fn default_file_filter(mut self, name: &str) -> Self {
714        self.config.default_file_filter = Some(name.to_string());
715        self
716    }
717
718    /// Adds a new file extension that the user can select in a dropdown widget when
719    /// saving a file.
720    ///
721    /// NOTE: The name must be unique. If an extension with the same name already exists,
722    ///       it will be overwritten.
723    ///
724    /// # Arguments
725    ///
726    /// * `name` - Display name of the save extension.
727    /// * `file_extension` - The file extension to use.
728    ///
729    /// # Examples
730    ///
731    /// ```
732    /// use std::sync::Arc;
733    /// use egui_file_dialog::FileDialog;
734    ///
735    /// let config = FileDialog::default()
736    ///     .add_save_extension("PNG files", "png")
737    ///     .add_save_extension("JPG files", "jpg");
738    /// ```
739    pub fn add_save_extension(mut self, name: &str, file_extension: &str) -> Self {
740        self.config = self.config.add_save_extension(name, file_extension);
741        self
742    }
743
744    /// Name of the file extension to be selected by default when saving a file.
745    ///
746    /// No file extension is selected if there is no extension with that name.
747    pub fn default_save_extension(mut self, name: &str) -> Self {
748        self.config.default_save_extension = Some(name.to_string());
749        self
750    }
751
752    /// Sets a new icon for specific files or folders.
753    ///
754    /// # Arguments
755    ///
756    /// * `icon` - The icon that should be used.
757    /// * `filter` - Sets a filter function that checks whether a given
758    ///   Path matches the criteria for this icon.
759    ///
760    /// # Examples
761    ///
762    /// ```
763    /// use std::sync::Arc;
764    /// use egui_file_dialog::FileDialog;
765    ///
766    /// FileDialog::new()
767    ///     // .png files should use the "document with picture (U+1F5BB)" icon.
768    ///     .set_file_icon("🖻", Arc::new(|path| path.extension().unwrap_or_default() == "png"))
769    ///     // .git directories should use the "web-github (U+E624)" icon.
770    ///     .set_file_icon("", Arc::new(|path| path.file_name().unwrap_or_default() == ".git"));
771    /// ```
772    pub fn set_file_icon(mut self, icon: &str, filter: Filter<std::path::Path>) -> Self {
773        self.config = self.config.set_file_icon(icon, filter);
774        self
775    }
776
777    /// Adds a new custom quick access section to the left panel.
778    ///
779    /// # Examples
780    ///
781    /// ```
782    /// use egui_file_dialog::FileDialog;
783    ///
784    /// FileDialog::new()
785    ///     .add_quick_access("My App", |s| {
786    ///         s.add_path("Config", "/app/config");
787    ///         s.add_path("Themes", "/app/themes");
788    ///         s.add_path("Languages", "/app/languages");
789    ///     });
790    /// ```
791    // pub fn add_quick_access(mut self, heading: &str, builder: &fn(&mut QuickAccess)) -> Self {
792    pub fn add_quick_access(
793        mut self,
794        heading: &str,
795        builder: impl FnOnce(&mut QuickAccess),
796    ) -> Self {
797        self.config = self.config.add_quick_access(heading, builder);
798        self
799    }
800
801    /// Overwrites the window title.
802    ///
803    /// By default, the title is set dynamically, based on the `DialogMode`
804    /// the dialog is currently in.
805    pub fn title(mut self, title: &str) -> Self {
806        self.config.title = Some(title.to_string());
807        self
808    }
809
810    /// Sets the ID of the window.
811    pub fn id(mut self, id: impl Into<egui::Id>) -> Self {
812        self.config.id = Some(id.into());
813        self
814    }
815
816    /// Sets the default position of the window.
817    pub fn default_pos(mut self, default_pos: impl Into<egui::Pos2>) -> Self {
818        self.config.default_pos = Some(default_pos.into());
819        self
820    }
821
822    /// Sets the window position and prevents it from being dragged around.
823    pub fn fixed_pos(mut self, pos: impl Into<egui::Pos2>) -> Self {
824        self.config.fixed_pos = Some(pos.into());
825        self
826    }
827
828    /// Sets the default size of the window.
829    pub fn default_size(mut self, size: impl Into<egui::Vec2>) -> Self {
830        self.config.default_size = size.into();
831        self
832    }
833
834    /// Sets the maximum size of the window.
835    pub fn max_size(mut self, max_size: impl Into<egui::Vec2>) -> Self {
836        self.config.max_size = Some(max_size.into());
837        self
838    }
839
840    /// Sets the minimum size of the window.
841    ///
842    /// Specifying a smaller minimum size than the default can lead to unexpected behavior.
843    pub fn min_size(mut self, min_size: impl Into<egui::Vec2>) -> Self {
844        self.config.min_size = min_size.into();
845        self
846    }
847
848    /// Sets the anchor of the window.
849    pub fn anchor(mut self, align: egui::Align2, offset: impl Into<egui::Vec2>) -> Self {
850        self.config.anchor = Some((align, offset.into()));
851        self
852    }
853
854    /// Sets if the window is resizable.
855    pub const fn resizable(mut self, resizable: bool) -> Self {
856        self.config.resizable = resizable;
857        self
858    }
859
860    /// Sets if the window is movable.
861    ///
862    /// Has no effect if an anchor is set.
863    pub const fn movable(mut self, movable: bool) -> Self {
864        self.config.movable = movable;
865        self
866    }
867
868    /// Sets if the title bar of the window is shown.
869    pub const fn title_bar(mut self, title_bar: bool) -> Self {
870        self.config.title_bar = title_bar;
871        self
872    }
873
874    /// Sets if the top panel with the navigation buttons, current path display
875    /// and search input should be visible.
876    pub const fn show_top_panel(mut self, show_top_panel: bool) -> Self {
877        self.config.show_top_panel = show_top_panel;
878        self
879    }
880
881    /// Sets whether the parent folder button should be visible in the top panel.
882    ///
883    /// Has no effect when `FileDialog::show_top_panel` is disabled.
884    pub const fn show_parent_button(mut self, show_parent_button: bool) -> Self {
885        self.config.show_parent_button = show_parent_button;
886        self
887    }
888
889    /// Sets whether the back button should be visible in the top panel.
890    ///
891    /// Has no effect when `FileDialog::show_top_panel` is disabled.
892    pub const fn show_back_button(mut self, show_back_button: bool) -> Self {
893        self.config.show_back_button = show_back_button;
894        self
895    }
896
897    /// Sets whether the forward button should be visible in the top panel.
898    ///
899    /// Has no effect when `FileDialog::show_top_panel` is disabled.
900    pub const fn show_forward_button(mut self, show_forward_button: bool) -> Self {
901        self.config.show_forward_button = show_forward_button;
902        self
903    }
904
905    /// Sets whether the button to create a new folder should be visible in the top panel.
906    ///
907    /// Has no effect when `FileDialog::show_top_panel` is disabled.
908    pub const fn show_new_folder_button(mut self, show_new_folder_button: bool) -> Self {
909        self.config.show_new_folder_button = show_new_folder_button;
910        self
911    }
912
913    /// Sets whether the current path should be visible in the top panel.
914    ///
915    /// Has no effect when `FileDialog::show_top_panel` is disabled.
916    pub const fn show_current_path(mut self, show_current_path: bool) -> Self {
917        self.config.show_current_path = show_current_path;
918        self
919    }
920
921    /// Sets whether the button to text edit the current path should be visible in the top panel.
922    ///
923    /// has no effect when `FileDialog::show_top_panel` is disabled.
924    pub const fn show_path_edit_button(mut self, show_path_edit_button: bool) -> Self {
925        self.config.show_path_edit_button = show_path_edit_button;
926        self
927    }
928
929    /// Sets whether the menu with the reload button and other options should be visible
930    /// inside the top panel.
931    ///
932    /// Has no effect when `FileDialog::show_top_panel` is disabled.
933    pub const fn show_menu_button(mut self, show_menu_button: bool) -> Self {
934        self.config.show_menu_button = show_menu_button;
935        self
936    }
937
938    /// Sets whether the reload button inside the top panel menu should be visible.
939    ///
940    /// Has no effect when `FileDialog::show_top_panel` or
941    /// `FileDialog::show_menu_button` is disabled.
942    pub const fn show_reload_button(mut self, show_reload_button: bool) -> Self {
943        self.config.show_reload_button = show_reload_button;
944        self
945    }
946
947    /// Sets if the "Open working directory" button should be visible in the hamburger menu.
948    /// The working directory button opens to the currently returned working directory
949    /// from `std::env::current_dir()`.
950    ///
951    /// Has no effect when `FileDialog::show_top_panel` or
952    /// `FileDialog::show_menu_button` is disabled.
953    pub const fn show_working_directory_button(
954        mut self,
955        show_working_directory_button: bool,
956    ) -> Self {
957        self.config.show_working_directory_button = show_working_directory_button;
958        self
959    }
960
961    /// Sets whether the show hidden files and folders option inside the top panel
962    /// menu should be visible.
963    ///
964    /// Has no effect when `FileDialog::show_top_panel` or
965    /// `FileDialog::show_menu_button` is disabled.
966    pub const fn show_hidden_option(mut self, show_hidden_option: bool) -> Self {
967        self.config.show_hidden_option = show_hidden_option;
968        self
969    }
970
971    /// Sets whether the show system files option inside the top panel
972    /// menu should be visible.
973    ///
974    /// Has no effect when `FileDialog::show_top_panel` or
975    /// `FileDialog::show_menu_button` is disabled.
976    pub const fn show_system_files_option(mut self, show_system_files_option: bool) -> Self {
977        self.config.show_system_files_option = show_system_files_option;
978        self
979    }
980
981    /// Sets whether the search input should be visible in the top panel.
982    ///
983    /// Has no effect when `FileDialog::show_top_panel` is disabled.
984    pub const fn show_search(mut self, show_search: bool) -> Self {
985        self.config.show_search = show_search;
986        self
987    }
988
989    /// Sets if the sidebar with the shortcut directories such as
990    /// “Home”, “Documents” etc. should be visible.
991    pub const fn show_left_panel(mut self, show_left_panel: bool) -> Self {
992        self.config.show_left_panel = show_left_panel;
993        self
994    }
995
996    /// Sets if pinned folders should be listed in the left sidebar.
997    /// Disabling this will also disable the functionality to pin a folder.
998    pub const fn show_pinned_folders(mut self, show_pinned_folders: bool) -> Self {
999        self.config.show_pinned_folders = show_pinned_folders;
1000        self
1001    }
1002
1003    /// Sets if the "Places" section should be visible in the left sidebar.
1004    /// The Places section contains the user directories such as Home or Documents.
1005    ///
1006    /// Has no effect when `FileDialog::show_left_panel` is disabled.
1007    pub const fn show_places(mut self, show_places: bool) -> Self {
1008        self.config.show_places = show_places;
1009        self
1010    }
1011
1012    /// Sets if the "Devices" section should be visible in the left sidebar.
1013    /// The Devices section contains the non removable system disks.
1014    ///
1015    /// Has no effect when `FileDialog::show_left_panel` is disabled.
1016    pub const fn show_devices(mut self, show_devices: bool) -> Self {
1017        self.config.show_devices = show_devices;
1018        self
1019    }
1020
1021    /// Sets if the "Removable Devices" section should be visible in the left sidebar.
1022    /// The Removable Devices section contains the removable disks like USB disks.
1023    ///
1024    /// Has no effect when `FileDialog::show_left_panel` is disabled.
1025    pub const fn show_removable_devices(mut self, show_removable_devices: bool) -> Self {
1026        self.config.show_removable_devices = show_removable_devices;
1027        self
1028    }
1029
1030    // -------------------------------------------------
1031    // Getter:
1032
1033    /// Returns the directory or file that the user picked, or the target file
1034    /// if the dialog is in `DialogMode::SaveFile` mode.
1035    ///
1036    /// None is returned when the user has not yet selected an item.
1037    pub fn picked(&self) -> Option<&Path> {
1038        match &self.state {
1039            DialogState::Picked(path) => Some(path),
1040            _ => None,
1041        }
1042    }
1043
1044    /// Returns the directory or file that the user picked, or the target file
1045    /// if the dialog is in `DialogMode::SaveFile` mode.
1046    /// Unlike `FileDialog::picked`, this method returns the picked path only once and
1047    /// sets the dialog's state to `DialogState::Closed`.
1048    ///
1049    /// None is returned when the user has not yet picked an item.
1050    pub fn take_picked(&mut self) -> Option<PathBuf> {
1051        match &mut self.state {
1052            DialogState::Picked(path) => {
1053                let path = std::mem::take(path);
1054                self.state = DialogState::Closed;
1055                Some(path)
1056            }
1057            _ => None,
1058        }
1059    }
1060
1061    /// Returns a list of the files and folders the user picked, when the dialog is in
1062    /// `DialogMode::PickMultiple` mode.
1063    ///
1064    /// None is returned when the user has not yet picked an item.
1065    pub fn picked_multiple(&self) -> Option<Vec<&Path>> {
1066        match &self.state {
1067            DialogState::PickedMultiple(items) => {
1068                Some(items.iter().map(std::path::PathBuf::as_path).collect())
1069            }
1070            _ => None,
1071        }
1072    }
1073
1074    /// Returns a list of the files and folders the user picked, when the dialog is in
1075    /// `DialogMode::PickMultiple` mode.
1076    /// Unlike `FileDialog::picked_multiple`, this method returns the picked paths only once
1077    /// and sets the dialog's state to `DialogState::Closed`.
1078    ///
1079    /// None is returned when the user has not yet picked an item.
1080    pub fn take_picked_multiple(&mut self) -> Option<Vec<PathBuf>> {
1081        match &mut self.state {
1082            DialogState::PickedMultiple(items) => {
1083                let items = std::mem::take(items);
1084                self.state = DialogState::Closed;
1085                Some(items)
1086            }
1087            _ => None,
1088        }
1089    }
1090
1091    /// Returns the currently active directory entry.
1092    ///
1093    /// This is either the currently highlighted entry, or the currently active directory
1094    /// if nothing is being highlighted.
1095    ///
1096    /// For the [`DialogMode::SelectMultiple`] counterpart,
1097    /// see [`FileDialog::active_selected_entries`].
1098    pub const fn selected_entry(&self) -> Option<&DirectoryEntry> {
1099        self.selected_item.as_ref()
1100    }
1101
1102    /// Returns an iterator over the currently selected entries in [`SelectMultiple`] mode.
1103    ///
1104    /// For the counterpart in single selection modes, see [`FileDialog::active_entry`].
1105    ///
1106    /// [`SelectMultiple`]: DialogMode::SelectMultiple
1107    pub fn selected_entries(&self) -> impl Iterator<Item = &DirectoryEntry> {
1108        self.get_dir_content_filtered_iter().filter(|p| p.selected)
1109    }
1110
1111    /// Returns a reference to the currently stored user data.
1112    ///
1113    /// See [`FileDialog::set_user_data`].
1114    pub fn user_data<U: Any>(&self) -> Option<&U> {
1115        #[allow(clippy::coerce_container_to_any)]
1116        self.user_data.as_ref().and_then(|u| u.downcast_ref())
1117    }
1118
1119    /// Returns a mutable reference to the currently stored user data.
1120    ///
1121    /// See [`FileDialog::set_user_data`].
1122    pub fn user_data_mut<U: Any>(&mut self) -> Option<&mut U> {
1123        #[allow(clippy::coerce_container_to_any)]
1124        self.user_data.as_mut().and_then(|u| u.downcast_mut())
1125    }
1126
1127    /// Stores custom user data inside this file dialog.
1128    ///
1129    /// This user data can be used for example to track what purpose you have opened the dialog for.
1130    ///
1131    /// For example, You might have an action for opening a document,
1132    /// and also an action for loading a configuration file.
1133    ///
1134    /// ```
1135    /// enum Action {
1136    ///     OpenDocument,
1137    ///     LoadConfig,
1138    /// }
1139    /// let mut dialog = egui_file_dialog::FileDialog::new();
1140    /// // ...
1141    /// // When the user presses "Open document" button
1142    /// dialog.set_user_data(Action::OpenDocument);
1143    /// // ... later, you check what action to perform
1144    /// match dialog.user_data::<Action>() {
1145    ///     Some(Action::OpenDocument) => { /* Open the document */ },
1146    ///     Some(Action::LoadConfig) => { /* Load the config file */},
1147    ///     None => { /* Do nothing */}
1148    /// }
1149    /// ```
1150    pub fn set_user_data<U: Any + Send + Sync>(&mut self, user_data: U) {
1151        self.user_data = Some(Box::new(user_data));
1152    }
1153
1154    /// Returns the mode the dialog is currently in.
1155    pub const fn mode(&self) -> DialogMode {
1156        self.mode
1157    }
1158
1159    /// Returns the state the dialog is currently in.
1160    pub const fn state(&self) -> &DialogState {
1161        &self.state
1162    }
1163
1164    /// Get the window Id
1165    pub const fn get_window_id(&self) -> egui::Id {
1166        self.window_id
1167    }
1168}
1169
1170/// UI methods
1171impl FileDialog {
1172    /// Main update method of the UI
1173    ///
1174    /// Takes an optional callback to show a custom right panel.
1175    fn update_ui(
1176        &mut self,
1177        ctx: &egui::Context,
1178        right_panel_fn: Option<&mut FileDialogUiCallback>,
1179    ) {
1180        let mut is_open = true;
1181
1182        if self.config.as_modal {
1183            let re = self.ui_update_modal_background(ctx);
1184            ctx.move_to_top(re.response.layer_id);
1185        }
1186
1187        let re = self.create_window(&mut is_open).show(ctx, |ui| {
1188            if !self.modals.is_empty() {
1189                self.ui_update_modals(ui);
1190                return;
1191            }
1192
1193            if self.config.show_top_panel {
1194                egui::TopBottomPanel::top(self.window_id.with("top_panel"))
1195                    .resizable(false)
1196                    .show_inside(ui, |ui| {
1197                        self.ui_update_top_panel(ui);
1198                    });
1199            }
1200
1201            if self.config.show_left_panel {
1202                egui::SidePanel::left(self.window_id.with("left_panel"))
1203                    .resizable(true)
1204                    .default_width(150.0)
1205                    .width_range(90.0..=250.0)
1206                    .show_inside(ui, |ui| {
1207                        self.ui_update_left_panel(ui);
1208                    });
1209            }
1210
1211            // Optionally, show a custom right panel (see `update_with_custom_right_panel`)
1212            if let Some(f) = right_panel_fn {
1213                let mut right_panel = egui::SidePanel::right(self.window_id.with("right_panel"))
1214                    // Unlike the left panel, we have no control over the contents, so
1215                    // we don't restrict the width. It's up to the user to make the UI presentable.
1216                    .resizable(true);
1217                if let Some(width) = self.config.right_panel_width {
1218                    right_panel = right_panel.default_width(width);
1219                }
1220                right_panel.show_inside(ui, |ui| {
1221                    f(ui, self);
1222                });
1223            }
1224
1225            egui::TopBottomPanel::bottom(self.window_id.with("bottom_panel"))
1226                .resizable(false)
1227                .show_inside(ui, |ui| {
1228                    self.ui_update_bottom_panel(ui);
1229                });
1230
1231            egui::CentralPanel::default().show_inside(ui, |ui| {
1232                self.ui_update_central_panel(ui);
1233            });
1234        });
1235
1236        if self.config.as_modal {
1237            if let Some(inner_response) = re {
1238                ctx.move_to_top(inner_response.response.layer_id);
1239            }
1240        }
1241
1242        self.any_focused_last_frame = ctx.memory(egui::Memory::focused).is_some();
1243
1244        // User closed the window without finishing the dialog
1245        if !is_open {
1246            self.cancel();
1247        }
1248
1249        let mut repaint = false;
1250
1251        // Collect dropped files:
1252        ctx.input(|i| {
1253            // Check if files were dropped
1254            if let Some(dropped_file) = i.raw.dropped_files.last() {
1255                if let Some(path) = &dropped_file.path {
1256                    if self.config.file_system.is_dir(path) {
1257                        // If we dropped a directory, go there
1258                        self.load_directory(path.as_path());
1259                        repaint = true;
1260                    } else if let Some(parent) = path.parent() {
1261                        // Else, go to the parent directory
1262                        self.load_directory(parent);
1263                        self.select_item(&mut DirectoryEntry::from_path(
1264                            &self.config,
1265                            path,
1266                            &*self.config.file_system,
1267                        ));
1268                        self.scroll_to_selection = true;
1269                        repaint = true;
1270                    }
1271                }
1272            }
1273        });
1274
1275        // Update GUI if we dropped a file
1276        if repaint {
1277            ctx.request_repaint();
1278        }
1279    }
1280
1281    /// Updates the main modal background of the file dialog window.
1282    fn ui_update_modal_background(&self, ctx: &egui::Context) -> egui::InnerResponse<()> {
1283        egui::Area::new(self.window_id.with("modal_overlay"))
1284            .interactable(true)
1285            .fixed_pos(egui::Pos2::ZERO)
1286            .show(ctx, |ui| {
1287                let content_rect = ctx.input(egui::InputState::content_rect);
1288
1289                ui.allocate_response(content_rect.size(), egui::Sense::click());
1290
1291                ui.painter().rect_filled(
1292                    content_rect,
1293                    egui::CornerRadius::ZERO,
1294                    self.config.modal_overlay_color,
1295                );
1296            })
1297    }
1298
1299    fn ui_update_modals(&mut self, ui: &mut egui::Ui) {
1300        // Currently, a rendering error occurs when only a single central panel is rendered
1301        // inside a window. Therefore, when rendering a modal, we render an invisible bottom panel,
1302        // which prevents the error.
1303        // This is currently a bit hacky and should be adjusted again in the future.
1304        egui::TopBottomPanel::bottom(self.window_id.with("modal_bottom_panel"))
1305            .resizable(false)
1306            .show_separator_line(false)
1307            .show_inside(ui, |_| {});
1308
1309        // We need to use a central panel for the modals so that the
1310        // window doesn't resize to the size of the modal.
1311        egui::CentralPanel::default().show_inside(ui, |ui| {
1312            if let Some(modal) = self.modals.last_mut() {
1313                #[allow(clippy::single_match)]
1314                match modal.update(&self.config, ui) {
1315                    ModalState::Close(action) => {
1316                        self.exec_modal_action(action);
1317                        self.modals.pop();
1318                    }
1319                    ModalState::Pending => {}
1320                }
1321            }
1322        });
1323    }
1324
1325    /// Creates a new egui window with the configured options.
1326    fn create_window<'a>(&self, is_open: &'a mut bool) -> egui::Window<'a> {
1327        let mut window = egui::Window::new(self.get_window_title())
1328            .id(self.window_id)
1329            .open(is_open)
1330            .default_size(self.config.default_size)
1331            .min_size(self.config.min_size)
1332            .resizable(self.config.resizable)
1333            .movable(self.config.movable)
1334            .title_bar(self.config.title_bar)
1335            .collapsible(false);
1336
1337        if let Some(pos) = self.config.default_pos {
1338            window = window.default_pos(pos);
1339        }
1340
1341        if let Some(pos) = self.config.fixed_pos {
1342            window = window.fixed_pos(pos);
1343        }
1344
1345        if let Some((anchor, offset)) = self.config.anchor {
1346            window = window.anchor(anchor, offset);
1347        }
1348
1349        if let Some(size) = self.config.max_size {
1350            window = window.max_size(size);
1351        }
1352
1353        window
1354    }
1355
1356    /// Gets the window title to use.
1357    /// This is either one of the default window titles or the configured window title.
1358    const fn get_window_title(&self) -> &String {
1359        match &self.config.title {
1360            Some(title) => title,
1361            None => match &self.mode {
1362                DialogMode::PickDirectory => &self.config.labels.title_select_directory,
1363                DialogMode::PickFile => &self.config.labels.title_select_file,
1364                DialogMode::PickMultiple => &self.config.labels.title_select_multiple,
1365                DialogMode::SaveFile => &self.config.labels.title_save_file,
1366            },
1367        }
1368    }
1369
1370    /// Updates the top panel of the dialog. Including the navigation buttons,
1371    /// the current path display, the reload button and the search field.
1372    fn ui_update_top_panel(&mut self, ui: &mut egui::Ui) {
1373        const BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(25.0, 25.0);
1374
1375        ui.horizontal(|ui| {
1376            self.ui_update_nav_buttons(ui, BUTTON_SIZE);
1377
1378            let mut path_display_width = ui.available_width();
1379
1380            // Leave some area for the menu button and search input
1381            if self.config.show_reload_button {
1382                path_display_width -= ui
1383                    .style()
1384                    .spacing
1385                    .item_spacing
1386                    .x
1387                    .mul_add(2.5, BUTTON_SIZE.x);
1388            }
1389
1390            if self.config.show_search {
1391                path_display_width -= 140.0;
1392            }
1393
1394            if self.config.show_current_path {
1395                self.ui_update_current_path(ui, path_display_width);
1396            }
1397
1398            // Hamburger menu containing different options
1399            if self.config.show_menu_button
1400                && (self.config.show_reload_button
1401                    || self.config.show_working_directory_button
1402                    || self.config.show_hidden_option
1403                    || self.config.show_system_files_option)
1404            {
1405                ui.allocate_ui_with_layout(
1406                    BUTTON_SIZE,
1407                    egui::Layout::centered_and_justified(egui::Direction::LeftToRight),
1408                    |ui| {
1409                        ui.menu_button("☰", |ui| {
1410                            self.ui_update_hamburger_menu(ui);
1411                        });
1412                    },
1413                );
1414            }
1415
1416            if self.config.show_search {
1417                self.ui_update_search(ui);
1418            }
1419        });
1420
1421        ui.add_space(ui.ctx().style().spacing.item_spacing.y);
1422    }
1423
1424    /// Updates the navigation buttons like parent or previous directory
1425    fn ui_update_nav_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
1426        if self.config.show_parent_button {
1427            if let Some(x) = self.current_directory() {
1428                if self.ui_button_sized(ui, x.parent().is_some(), button_size, "⏶", None) {
1429                    self.load_parent_directory();
1430                }
1431            } else {
1432                let _ = self.ui_button_sized(ui, false, button_size, "⏶", None);
1433            }
1434        }
1435
1436        if self.config.show_back_button
1437            && self.ui_button_sized(
1438                ui,
1439                self.directory_offset + 1 < self.directory_stack.len(),
1440                button_size,
1441                "⏴",
1442                None,
1443            )
1444        {
1445            self.load_previous_directory();
1446        }
1447
1448        if self.config.show_forward_button
1449            && self.ui_button_sized(ui, self.directory_offset != 0, button_size, "⏵", None)
1450        {
1451            self.load_next_directory();
1452        }
1453
1454        if self.config.show_new_folder_button
1455            && self.ui_button_sized(
1456                ui,
1457                !self.create_directory_dialog.is_open(),
1458                button_size,
1459                "+",
1460                None,
1461            )
1462        {
1463            self.open_new_folder_dialog();
1464        }
1465    }
1466
1467    /// Updates the view to display the current path.
1468    /// This could be the view for displaying the current path and the individual sections,
1469    /// as well as the view for text editing of the current path.
1470    fn ui_update_current_path(&mut self, ui: &mut egui::Ui, width: f32) {
1471        egui::Frame::default()
1472            .stroke(egui::Stroke::new(
1473                1.0,
1474                ui.ctx().style().visuals.window_stroke.color,
1475            ))
1476            .inner_margin(egui::Margin::from(4))
1477            .corner_radius(egui::CornerRadius::from(4))
1478            .show(ui, |ui| {
1479                const EDIT_BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(22.0, 20.0);
1480
1481                if self.path_edit_visible {
1482                    self.ui_update_path_edit(ui, width, EDIT_BUTTON_SIZE);
1483                } else {
1484                    self.ui_update_path_display(ui, width, EDIT_BUTTON_SIZE);
1485                }
1486            });
1487    }
1488
1489    /// Updates the view when the currently open path with the individual sections is displayed.
1490    fn ui_update_path_display(
1491        &mut self,
1492        ui: &mut egui::Ui,
1493        width: f32,
1494        edit_button_size: egui::Vec2,
1495    ) {
1496        ui.style_mut().always_scroll_the_only_direction = true;
1497        ui.style_mut().spacing.scroll.bar_width = 8.0;
1498
1499        let max_width = if self.config.show_path_edit_button {
1500            ui.style()
1501                .spacing
1502                .item_spacing
1503                .x
1504                .mul_add(-2.0, width - edit_button_size.x)
1505        } else {
1506            width
1507        };
1508
1509        egui::ScrollArea::horizontal()
1510            .auto_shrink([false, false])
1511            .stick_to_right(true)
1512            .max_width(max_width)
1513            .show(ui, |ui| {
1514                ui.horizontal(|ui| {
1515                    ui.style_mut().spacing.item_spacing.x /= 2.5;
1516                    ui.style_mut().spacing.button_padding = egui::Vec2::new(5.0, 3.0);
1517
1518                    let mut path = PathBuf::new();
1519
1520                    if let Some(data) = self.current_directory().map(Path::to_path_buf) {
1521                        for (i, segment) in data.iter().enumerate() {
1522                            path.push(segment);
1523
1524                            let mut segment_str = segment.to_str().unwrap_or_default().to_string();
1525
1526                            if self.is_pinned(&path) {
1527                                segment_str =
1528                                    format!("{} {}", &self.config.pinned_icon, segment_str);
1529                            }
1530
1531                            if i != 0 {
1532                                ui.label(self.config.directory_separator.as_str());
1533                            }
1534
1535                            let re = ui.button(segment_str);
1536
1537                            if re.clicked() {
1538                                self.load_directory(path.as_path());
1539                                return;
1540                            }
1541
1542                            self.ui_update_central_panel_path_context_menu(&re, &path.clone());
1543                        }
1544                    }
1545                });
1546            });
1547
1548        if !self.config.show_path_edit_button {
1549            return;
1550        }
1551
1552        if ui
1553            .add_sized(
1554                edit_button_size,
1555                egui::Button::new("🖊").fill(egui::Color32::TRANSPARENT),
1556            )
1557            .clicked()
1558        {
1559            self.open_path_edit();
1560        }
1561    }
1562
1563    /// Updates the view when the user currently wants to text edit the current path.
1564    fn ui_update_path_edit(&mut self, ui: &mut egui::Ui, width: f32, edit_button_size: egui::Vec2) {
1565        let desired_width: f32 = ui
1566            .style()
1567            .spacing
1568            .item_spacing
1569            .x
1570            .mul_add(-3.0, width - edit_button_size.x);
1571
1572        let response = egui::TextEdit::singleline(&mut self.path_edit_value)
1573            .desired_width(desired_width)
1574            .show(ui)
1575            .response;
1576
1577        if self.path_edit_activate {
1578            response.request_focus();
1579            Self::set_cursor_to_end(&response, &self.path_edit_value);
1580            self.path_edit_activate = false;
1581        }
1582
1583        if self.path_edit_request_focus {
1584            response.request_focus();
1585            self.path_edit_request_focus = false;
1586        }
1587
1588        let btn_response = ui.add_sized(edit_button_size, egui::Button::new("✔"));
1589
1590        if btn_response.clicked() {
1591            self.submit_path_edit();
1592        }
1593
1594        if !response.has_focus() && !btn_response.contains_pointer() {
1595            self.path_edit_visible = false;
1596        }
1597    }
1598
1599    /// Updates the hamburger menu containing different options.
1600    fn ui_update_hamburger_menu(&mut self, ui: &mut egui::Ui) {
1601        const SEPARATOR_SPACING: f32 = 2.0;
1602
1603        if self.config.show_reload_button && ui.button(&self.config.labels.reload).clicked() {
1604            self.refresh();
1605            ui.close();
1606        }
1607
1608        let working_dir = self.config.file_system.current_dir();
1609
1610        if self.config.show_working_directory_button
1611            && working_dir.is_ok()
1612            && ui.button(&self.config.labels.working_directory).clicked()
1613        {
1614            self.load_directory(&working_dir.unwrap_or_default());
1615            ui.close();
1616        }
1617
1618        if (self.config.show_reload_button || self.config.show_working_directory_button)
1619            && (self.config.show_hidden_option || self.config.show_system_files_option)
1620        {
1621            ui.add_space(SEPARATOR_SPACING);
1622            ui.separator();
1623            ui.add_space(SEPARATOR_SPACING);
1624        }
1625
1626        if self.config.show_hidden_option
1627            && ui
1628                .checkbox(
1629                    &mut self.storage.show_hidden,
1630                    &self.config.labels.show_hidden,
1631                )
1632                .clicked()
1633        {
1634            self.refresh();
1635            ui.close();
1636        }
1637
1638        if self.config.show_system_files_option
1639            && ui
1640                .checkbox(
1641                    &mut self.storage.show_system_files,
1642                    &self.config.labels.show_system_files,
1643                )
1644                .clicked()
1645        {
1646            self.refresh();
1647            ui.close();
1648        }
1649    }
1650
1651    /// Updates the search input
1652    fn ui_update_search(&mut self, ui: &mut egui::Ui) {
1653        egui::Frame::default()
1654            .stroke(egui::Stroke::new(
1655                1.0,
1656                ui.ctx().style().visuals.window_stroke.color,
1657            ))
1658            .inner_margin(egui::Margin::symmetric(4, 4))
1659            .corner_radius(egui::CornerRadius::from(4))
1660            .show(ui, |ui| {
1661                ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1662                    ui.add_space(ui.ctx().style().spacing.item_spacing.y);
1663
1664                    ui.label(egui::RichText::from("🔍").size(15.0));
1665
1666                    let re = ui.add_sized(
1667                        egui::Vec2::new(ui.available_width(), 0.0),
1668                        egui::TextEdit::singleline(&mut self.search_value),
1669                    );
1670
1671                    self.edit_search_on_text_input(ui);
1672
1673                    if re.changed() || self.init_search {
1674                        self.selected_item = None;
1675                        self.select_first_visible_item();
1676                    }
1677
1678                    if self.init_search {
1679                        re.request_focus();
1680                        Self::set_cursor_to_end(&re, &self.search_value);
1681                        self.directory_content.reset_multi_selection();
1682
1683                        self.init_search = false;
1684                    }
1685                });
1686            });
1687    }
1688
1689    /// Focuses and types into the search input, if text input without
1690    /// shortcut modifiers is detected, and no other inputs are focused.
1691    ///
1692    /// # Arguments
1693    ///
1694    /// - `re`: The [`egui::Response`] returned by the filter text edit widget
1695    fn edit_search_on_text_input(&mut self, ui: &egui::Ui) {
1696        if ui.memory(|mem| mem.focused().is_some()) {
1697            return;
1698        }
1699
1700        ui.input(|inp| {
1701            // We stop if any modifier is active besides only shift
1702            if inp.modifiers.any() && !inp.modifiers.shift_only() {
1703                return;
1704            }
1705
1706            // If we find any text input event, we append it to the filter string
1707            // and allow proceeding to activating the filter input widget.
1708            for text in inp.events.iter().filter_map(|ev| match ev {
1709                egui::Event::Text(t) => Some(t),
1710                _ => None,
1711            }) {
1712                self.search_value.push_str(text);
1713                self.init_search = true;
1714            }
1715        });
1716    }
1717
1718    /// Updates the left panel of the dialog. Including the list of the user directories (Places)
1719    /// and system disks (Devices, Removable Devices).
1720    fn ui_update_left_panel(&mut self, ui: &mut egui::Ui) {
1721        ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
1722            // Spacing multiplier used between sections in the left sidebar
1723            const SPACING_MULTIPLIER: f32 = 4.0;
1724
1725            egui::containers::ScrollArea::vertical()
1726                .auto_shrink([false, false])
1727                .show(ui, |ui| {
1728                    // Spacing for the first section in the left sidebar
1729                    let mut spacing = ui.ctx().style().spacing.item_spacing.y * 2.0;
1730
1731                    // Update paths pinned to the left sidebar by the user
1732                    if self.config.show_pinned_folders && self.ui_update_pinned_folders(ui, spacing)
1733                    {
1734                        spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1735                    }
1736
1737                    // Update custom quick access sections
1738                    let quick_accesses = std::mem::take(&mut self.config.quick_accesses);
1739
1740                    for quick_access in &quick_accesses {
1741                        ui.add_space(spacing);
1742                        self.ui_update_quick_access(ui, quick_access);
1743                        spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1744                    }
1745
1746                    self.config.quick_accesses = quick_accesses;
1747
1748                    // Update native quick access sections
1749                    if self.config.show_places && self.ui_update_user_directories(ui, spacing) {
1750                        spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1751                    }
1752
1753                    let disks = std::mem::take(&mut self.system_disks);
1754
1755                    if self.config.show_devices && self.ui_update_devices(ui, spacing, &disks) {
1756                        spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1757                    }
1758
1759                    if self.config.show_removable_devices
1760                        && self.ui_update_removable_devices(ui, spacing, &disks)
1761                    {
1762                        // Add this when we add a new section after removable devices
1763                        // spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1764                    }
1765
1766                    self.system_disks = disks;
1767                });
1768        });
1769    }
1770
1771    /// Updates a path entry in the left panel.
1772    ///
1773    /// Returns the response of the selectable label.
1774    fn ui_update_left_panel_entry(
1775        &mut self,
1776        ui: &mut egui::Ui,
1777        display_name: &str,
1778        path: &Path,
1779    ) -> egui::Response {
1780        let response = ui.selectable_label(self.current_directory() == Some(path), display_name);
1781
1782        if response.clicked() {
1783            self.load_directory(path);
1784        }
1785
1786        response
1787    }
1788
1789    /// Updates a custom quick access section added to the left panel.
1790    fn ui_update_quick_access(&mut self, ui: &mut egui::Ui, quick_access: &QuickAccess) {
1791        ui.label(&quick_access.heading);
1792
1793        for entry in &quick_access.paths {
1794            self.ui_update_left_panel_entry(ui, &entry.display_name, &entry.path);
1795        }
1796    }
1797
1798    /// Updates the list of pinned folders.
1799    ///
1800    /// Returns true if at least one directory item was included in the list and the
1801    /// heading is visible. If no item was listed, false is returned.
1802    fn ui_update_pinned_folders(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1803        let mut visible = false;
1804
1805        for (i, pinned) in self.storage.pinned_folders.clone().iter().enumerate() {
1806            if i == 0 {
1807                ui.add_space(spacing);
1808                ui.label(self.config.labels.heading_pinned.as_str());
1809
1810                visible = true;
1811            }
1812
1813            if self.is_pinned_folder_being_renamed(pinned) {
1814                self.ui_update_pinned_folder_rename(ui);
1815                continue;
1816            }
1817
1818            let response = self.ui_update_left_panel_entry(
1819                ui,
1820                &format!("{}  {}", self.config.pinned_icon, &pinned.label),
1821                pinned.path.as_path(),
1822            );
1823
1824            self.ui_update_pinned_folder_context_menu(&response, pinned);
1825        }
1826
1827        visible
1828    }
1829
1830    fn ui_update_pinned_folder_rename(&mut self, ui: &mut egui::Ui) {
1831        if let Some(r) = &mut self.rename_pinned_folder {
1832            let id = self.window_id.with("pinned_folder_rename").with(&r.path);
1833            let mut output = egui::TextEdit::singleline(&mut r.label)
1834                .id(id)
1835                .cursor_at_end(true)
1836                .show(ui);
1837
1838            if self.rename_pinned_folder_request_focus {
1839                output.state.cursor.set_char_range(Some(CCursorRange::two(
1840                    CCursor::new(0),
1841                    CCursor::new(r.label.chars().count()),
1842                )));
1843                output.state.store(ui.ctx(), output.response.id);
1844
1845                output.response.request_focus();
1846
1847                self.rename_pinned_folder_request_focus = false;
1848            }
1849
1850            if output.response.lost_focus() {
1851                self.end_rename_pinned_folder();
1852            }
1853        }
1854    }
1855
1856    fn ui_update_pinned_folder_context_menu(
1857        &mut self,
1858        item: &egui::Response,
1859        pinned: &PinnedFolder,
1860    ) {
1861        item.context_menu(|ui| {
1862            if ui.button(&self.config.labels.unpin_folder).clicked() {
1863                self.unpin_path(&pinned.path);
1864                ui.close();
1865            }
1866
1867            if ui
1868                .button(&self.config.labels.rename_pinned_folder)
1869                .clicked()
1870            {
1871                self.begin_rename_pinned_folder(pinned.clone());
1872                ui.close();
1873            }
1874        });
1875    }
1876
1877    /// Updates the list of user directories (Places).
1878    ///
1879    /// Returns true if at least one directory was included in the list and the
1880    /// heading is visible. If no directory was listed, false is returned.
1881    fn ui_update_user_directories(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1882        // Take temporary ownership of the user directories and configuration.
1883        // This is done so that we don't have to clone the user directories and
1884        // configured display names.
1885        let user_directories = std::mem::take(&mut self.user_directories);
1886        let labels = std::mem::take(&mut self.config.labels);
1887
1888        let visible = if let Some(dirs) = &user_directories {
1889            ui.add_space(spacing);
1890            ui.label(labels.heading_places.as_str());
1891
1892            if let Some(path) = dirs.home_dir() {
1893                self.ui_update_left_panel_entry(ui, &labels.home_dir, path);
1894            }
1895            if let Some(path) = dirs.desktop_dir() {
1896                self.ui_update_left_panel_entry(ui, &labels.desktop_dir, path);
1897            }
1898            if let Some(path) = dirs.document_dir() {
1899                self.ui_update_left_panel_entry(ui, &labels.documents_dir, path);
1900            }
1901            if let Some(path) = dirs.download_dir() {
1902                self.ui_update_left_panel_entry(ui, &labels.downloads_dir, path);
1903            }
1904            if let Some(path) = dirs.audio_dir() {
1905                self.ui_update_left_panel_entry(ui, &labels.audio_dir, path);
1906            }
1907            if let Some(path) = dirs.picture_dir() {
1908                self.ui_update_left_panel_entry(ui, &labels.pictures_dir, path);
1909            }
1910            if let Some(path) = dirs.video_dir() {
1911                self.ui_update_left_panel_entry(ui, &labels.videos_dir, path);
1912            }
1913
1914            true
1915        } else {
1916            false
1917        };
1918
1919        self.user_directories = user_directories;
1920        self.config.labels = labels;
1921
1922        visible
1923    }
1924
1925    /// Updates the list of devices like system disks.
1926    ///
1927    /// Returns true if at least one device was included in the list and the
1928    /// heading is visible. If no device was listed, false is returned.
1929    fn ui_update_devices(&mut self, ui: &mut egui::Ui, spacing: f32, disks: &Disks) -> bool {
1930        let mut visible = false;
1931
1932        for (i, disk) in disks.iter().filter(|x| !x.is_removable()).enumerate() {
1933            if i == 0 {
1934                ui.add_space(spacing);
1935                ui.label(self.config.labels.heading_devices.as_str());
1936
1937                visible = true;
1938            }
1939
1940            self.ui_update_device_entry(ui, disk);
1941        }
1942
1943        visible
1944    }
1945
1946    /// Updates the list of removable devices like USB drives.
1947    ///
1948    /// Returns true if at least one device was included in the list and the
1949    /// heading is visible. If no device was listed, false is returned.
1950    fn ui_update_removable_devices(
1951        &mut self,
1952        ui: &mut egui::Ui,
1953        spacing: f32,
1954        disks: &Disks,
1955    ) -> bool {
1956        let mut visible = false;
1957
1958        for (i, disk) in disks.iter().filter(|x| x.is_removable()).enumerate() {
1959            if i == 0 {
1960                ui.add_space(spacing);
1961                ui.label(self.config.labels.heading_removable_devices.as_str());
1962
1963                visible = true;
1964            }
1965
1966            self.ui_update_device_entry(ui, disk);
1967        }
1968
1969        visible
1970    }
1971
1972    /// Updates a device entry of a device list like "Devices" or "Removable Devices".
1973    fn ui_update_device_entry(&mut self, ui: &mut egui::Ui, device: &Disk) {
1974        let label = if device.is_removable() {
1975            format!(
1976                "{}  {}",
1977                self.config.removable_device_icon,
1978                device.display_name()
1979            )
1980        } else {
1981            format!("{}  {}", self.config.device_icon, device.display_name())
1982        };
1983
1984        self.ui_update_left_panel_entry(ui, &label, device.mount_point());
1985    }
1986
1987    /// Updates the bottom panel showing the selected item and main action buttons.
1988    fn ui_update_bottom_panel(&mut self, ui: &mut egui::Ui) {
1989        const BUTTON_HEIGHT: f32 = 20.0;
1990        ui.add_space(5.0);
1991
1992        // Calculate the width of the action buttons
1993        let label_submit_width = match self.mode {
1994            DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
1995                Self::calc_text_width(ui, &self.config.labels.open_button)
1996            }
1997            DialogMode::SaveFile => Self::calc_text_width(ui, &self.config.labels.save_button),
1998        };
1999
2000        let mut btn_width = Self::calc_text_width(ui, &self.config.labels.cancel_button);
2001        if label_submit_width > btn_width {
2002            btn_width = label_submit_width;
2003        }
2004
2005        btn_width += ui.spacing().button_padding.x * 4.0;
2006
2007        // The size of the action buttons "cancel" and "open"/"save"
2008        let button_size: egui::Vec2 = egui::Vec2::new(btn_width, BUTTON_HEIGHT);
2009
2010        self.ui_update_selection_preview(ui, button_size);
2011
2012        if self.mode == DialogMode::SaveFile && self.config.save_extensions.is_empty() {
2013            ui.add_space(ui.style().spacing.item_spacing.y);
2014        }
2015
2016        self.ui_update_action_buttons(ui, button_size);
2017    }
2018
2019    /// Updates the selection preview like "Selected directory: X"
2020    fn ui_update_selection_preview(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2021        const SELECTION_PREVIEW_MIN_WIDTH: f32 = 50.0;
2022        let item_spacing = ui.style().spacing.item_spacing;
2023
2024        let render_filter_selection = (!self.config.file_filters.is_empty()
2025            && (self.mode == DialogMode::PickFile || self.mode == DialogMode::PickMultiple))
2026            || (!self.config.save_extensions.is_empty() && self.mode == DialogMode::SaveFile);
2027
2028        let filter_selection_width = button_size.x.mul_add(2.0, item_spacing.x);
2029        let mut filter_selection_separate_line = false;
2030
2031        ui.horizontal(|ui| {
2032            match &self.mode {
2033                DialogMode::PickDirectory => ui.label(&self.config.labels.selected_directory),
2034                DialogMode::PickFile => ui.label(&self.config.labels.selected_file),
2035                DialogMode::PickMultiple => ui.label(&self.config.labels.selected_items),
2036                DialogMode::SaveFile => ui.label(&self.config.labels.file_name),
2037            };
2038
2039            // Make sure there is enough width for the selection preview. If the available
2040            // width is not enough, render the drop-down menu to select a file filter or
2041            // save extension on a separate line and give the selection preview
2042            // the entire available width.
2043            let mut scroll_bar_width: f32 =
2044                ui.available_width() - filter_selection_width - item_spacing.x;
2045
2046            if scroll_bar_width < SELECTION_PREVIEW_MIN_WIDTH || !render_filter_selection {
2047                filter_selection_separate_line = true;
2048                scroll_bar_width = ui.available_width();
2049            }
2050
2051            match &self.mode {
2052                DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2053                    use egui::containers::scroll_area::ScrollBarVisibility;
2054
2055                    let text = self.get_selection_preview_text();
2056
2057                    egui::containers::ScrollArea::horizontal()
2058                        .auto_shrink([false, false])
2059                        .max_width(scroll_bar_width)
2060                        .stick_to_right(true)
2061                        .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
2062                        .show(ui, |ui| {
2063                            ui.colored_label(ui.style().visuals.selection.bg_fill, text);
2064                        });
2065                }
2066                DialogMode::SaveFile => {
2067                    let mut output = egui::TextEdit::singleline(&mut self.file_name_input)
2068                        .cursor_at_end(false)
2069                        .margin(egui::Margin::symmetric(4, 3))
2070                        .desired_width(scroll_bar_width - item_spacing.x)
2071                        .show(ui);
2072
2073                    if self.file_name_input_request_focus {
2074                        self.highlight_file_name_input(&mut output);
2075                        output.state.store(ui.ctx(), output.response.id);
2076
2077                        output.response.request_focus();
2078                        self.file_name_input_request_focus = false;
2079                    }
2080
2081                    if output.response.changed() {
2082                        self.file_name_input_error = self.validate_file_name_input();
2083                    }
2084
2085                    if output.response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
2086                    {
2087                        self.submit();
2088                    }
2089                }
2090            }
2091
2092            if !filter_selection_separate_line && render_filter_selection {
2093                if self.mode == DialogMode::SaveFile {
2094                    self.ui_update_save_extension_selection(ui, filter_selection_width);
2095                } else {
2096                    self.ui_update_file_filter_selection(ui, filter_selection_width);
2097                }
2098            }
2099        });
2100
2101        if filter_selection_separate_line && render_filter_selection {
2102            ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2103                if self.mode == DialogMode::SaveFile {
2104                    self.ui_update_save_extension_selection(ui, filter_selection_width);
2105                } else {
2106                    self.ui_update_file_filter_selection(ui, filter_selection_width);
2107                }
2108            });
2109        }
2110    }
2111
2112    /// Highlights the characters inside the file name input until the file extension.
2113    /// Do not forget to store these changes after calling this function:
2114    /// `output.state.store(ui.ctx(), output.response.id);`
2115    fn highlight_file_name_input(&self, output: &mut egui::text_edit::TextEditOutput) {
2116        if let Some(pos) = self.file_name_input.rfind('.') {
2117            let range = if pos == 0 {
2118                CCursorRange::two(CCursor::new(0), CCursor::new(0))
2119            } else {
2120                CCursorRange::two(CCursor::new(0), CCursor::new(pos))
2121            };
2122
2123            output.state.cursor.set_char_range(Some(range));
2124        }
2125    }
2126
2127    fn get_selection_preview_text(&self) -> String {
2128        if self.is_selection_valid() {
2129            match &self.mode {
2130                DialogMode::PickDirectory | DialogMode::PickFile => self
2131                    .selected_item
2132                    .as_ref()
2133                    .map_or_else(String::new, |item| item.file_name().to_string()),
2134                DialogMode::PickMultiple => {
2135                    let mut result = String::new();
2136
2137                    for (i, item) in self
2138                        .get_dir_content_filtered_iter()
2139                        .filter(|p| p.selected)
2140                        .enumerate()
2141                    {
2142                        if i == 0 {
2143                            result += item.file_name();
2144                            continue;
2145                        }
2146
2147                        result += format!(", {}", item.file_name()).as_str();
2148                    }
2149
2150                    result
2151                }
2152                DialogMode::SaveFile => String::new(),
2153            }
2154        } else {
2155            String::new()
2156        }
2157    }
2158
2159    fn ui_update_file_filter_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2160        let selected_filter = self.get_selected_file_filter();
2161        let selected_text = match selected_filter {
2162            Some(f) => &f.name,
2163            None => &self.config.labels.file_filter_all_files,
2164        };
2165
2166        // The item that the user selected inside the drop down.
2167        // If none, the user did not change the selected item this frame.
2168        let mut select_filter: Option<Option<FileFilter>> = None;
2169
2170        egui::containers::ComboBox::from_id_salt(self.window_id.with("file_filter_selection"))
2171            .width(width)
2172            .selected_text(selected_text)
2173            .wrap_mode(egui::TextWrapMode::Truncate)
2174            .show_ui(ui, |ui| {
2175                for filter in &self.config.file_filters {
2176                    let selected = selected_filter.is_some_and(|f| f.id == filter.id);
2177
2178                    if ui.selectable_label(selected, &filter.name).clicked() {
2179                        select_filter = Some(Some(filter.clone()));
2180                    }
2181                }
2182
2183                if ui
2184                    .selectable_label(
2185                        selected_filter.is_none(),
2186                        &self.config.labels.file_filter_all_files,
2187                    )
2188                    .clicked()
2189                {
2190                    select_filter = Some(None);
2191                }
2192            });
2193
2194        if let Some(i) = select_filter {
2195            self.select_file_filter(i);
2196        }
2197    }
2198
2199    fn ui_update_save_extension_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2200        let selected_extension = self.get_selected_save_extension();
2201        let selected_text = match selected_extension {
2202            Some(e) => &e.to_string(),
2203            None => &self.config.labels.save_extension_any,
2204        };
2205
2206        // The item that the user selected inside the drop down.
2207        // If none, the user did not change the selected item this frame.
2208        let mut select_extension: Option<Option<SaveExtension>> = None;
2209
2210        egui::containers::ComboBox::from_id_salt(self.window_id.with("save_extension_selection"))
2211            .width(width)
2212            .selected_text(selected_text)
2213            .wrap_mode(egui::TextWrapMode::Truncate)
2214            .show_ui(ui, |ui| {
2215                for extension in &self.config.save_extensions {
2216                    let selected = selected_extension.is_some_and(|s| s.id == extension.id);
2217
2218                    if ui
2219                        .selectable_label(selected, extension.to_string())
2220                        .clicked()
2221                    {
2222                        select_extension = Some(Some(extension.clone()));
2223                    }
2224                }
2225            });
2226
2227        if let Some(i) = select_extension {
2228            self.file_name_input_request_focus = true;
2229            self.select_save_extension(i);
2230        }
2231    }
2232
2233    /// Updates the action buttons like save, open and cancel
2234    fn ui_update_action_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2235        ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2236            let label = match &self.mode {
2237                DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2238                    self.config.labels.open_button.as_str()
2239                }
2240                DialogMode::SaveFile => self.config.labels.save_button.as_str(),
2241            };
2242
2243            if self.ui_button_sized(
2244                ui,
2245                self.is_selection_valid(),
2246                button_size,
2247                label,
2248                self.file_name_input_error.as_deref(),
2249            ) {
2250                self.submit();
2251            }
2252
2253            if ui
2254                .add_sized(
2255                    button_size,
2256                    egui::Button::new(self.config.labels.cancel_button.as_str()),
2257                )
2258                .clicked()
2259            {
2260                self.cancel();
2261            }
2262        });
2263    }
2264
2265    /// Updates the central panel. This is either the contents of the directory
2266    /// or the error message when there was an error loading the current directory.
2267    fn ui_update_central_panel(&mut self, ui: &mut egui::Ui) {
2268        if self.update_directory_content(ui) {
2269            return;
2270        }
2271
2272        self.ui_update_central_panel_content(ui);
2273    }
2274
2275    /// Updates the directory content (Not the UI!).
2276    /// This is required because the contents of the directory might be loaded on a
2277    /// separate thread. This function checks the status of the directory content
2278    /// and updates the UI accordingly.
2279    fn update_directory_content(&mut self, ui: &mut egui::Ui) -> bool {
2280        const SHOW_SPINNER_AFTER: f32 = 0.2;
2281
2282        match self.directory_content.update() {
2283            DirectoryContentState::Pending(timestamp) => {
2284                let now = std::time::SystemTime::now();
2285
2286                if now
2287                    .duration_since(*timestamp)
2288                    .unwrap_or_default()
2289                    .as_secs_f32()
2290                    > SHOW_SPINNER_AFTER
2291                {
2292                    ui.centered_and_justified(egui::Ui::spinner);
2293                }
2294
2295                // Prevent egui from not updating the UI when there is no user input
2296                ui.ctx().request_repaint();
2297
2298                true
2299            }
2300            DirectoryContentState::Errored(err) => {
2301                ui.centered_and_justified(|ui| ui.colored_label(ui.visuals().error_fg_color, err));
2302                true
2303            }
2304            DirectoryContentState::Finished => {
2305                if self.mode == DialogMode::PickDirectory {
2306                    if let Some(dir) = self.current_directory() {
2307                        let mut dir_entry =
2308                            DirectoryEntry::from_path(&self.config, dir, &*self.config.file_system);
2309                        self.select_item(&mut dir_entry);
2310                    }
2311                }
2312
2313                false
2314            }
2315            DirectoryContentState::Success => false,
2316        }
2317    }
2318
2319    /// Updates the contents of the currently open directory.
2320    /// TODO: Refactor
2321    fn ui_update_central_panel_content(&mut self, ui: &mut egui::Ui) {
2322        // Temporarily take ownership of the directory content.
2323        let mut data = std::mem::take(&mut self.directory_content);
2324
2325        // If the multi selection should be reset, excluding the currently
2326        // selected primary item.
2327        let mut reset_multi_selection = false;
2328
2329        // The item the user wants to make a batch selection from.
2330        // The primary selected item is used for item a.
2331        let mut batch_select_item_b: Option<DirectoryEntry> = None;
2332
2333        // If we should return after updating the directory entries.
2334        let mut should_return = false;
2335
2336        ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
2337            let scroll_area = egui::containers::ScrollArea::vertical().auto_shrink([false, false]);
2338
2339            if self.search_value.is_empty()
2340                && !self.create_directory_dialog.is_open()
2341                && !self.scroll_to_selection
2342            {
2343                // Only update visible items when the search value is empty,
2344                // the create directory dialog is closed and we are currently not scrolling
2345                // to the current item.
2346                scroll_area.show_rows(ui, ui.spacing().interact_size.y, data.len(), |ui, range| {
2347                    for item in data.iter_range_mut(range) {
2348                        if self.ui_update_central_panel_entry(
2349                            ui,
2350                            item,
2351                            &mut reset_multi_selection,
2352                            &mut batch_select_item_b,
2353                        ) {
2354                            should_return = true;
2355                        }
2356                    }
2357                });
2358            } else {
2359                // Update each element if the search value is not empty as we apply the
2360                // search value in every frame. We can't use `egui::ScrollArea::show_rows`
2361                // because we don't know how many files the search value applies to.
2362                // We also have to update every item when the create directory dialog is open as
2363                // it's displayed as the last element.
2364                scroll_area.show(ui, |ui| {
2365                    for item in data.filtered_iter_mut(&self.search_value.clone()) {
2366                        if self.ui_update_central_panel_entry(
2367                            ui,
2368                            item,
2369                            &mut reset_multi_selection,
2370                            &mut batch_select_item_b,
2371                        ) {
2372                            should_return = true;
2373                        }
2374                    }
2375
2376                    if let Some(entry) = self.ui_update_create_directory_dialog(ui) {
2377                        data.push(entry);
2378                    }
2379                });
2380            }
2381        });
2382
2383        if should_return {
2384            return;
2385        }
2386
2387        // Reset the multi selection except the currently selected primary item
2388        if reset_multi_selection {
2389            for item in data.filtered_iter_mut(&self.search_value) {
2390                if let Some(selected_item) = &self.selected_item {
2391                    if selected_item.path_eq(item) {
2392                        continue;
2393                    }
2394                }
2395
2396                item.selected = false;
2397            }
2398        }
2399
2400        // Check if we should perform a batch selection
2401        if let Some(item_b) = batch_select_item_b {
2402            if let Some(item_a) = &self.selected_item {
2403                self.batch_select_between(&mut data, item_a, &item_b);
2404            }
2405        }
2406
2407        self.directory_content = data;
2408        self.scroll_to_selection = false;
2409    }
2410
2411    /// Updates a single directory content entry.
2412    /// TODO: Refactor
2413    fn ui_update_central_panel_entry(
2414        &mut self,
2415        ui: &mut egui::Ui,
2416        item: &mut DirectoryEntry,
2417        reset_multi_selection: &mut bool,
2418        batch_select_item_b: &mut Option<DirectoryEntry>,
2419    ) -> bool {
2420        let file_name = item.file_name();
2421        let primary_selected = self.is_primary_selected(item);
2422        let pinned = self.is_pinned(item.as_path());
2423
2424        let icons = if pinned {
2425            format!("{} {} ", item.icon(), self.config.pinned_icon)
2426        } else {
2427            format!("{} ", item.icon())
2428        };
2429
2430        let icons_width = Self::calc_text_width(ui, &icons);
2431
2432        // Calc available width for the file name and include a small margin
2433        let available_width = ui.available_width() - icons_width - 15.0;
2434
2435        let truncate = self.config.truncate_filenames
2436            && available_width < Self::calc_text_width(ui, file_name);
2437
2438        let text = if truncate {
2439            Self::truncate_filename(ui, item, available_width)
2440        } else {
2441            file_name.to_owned()
2442        };
2443
2444        let mut re =
2445            ui.selectable_label(primary_selected || item.selected, format!("{icons}{text}"));
2446
2447        if truncate {
2448            re = re.on_hover_text(file_name);
2449        }
2450
2451        if item.is_dir() {
2452            self.ui_update_central_panel_path_context_menu(&re, item.as_path());
2453
2454            if re.context_menu_opened() {
2455                self.select_item(item);
2456            }
2457        }
2458
2459        if primary_selected && self.scroll_to_selection {
2460            re.scroll_to_me(Some(egui::Align::Center));
2461            self.scroll_to_selection = false;
2462        }
2463
2464        // The user wants to select the item as the primary selected item
2465        if re.clicked()
2466            && !ui.input(|i| i.modifiers.command)
2467            && !ui.input(|i| i.modifiers.shift_only())
2468        {
2469            self.select_item(item);
2470
2471            // Reset the multi selection except the now primary selected item
2472            if self.mode == DialogMode::PickMultiple {
2473                *reset_multi_selection = true;
2474            }
2475        }
2476
2477        // The user wants to select or unselect the item as part of a
2478        // multi selection
2479        if self.mode == DialogMode::PickMultiple
2480            && re.clicked()
2481            && ui.input(|i| i.modifiers.command)
2482        {
2483            if primary_selected {
2484                // If the clicked item is the primary selected item,
2485                // deselect it and remove it from the multi selection
2486                item.selected = false;
2487                self.selected_item = None;
2488            } else {
2489                item.selected = !item.selected;
2490
2491                // If the item was selected, make it the primary selected item
2492                if item.selected {
2493                    self.select_item(item);
2494                }
2495            }
2496        }
2497
2498        // The user wants to select every item between the last selected item
2499        // and the current item
2500        if self.mode == DialogMode::PickMultiple
2501            && re.clicked()
2502            && ui.input(|i| i.modifiers.shift_only())
2503        {
2504            if let Some(selected_item) = self.selected_item.clone() {
2505                // We perform a batch selection from the item that was
2506                // primarily selected before the user clicked on this item.
2507                *batch_select_item_b = Some(selected_item);
2508
2509                // And now make this item the primary selected item
2510                item.selected = true;
2511                self.select_item(item);
2512            }
2513        }
2514
2515        // The user double clicked on the directory entry.
2516        // Either open the directory or submit the dialog.
2517        if re.double_clicked() && !ui.input(|i| i.modifiers.command) {
2518            if item.is_dir() {
2519                self.load_directory(&item.to_path_buf());
2520                return true;
2521            }
2522
2523            self.select_item(item);
2524
2525            self.submit();
2526        }
2527
2528        false
2529    }
2530
2531    fn ui_update_create_directory_dialog(&mut self, ui: &mut egui::Ui) -> Option<DirectoryEntry> {
2532        self.create_directory_dialog
2533            .update(ui, &self.config)
2534            .directory()
2535            .map(|path| self.process_new_folder(&path))
2536    }
2537
2538    /// Selects every item inside the `directory_content` between `item_a` and `item_b`,
2539    /// excluding both given items.
2540    fn batch_select_between(
2541        &self,
2542        directory_content: &mut DirectoryContent,
2543        item_a: &DirectoryEntry,
2544        item_b: &DirectoryEntry,
2545    ) {
2546        // Get the position of item a and item b
2547        let pos_a = directory_content
2548            .filtered_iter(&self.search_value)
2549            .position(|p| p.path_eq(item_a));
2550        let pos_b = directory_content
2551            .filtered_iter(&self.search_value)
2552            .position(|p| p.path_eq(item_b));
2553
2554        // If both items where found inside the directory entry, mark every item between
2555        // them as selected
2556        if let Some(pos_a) = pos_a {
2557            if let Some(pos_b) = pos_b {
2558                if pos_a == pos_b {
2559                    return;
2560                }
2561
2562                // Get the min and max of both positions.
2563                // We will iterate from min to max.
2564                let mut min = pos_a;
2565                let mut max = pos_b;
2566
2567                if min > max {
2568                    min = pos_b;
2569                    max = pos_a;
2570                }
2571
2572                for item in directory_content
2573                    .filtered_iter_mut(&self.search_value)
2574                    .enumerate()
2575                    .filter(|(i, _)| i > &min && i < &max)
2576                    .map(|(_, p)| p)
2577                {
2578                    item.selected = true;
2579                }
2580            }
2581        }
2582    }
2583
2584    /// Helper function to add a sized button that can be enabled or disabled
2585    fn ui_button_sized(
2586        &self,
2587        ui: &mut egui::Ui,
2588        enabled: bool,
2589        size: egui::Vec2,
2590        label: &str,
2591        err_tooltip: Option<&str>,
2592    ) -> bool {
2593        let mut clicked = false;
2594
2595        ui.add_enabled_ui(enabled, |ui| {
2596            let response = ui.add_sized(size, egui::Button::new(label));
2597            clicked = response.clicked();
2598
2599            if let Some(err) = err_tooltip {
2600                response.on_disabled_hover_ui(|ui| {
2601                    ui.horizontal_wrapped(|ui| {
2602                        ui.spacing_mut().item_spacing.x = 0.0;
2603
2604                        ui.colored_label(
2605                            ui.ctx().style().visuals.error_fg_color,
2606                            format!("{} ", self.config.err_icon),
2607                        );
2608
2609                        ui.label(err);
2610                    });
2611                });
2612            }
2613        });
2614
2615        clicked
2616    }
2617
2618    /// Updates the context menu of a path inside the central panel.
2619    ///
2620    /// # Arguments
2621    ///
2622    /// * `item` - The response of the egui item for which the context menu should be opened.
2623    /// * `path` - The path for which the context menu should be opened.
2624    fn ui_update_central_panel_path_context_menu(&mut self, item: &egui::Response, path: &Path) {
2625        // Path context menus are currently only used for pinned folders.
2626        if !self.config.show_pinned_folders {
2627            return;
2628        }
2629
2630        item.context_menu(|ui| {
2631            let pinned = self.is_pinned(path);
2632
2633            if pinned {
2634                if ui.button(&self.config.labels.unpin_folder).clicked() {
2635                    self.unpin_path(path);
2636                    ui.close();
2637                }
2638            } else if ui.button(&self.config.labels.pin_folder).clicked() {
2639                self.pin_path(path.to_path_buf());
2640                ui.close();
2641            }
2642        });
2643    }
2644
2645    /// Sets the cursor position to the end of a text input field.
2646    ///
2647    /// # Arguments
2648    ///
2649    /// * `re` - response of the text input widget
2650    /// * `data` - buffer holding the text of the input widget
2651    fn set_cursor_to_end(re: &egui::Response, data: &str) {
2652        // Set the cursor to the end of the filter input string
2653        if let Some(mut state) = egui::TextEdit::load_state(&re.ctx, re.id) {
2654            state
2655                .cursor
2656                .set_char_range(Some(CCursorRange::one(CCursor::new(data.len()))));
2657            state.store(&re.ctx, re.id);
2658        }
2659    }
2660
2661    /// Calculates the width of a single char.
2662    fn calc_char_width(ui: &egui::Ui, char: char) -> f32 {
2663        ui.fonts_mut(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char))
2664    }
2665
2666    /// Calculates the width of the specified text using the current font configuration.
2667    /// Does not take new lines or text breaks into account!
2668    fn calc_text_width(ui: &egui::Ui, text: &str) -> f32 {
2669        let mut width = 0.0;
2670
2671        for char in text.chars() {
2672            width += Self::calc_char_width(ui, char);
2673        }
2674
2675        width
2676    }
2677
2678    fn truncate_filename(ui: &egui::Ui, item: &DirectoryEntry, max_length: f32) -> String {
2679        const TRUNCATE_STR: &str = "...";
2680
2681        let path = item.as_path();
2682
2683        let file_stem = if item.is_file() {
2684            path.file_stem().and_then(|f| f.to_str()).unwrap_or("")
2685        } else {
2686            item.file_name()
2687        };
2688
2689        let extension = if item.is_file() {
2690            path.extension().map_or(String::new(), |ext| {
2691                format!(".{}", ext.to_str().unwrap_or(""))
2692            })
2693        } else {
2694            String::new()
2695        };
2696
2697        let extension_width = Self::calc_text_width(ui, &extension);
2698        let reserved = extension_width + Self::calc_text_width(ui, TRUNCATE_STR);
2699
2700        if max_length <= reserved {
2701            return format!("{TRUNCATE_STR}{extension}");
2702        }
2703
2704        let mut width = reserved;
2705        let mut front = String::new();
2706        let mut back = String::new();
2707
2708        for (i, char) in file_stem.chars().enumerate() {
2709            let w = Self::calc_char_width(ui, char);
2710
2711            if width + w > max_length {
2712                break;
2713            }
2714
2715            front.push(char);
2716            width += w;
2717
2718            let back_index = file_stem.len() - i - 1;
2719
2720            if back_index <= i {
2721                break;
2722            }
2723
2724            if let Some(char) = file_stem.chars().nth(back_index) {
2725                let w = Self::calc_char_width(ui, char);
2726
2727                if width + w > max_length {
2728                    break;
2729                }
2730
2731                back.push(char);
2732                width += w;
2733            }
2734        }
2735
2736        format!(
2737            "{front}{TRUNCATE_STR}{}{extension}",
2738            back.chars().rev().collect::<String>()
2739        )
2740    }
2741}
2742
2743/// Keybindings
2744impl FileDialog {
2745    /// Checks whether certain keybindings have been pressed and executes the corresponding actions.
2746    fn update_keybindings(&mut self, ctx: &egui::Context) {
2747        // We don't want to execute keybindings if a modal is currently open.
2748        // The modals implement the keybindings themselves.
2749        if let Some(modal) = self.modals.last_mut() {
2750            modal.update_keybindings(&self.config, ctx);
2751            return;
2752        }
2753
2754        let keybindings = std::mem::take(&mut self.config.keybindings);
2755
2756        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.submit, false) {
2757            self.exec_keybinding_submit();
2758        }
2759
2760        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.cancel, false) {
2761            self.exec_keybinding_cancel();
2762        }
2763
2764        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.parent, true) {
2765            self.load_parent_directory();
2766        }
2767
2768        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.back, true) {
2769            self.load_previous_directory();
2770        }
2771
2772        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.forward, true) {
2773            self.load_next_directory();
2774        }
2775
2776        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.reload, true) {
2777            self.refresh();
2778        }
2779
2780        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.new_folder, true) {
2781            self.open_new_folder_dialog();
2782        }
2783
2784        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.edit_path, true) {
2785            self.open_path_edit();
2786        }
2787
2788        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.home_edit_path, true) {
2789            if let Some(dirs) = &self.user_directories {
2790                if let Some(home) = dirs.home_dir() {
2791                    self.load_directory(home.to_path_buf().as_path());
2792                    self.open_path_edit();
2793                }
2794            }
2795        }
2796
2797        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_up, false) {
2798            self.exec_keybinding_selection_up();
2799
2800            // We want to break out of input fields like search when pressing selection keys
2801            if let Some(id) = ctx.memory(egui::Memory::focused) {
2802                ctx.memory_mut(|w| w.surrender_focus(id));
2803            }
2804        }
2805
2806        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_down, false) {
2807            self.exec_keybinding_selection_down();
2808
2809            // We want to break out of input fields like search when pressing selection keys
2810            if let Some(id) = ctx.memory(egui::Memory::focused) {
2811                ctx.memory_mut(|w| w.surrender_focus(id));
2812            }
2813        }
2814
2815        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.select_all, true)
2816            && self.mode == DialogMode::PickMultiple
2817        {
2818            for item in self.directory_content.filtered_iter_mut(&self.search_value) {
2819                item.selected = true;
2820            }
2821        }
2822
2823        self.config.keybindings = keybindings;
2824    }
2825
2826    /// Executes the action when the keybinding `submit` is pressed.
2827    fn exec_keybinding_submit(&mut self) {
2828        if self.path_edit_visible {
2829            self.submit_path_edit();
2830            return;
2831        }
2832
2833        if self.create_directory_dialog.is_open() {
2834            if let Some(dir) = self.create_directory_dialog.submit().directory() {
2835                self.process_new_folder(&dir);
2836            }
2837            return;
2838        }
2839
2840        if self.any_focused_last_frame {
2841            return;
2842        }
2843
2844        // Check if there is a directory selected we can open
2845        if let Some(item) = &self.selected_item {
2846            // Make sure the selected item is visible inside the directory view.
2847            let is_visible = self
2848                .get_dir_content_filtered_iter()
2849                .any(|p| p.path_eq(item));
2850
2851            if is_visible && item.is_dir() {
2852                self.load_directory(&item.to_path_buf());
2853                return;
2854            }
2855        }
2856
2857        self.submit();
2858    }
2859
2860    /// Executes the action when the keybinding `cancel` is pressed.
2861    fn exec_keybinding_cancel(&mut self) {
2862        // We have to check if the `create_directory_dialog` and `path_edit_visible` is open,
2863        // because egui does not consume pressing the escape key inside a text input.
2864        // So when pressing the escape key inside a text input, the text input is closed
2865        // but the keybindings still register the press on the escape key.
2866        // (Although the keybindings are updated before the UI and they check whether another
2867        //  widget is currently in focus!)
2868        //
2869        // This is practical for us because we can close the path edit and
2870        // the create directory dialog.
2871        // However, this causes problems when the user presses escape in other text
2872        // inputs for which we have no status saved. This would then close the entire file dialog.
2873        // To fix this, we check if any item was focused in the last frame.
2874        //
2875        // Note that this only happens with the escape key and not when the enter key is
2876        // used to close a text input. This is why we don't have to check for the
2877        // dialogs in `exec_keybinding_submit`.
2878
2879        if self.create_directory_dialog.is_open() {
2880            self.create_directory_dialog.close();
2881        } else if self.path_edit_visible {
2882            self.close_path_edit();
2883        } else if !self.any_focused_last_frame {
2884            self.cancel();
2885        }
2886    }
2887
2888    /// Executes the action when the keybinding `selection_up` is pressed.
2889    fn exec_keybinding_selection_up(&mut self) {
2890        if self.directory_content.len() == 0 {
2891            return;
2892        }
2893
2894        self.directory_content.reset_multi_selection();
2895
2896        if let Some(item) = &self.selected_item {
2897            if self.select_next_visible_item_before(&item.clone()) {
2898                return;
2899            }
2900        }
2901
2902        // No item is selected or no more items left.
2903        // Select the last item from the directory content.
2904        self.select_last_visible_item();
2905    }
2906
2907    /// Executes the action when the keybinding `selection_down` is pressed.
2908    fn exec_keybinding_selection_down(&mut self) {
2909        if self.directory_content.len() == 0 {
2910            return;
2911        }
2912
2913        self.directory_content.reset_multi_selection();
2914
2915        if let Some(item) = &self.selected_item {
2916            if self.select_next_visible_item_after(&item.clone()) {
2917                return;
2918            }
2919        }
2920
2921        // No item is selected or no more items left.
2922        // Select the last item from the directory content.
2923        self.select_first_visible_item();
2924    }
2925}
2926
2927/// Implementation
2928impl FileDialog {
2929    /// Get the file filter the user currently selected.
2930    fn get_selected_file_filter(&self) -> Option<&FileFilter> {
2931        self.selected_file_filter
2932            .and_then(|id| self.config.file_filters.iter().find(|p| p.id == id))
2933    }
2934
2935    /// Sets the default file filter to use.
2936    fn set_default_file_filter(&mut self) {
2937        if let Some(name) = &self.config.default_file_filter {
2938            for filter in &self.config.file_filters {
2939                if filter.name == name.as_str() {
2940                    self.selected_file_filter = Some(filter.id);
2941                }
2942            }
2943        }
2944    }
2945
2946    /// Selects the given file filter and applies the appropriate filters.
2947    fn select_file_filter(&mut self, filter: Option<FileFilter>) {
2948        self.selected_file_filter = filter.map(|f| f.id);
2949        self.selected_item = None;
2950        self.refresh();
2951    }
2952
2953    /// Get the save extension the user currently selected.
2954    fn get_selected_save_extension(&self) -> Option<&SaveExtension> {
2955        self.selected_save_extension
2956            .and_then(|id| self.config.save_extensions.iter().find(|p| p.id == id))
2957    }
2958
2959    /// Sets the save extension to use.
2960    fn set_default_save_extension(&mut self) {
2961        let config = std::mem::take(&mut self.config);
2962
2963        if let Some(name) = &config.default_save_extension {
2964            for extension in &config.save_extensions {
2965                if extension.name == name.as_str() {
2966                    self.selected_save_extension = Some(extension.id);
2967                    self.set_file_name_extension(&extension.file_extension);
2968                }
2969            }
2970        }
2971
2972        self.config = config;
2973    }
2974
2975    /// Selects the given save extension.
2976    fn select_save_extension(&mut self, extension: Option<SaveExtension>) {
2977        if let Some(ex) = extension {
2978            self.selected_save_extension = Some(ex.id);
2979            self.set_file_name_extension(&ex.file_extension);
2980        }
2981
2982        self.selected_item = None;
2983        self.refresh();
2984    }
2985
2986    /// Updates the extension of `Self::file_name_input`.
2987    fn set_file_name_extension(&mut self, extension: &str) {
2988        // Prevent `PathBuf::set_extension` to append the file extension when there is
2989        // already one without a file name. For example `.png` would be changed to `.png.txt`
2990        // when using `PathBuf::set_extension`.
2991        let dot_count = self.file_name_input.chars().filter(|c| *c == '.').count();
2992        let use_simple = dot_count == 1 && self.file_name_input.chars().nth(0) == Some('.');
2993
2994        let mut p = PathBuf::from(&self.file_name_input);
2995        if !use_simple && p.set_extension(extension) {
2996            self.file_name_input = p.to_string_lossy().into_owned();
2997        } else {
2998            self.file_name_input = format!(".{extension}");
2999        }
3000    }
3001
3002    /// Gets a filtered iterator of the directory content of this object.
3003    fn get_dir_content_filtered_iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
3004        self.directory_content.filtered_iter(&self.search_value)
3005    }
3006
3007    /// Opens the dialog to create a new folder.
3008    fn open_new_folder_dialog(&mut self) {
3009        if let Some(x) = self.current_directory() {
3010            self.create_directory_dialog.open(x.to_path_buf());
3011        }
3012    }
3013
3014    /// Function that processes a newly created folder.
3015    fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry {
3016        let mut entry =
3017            DirectoryEntry::from_path(&self.config, created_dir, &*self.config.file_system);
3018
3019        self.directory_content.push(entry.clone());
3020
3021        self.select_item(&mut entry);
3022
3023        entry
3024    }
3025
3026    /// Opens a new modal window.
3027    fn open_modal(&mut self, modal: Box<dyn FileDialogModal + Send + Sync>) {
3028        self.modals.push(modal);
3029    }
3030
3031    /// Executes the given modal action.
3032    fn exec_modal_action(&mut self, action: ModalAction) {
3033        match action {
3034            ModalAction::None => {}
3035            ModalAction::SaveFile(path) => self.state = DialogState::Picked(path),
3036        }
3037    }
3038
3039    /// Canonicalizes the specified path if canonicalization is enabled.
3040    /// Returns the input path if an error occurs or canonicalization is disabled.
3041    fn canonicalize_path(&self, path: &Path) -> PathBuf {
3042        if self.config.canonicalize_paths {
3043            dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
3044        } else {
3045            path.to_path_buf()
3046        }
3047    }
3048
3049    /// Pins a path to the left sidebar.
3050    fn pin_path(&mut self, path: PathBuf) {
3051        let pinned = PinnedFolder::from_path(path);
3052        self.storage.pinned_folders.push(pinned);
3053    }
3054
3055    /// Unpins a path from the left sidebar.
3056    fn unpin_path(&mut self, path: &Path) {
3057        self.storage
3058            .pinned_folders
3059            .retain(|p| p.path.as_path() != path);
3060    }
3061
3062    /// Checks if the path is pinned to the left sidebar.
3063    fn is_pinned(&self, path: &Path) -> bool {
3064        self.storage
3065            .pinned_folders
3066            .iter()
3067            .any(|p| p.path.as_path() == path)
3068    }
3069
3070    /// Starts to rename a pinned folder by showing the user a text input field.
3071    fn begin_rename_pinned_folder(&mut self, pinned: PinnedFolder) {
3072        self.rename_pinned_folder = Some(pinned);
3073        self.rename_pinned_folder_request_focus = true;
3074    }
3075
3076    /// Ends the renaming of a pinned folder. This updates the real pinned folder
3077    /// in `FileDialogStorage`.
3078    fn end_rename_pinned_folder(&mut self) {
3079        let renamed = std::mem::take(&mut self.rename_pinned_folder);
3080
3081        if let Some(renamed) = renamed {
3082            let old = self
3083                .storage
3084                .pinned_folders
3085                .iter_mut()
3086                .find(|p| p.path == renamed.path);
3087            if let Some(old) = old {
3088                old.label = renamed.label;
3089            }
3090        }
3091    }
3092
3093    /// Checks if the given pinned folder is currently being renamed.
3094    fn is_pinned_folder_being_renamed(&self, pinned: &PinnedFolder) -> bool {
3095        self.rename_pinned_folder
3096            .as_ref()
3097            .is_some_and(|p| p.path == pinned.path)
3098    }
3099
3100    fn is_primary_selected(&self, item: &DirectoryEntry) -> bool {
3101        self.selected_item.as_ref().is_some_and(|x| x.path_eq(item))
3102    }
3103
3104    /// Resets the dialog to use default values.
3105    /// The user data and configuration variables are retained.
3106    fn reset(&mut self) {
3107        let user_data = std::mem::take(&mut self.user_data);
3108        let storage = self.storage.clone();
3109        let config = self.config.clone();
3110
3111        *self = Self::with_config(config);
3112        self.storage = storage;
3113        self.user_data = user_data;
3114    }
3115
3116    /// Refreshes the dialog.
3117    /// Including the user directories, system disks and currently open directory.
3118    fn refresh(&mut self) {
3119        self.user_directories = self
3120            .config
3121            .file_system
3122            .user_dirs(self.config.canonicalize_paths);
3123        self.system_disks = self
3124            .config
3125            .file_system
3126            .get_disks(self.config.canonicalize_paths);
3127
3128        self.reload_directory();
3129    }
3130
3131    /// Submits the current selection and tries to finish the dialog, if the selection is valid.
3132    fn submit(&mut self) {
3133        // Make sure the selected item or entered file name is valid.
3134        if !self.is_selection_valid() {
3135            return;
3136        }
3137
3138        self.storage.last_picked_dir = self.current_directory().map(PathBuf::from);
3139
3140        match &self.mode {
3141            DialogMode::PickDirectory | DialogMode::PickFile => {
3142                // Should always contain a value since `is_selection_valid` is used to
3143                // validate the selection.
3144                if let Some(item) = self.selected_item.clone() {
3145                    self.state = DialogState::Picked(item.to_path_buf());
3146                }
3147            }
3148            DialogMode::PickMultiple => {
3149                let result: Vec<PathBuf> = self
3150                    .selected_entries()
3151                    .map(crate::DirectoryEntry::to_path_buf)
3152                    .collect();
3153
3154                self.state = DialogState::PickedMultiple(result);
3155            }
3156            DialogMode::SaveFile => {
3157                // Should always contain a value since `is_selection_valid` is used to
3158                // validate the selection.
3159                if let Some(path) = self.current_directory() {
3160                    let full_path = path.join(&self.file_name_input);
3161                    self.submit_save_file(full_path);
3162                }
3163            }
3164        }
3165    }
3166
3167    /// Submits the file dialog with the specified path and opens the `OverwriteFileModal`
3168    /// if the path already exists.
3169    fn submit_save_file(&mut self, path: PathBuf) {
3170        if path.exists() {
3171            self.open_modal(Box::new(OverwriteFileModal::new(path)));
3172
3173            return;
3174        }
3175
3176        self.state = DialogState::Picked(path);
3177    }
3178
3179    /// Cancels the dialog.
3180    fn cancel(&mut self) {
3181        self.state = DialogState::Cancelled;
3182    }
3183
3184    /// This function generates the initial directory based on the configuration.
3185    /// The function does the following things:
3186    ///   - Get the path to open based on the opening mode
3187    ///   - Canonicalize the path if enabled
3188    ///   - Attempts to use the parent directory if the path is a file
3189    fn get_initial_directory(&self) -> PathBuf {
3190        let path = match self.config.opening_mode {
3191            OpeningMode::AlwaysInitialDir => &self.config.initial_directory,
3192            OpeningMode::LastVisitedDir => self
3193                .storage
3194                .last_visited_dir
3195                .as_deref()
3196                .unwrap_or(&self.config.initial_directory),
3197            OpeningMode::LastPickedDir => self
3198                .storage
3199                .last_picked_dir
3200                .as_deref()
3201                .unwrap_or(&self.config.initial_directory),
3202        };
3203
3204        let mut path = self.canonicalize_path(path);
3205
3206        if self.config.file_system.is_file(&path) {
3207            if let Some(parent) = path.parent() {
3208                path = parent.to_path_buf();
3209            }
3210        }
3211
3212        path
3213    }
3214
3215    /// Gets the currently open directory.
3216    fn current_directory(&self) -> Option<&Path> {
3217        if let Some(x) = self.directory_stack.iter().nth_back(self.directory_offset) {
3218            return Some(x.as_path());
3219        }
3220
3221        None
3222    }
3223
3224    /// Checks whether the selection or the file name entered is valid.
3225    /// What is checked depends on the mode the dialog is currently in.
3226    fn is_selection_valid(&self) -> bool {
3227        match &self.mode {
3228            DialogMode::PickDirectory => self
3229                .selected_item
3230                .as_ref()
3231                .is_some_and(crate::DirectoryEntry::is_dir),
3232            DialogMode::PickFile => self
3233                .selected_item
3234                .as_ref()
3235                .is_some_and(DirectoryEntry::is_file),
3236            DialogMode::PickMultiple => self.get_dir_content_filtered_iter().any(|p| p.selected),
3237            DialogMode::SaveFile => self.file_name_input_error.is_none(),
3238        }
3239    }
3240
3241    /// Validates the file name entered by the user.
3242    ///
3243    /// Returns None if the file name is valid. Otherwise returns an error message.
3244    fn validate_file_name_input(&self) -> Option<String> {
3245        if self.file_name_input.is_empty() {
3246            return Some(self.config.labels.err_empty_file_name.clone());
3247        }
3248
3249        if let Some(x) = self.current_directory() {
3250            let mut full_path = x.to_path_buf();
3251            full_path.push(self.file_name_input.as_str());
3252
3253            if self.config.file_system.is_dir(&full_path) {
3254                return Some(self.config.labels.err_directory_exists.clone());
3255            }
3256
3257            if !self.config.allow_file_overwrite && self.config.file_system.is_file(&full_path) {
3258                return Some(self.config.labels.err_file_exists.clone());
3259            }
3260        } else {
3261            // There is most likely a bug in the code if we get this error message!
3262            return Some("Currently not in a directory".to_string());
3263        }
3264
3265        None
3266    }
3267
3268    /// Marks the given item as the selected directory item.
3269    /// Also updates the `file_name_input` to the name of the selected item.
3270    fn select_item(&mut self, item: &mut DirectoryEntry) {
3271        if self.mode == DialogMode::PickMultiple {
3272            item.selected = true;
3273        }
3274        self.selected_item = Some(item.clone());
3275
3276        if self.mode == DialogMode::SaveFile && item.is_file() {
3277            self.file_name_input = item.file_name().to_string();
3278            self.file_name_input_error = self.validate_file_name_input();
3279        }
3280    }
3281
3282    /// Attempts to select the last visible item in `directory_content` before the specified item.
3283    ///
3284    /// Returns true if an item is found and selected.
3285    /// Returns false if no visible item is found before the specified item.
3286    fn select_next_visible_item_before(&mut self, item: &DirectoryEntry) -> bool {
3287        let mut return_val = false;
3288
3289        self.directory_content.reset_multi_selection();
3290
3291        let mut directory_content = std::mem::take(&mut self.directory_content);
3292        let search_value = std::mem::take(&mut self.search_value);
3293
3294        let index = directory_content
3295            .filtered_iter(&search_value)
3296            .position(|p| p.path_eq(item));
3297
3298        if let Some(index) = index {
3299            if index != 0 {
3300                if let Some(item) = directory_content
3301                    .filtered_iter_mut(&search_value)
3302                    .nth(index.saturating_sub(1))
3303                {
3304                    self.select_item(item);
3305                    self.scroll_to_selection = true;
3306                    return_val = true;
3307                }
3308            }
3309        }
3310
3311        self.directory_content = directory_content;
3312        self.search_value = search_value;
3313
3314        return_val
3315    }
3316
3317    /// Attempts to select the last visible item in `directory_content` after the specified item.
3318    ///
3319    /// Returns true if an item is found and selected.
3320    /// Returns false if no visible item is found after the specified item.
3321    fn select_next_visible_item_after(&mut self, item: &DirectoryEntry) -> bool {
3322        let mut return_val = false;
3323
3324        self.directory_content.reset_multi_selection();
3325
3326        let mut directory_content = std::mem::take(&mut self.directory_content);
3327        let search_value = std::mem::take(&mut self.search_value);
3328
3329        let index = directory_content
3330            .filtered_iter(&search_value)
3331            .position(|p| p.path_eq(item));
3332
3333        if let Some(index) = index {
3334            if let Some(item) = directory_content
3335                .filtered_iter_mut(&search_value)
3336                .nth(index.saturating_add(1))
3337            {
3338                self.select_item(item);
3339                self.scroll_to_selection = true;
3340                return_val = true;
3341            }
3342        }
3343
3344        self.directory_content = directory_content;
3345        self.search_value = search_value;
3346
3347        return_val
3348    }
3349
3350    /// Tries to select the first visible item inside `directory_content`.
3351    fn select_first_visible_item(&mut self) {
3352        self.directory_content.reset_multi_selection();
3353
3354        let mut directory_content = std::mem::take(&mut self.directory_content);
3355
3356        if let Some(item) = directory_content
3357            .filtered_iter_mut(&self.search_value.clone())
3358            .next()
3359        {
3360            self.select_item(item);
3361            self.scroll_to_selection = true;
3362        }
3363
3364        self.directory_content = directory_content;
3365    }
3366
3367    /// Tries to select the last visible item inside `directory_content`.
3368    fn select_last_visible_item(&mut self) {
3369        self.directory_content.reset_multi_selection();
3370
3371        let mut directory_content = std::mem::take(&mut self.directory_content);
3372
3373        if let Some(item) = directory_content
3374            .filtered_iter_mut(&self.search_value.clone())
3375            .last()
3376        {
3377            self.select_item(item);
3378            self.scroll_to_selection = true;
3379        }
3380
3381        self.directory_content = directory_content;
3382    }
3383
3384    /// Opens the text field in the top panel to text edit the current path.
3385    fn open_path_edit(&mut self) {
3386        let path = self.current_directory().map_or_else(String::new, |path| {
3387            path.to_str().unwrap_or_default().to_string()
3388        });
3389
3390        self.path_edit_value = path;
3391        self.path_edit_activate = true;
3392        self.path_edit_visible = true;
3393    }
3394
3395    /// Loads the directory from the path text edit.
3396    fn submit_path_edit(&mut self) {
3397        self.close_path_edit();
3398
3399        let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value));
3400
3401        if self.mode == DialogMode::PickFile && self.config.file_system.is_file(&path) {
3402            self.state = DialogState::Picked(path);
3403            return;
3404        }
3405
3406        // Assume the user wants to save the given path when
3407        //   - an extension to the file name is given or the path
3408        //     edit is allowed to save a file without extension,
3409        //   - the path is not an existing directory,
3410        //   - and the parent directory exists
3411        // Otherwise we will assume the user wants to open the path as a directory.
3412        if self.mode == DialogMode::SaveFile
3413            && (path.extension().is_some()
3414                || self.config.allow_path_edit_to_save_file_without_extension)
3415            && !self.config.file_system.is_dir(&path)
3416            && path.parent().is_some_and(std::path::Path::exists)
3417        {
3418            self.submit_save_file(path);
3419            return;
3420        }
3421
3422        self.load_directory(&path);
3423    }
3424
3425    /// Closes the text field at the top to edit the current path without loading
3426    /// the entered directory.
3427    const fn close_path_edit(&mut self) {
3428        self.path_edit_visible = false;
3429    }
3430
3431    /// Loads the next directory in the `directory_stack`.
3432    /// If `directory_offset` is 0 and there is no other directory to load, `Ok()` is returned and
3433    /// nothing changes.
3434    /// Otherwise, the result of the directory loading operation is returned.
3435    fn load_next_directory(&mut self) {
3436        if self.directory_offset == 0 {
3437            // There is no next directory that can be loaded
3438            return;
3439        }
3440
3441        self.directory_offset -= 1;
3442
3443        // Copy path and load directory
3444        if let Some(path) = self.current_directory() {
3445            self.load_directory_content(path.to_path_buf().as_path());
3446        }
3447    }
3448
3449    /// Loads the previous directory the user opened.
3450    /// If there is no previous directory left, `Ok()` is returned and nothing changes.
3451    /// Otherwise, the result of the directory loading operation is returned.
3452    fn load_previous_directory(&mut self) {
3453        if self.directory_offset + 1 >= self.directory_stack.len() {
3454            // There is no previous directory that can be loaded
3455            return;
3456        }
3457
3458        self.directory_offset += 1;
3459
3460        // Copy path and load directory
3461        if let Some(path) = self.current_directory() {
3462            self.load_directory_content(path.to_path_buf().as_path());
3463        }
3464    }
3465
3466    /// Loads the parent directory of the currently open directory.
3467    /// If the directory doesn't have a parent, `Ok()` is returned and nothing changes.
3468    /// Otherwise, the result of the directory loading operation is returned.
3469    fn load_parent_directory(&mut self) {
3470        if let Some(x) = self.current_directory() {
3471            if let Some(x) = x.to_path_buf().parent() {
3472                self.load_directory(x);
3473            }
3474        }
3475    }
3476
3477    /// Reloads the currently open directory.
3478    /// If no directory is currently open, `Ok()` will be returned.
3479    /// Otherwise, the result of the directory loading operation is returned.
3480    ///
3481    /// In most cases, this function should not be called directly.
3482    /// Instead, `refresh` should be used to reload all other data like system disks too.
3483    fn reload_directory(&mut self) {
3484        if let Some(x) = self.current_directory() {
3485            self.load_directory_content(x.to_path_buf().as_path());
3486        }
3487    }
3488
3489    /// Loads the given directory and updates the `directory_stack`.
3490    /// The function deletes all directories from the `directory_stack` that are currently
3491    /// stored in the vector before the `directory_offset`.
3492    ///
3493    /// The function also sets the loaded directory as the selected item.
3494    fn load_directory(&mut self, path: &Path) {
3495        // Do not load the same directory again.
3496        // Use reload_directory if the content of the directory should be updated.
3497        if let Some(x) = self.current_directory() {
3498            if x == path {
3499                return;
3500            }
3501        }
3502
3503        if self.directory_offset != 0 && self.directory_stack.len() > self.directory_offset {
3504            self.directory_stack
3505                .drain(self.directory_stack.len() - self.directory_offset..);
3506        }
3507
3508        self.directory_stack.push(path.to_path_buf());
3509        self.directory_offset = 0;
3510
3511        self.load_directory_content(path);
3512
3513        // Clear the entry filter buffer.
3514        // It's unlikely the user wants to keep the current filter when entering a new directory.
3515        self.search_value.clear();
3516    }
3517
3518    /// Loads the directory content of the given path.
3519    fn load_directory_content(&mut self, path: &Path) {
3520        self.storage.last_visited_dir = Some(path.to_path_buf());
3521
3522        let selected_file_filter = match self.mode {
3523            DialogMode::PickFile | DialogMode::PickMultiple => self.get_selected_file_filter(),
3524            _ => None,
3525        };
3526
3527        let selected_save_extension = if self.mode == DialogMode::SaveFile {
3528            self.get_selected_save_extension()
3529                .map(|e| e.file_extension.as_str())
3530        } else {
3531            None
3532        };
3533
3534        let filter = DirectoryFilter {
3535            show_files: self.show_files,
3536            show_hidden: self.storage.show_hidden,
3537            show_system_files: self.storage.show_system_files,
3538            file_filter: selected_file_filter.cloned(),
3539            filter_extension: selected_save_extension.map(str::to_string),
3540        };
3541
3542        self.directory_content = DirectoryContent::from_path(
3543            &self.config,
3544            path,
3545            self.config.file_system.clone(),
3546            filter,
3547        );
3548
3549        self.create_directory_dialog.close();
3550        self.scroll_to_selection = true;
3551
3552        if self.mode == DialogMode::SaveFile {
3553            self.file_name_input_error = self.validate_file_name_input();
3554        }
3555    }
3556}