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        self.user_data.as_ref().and_then(|u| u.downcast_ref())
1116    }
1117
1118    /// Returns a mutable reference to the currently stored user data.
1119    ///
1120    /// See [`FileDialog::set_user_data`].
1121    pub fn user_data_mut<U: Any>(&mut self) -> Option<&mut U> {
1122        self.user_data.as_mut().and_then(|u| u.downcast_mut())
1123    }
1124
1125    /// Stores custom user data inside this file dialog.
1126    ///
1127    /// This user data can be used for example to track what purpose you have opened the dialog for.
1128    ///
1129    /// For example, You might have an action for opening a document,
1130    /// and also an action for loading a configuration file.
1131    ///
1132    /// ```
1133    /// enum Action {
1134    ///     OpenDocument,
1135    ///     LoadConfig,
1136    /// }
1137    /// let mut dialog = egui_file_dialog::FileDialog::new();
1138    /// // ...
1139    /// // When the user presses "Open document" button
1140    /// dialog.set_user_data(Action::OpenDocument);
1141    /// // ... later, you check what action to perform
1142    /// match dialog.user_data::<Action>() {
1143    ///     Some(Action::OpenDocument) => { /* Open the document */ },
1144    ///     Some(Action::LoadConfig) => { /* Load the config file */},
1145    ///     None => { /* Do nothing */}
1146    /// }
1147    /// ```
1148    pub fn set_user_data<U: Any + Send + Sync>(&mut self, user_data: U) {
1149        self.user_data = Some(Box::new(user_data));
1150    }
1151
1152    /// Returns the mode the dialog is currently in.
1153    pub const fn mode(&self) -> DialogMode {
1154        self.mode
1155    }
1156
1157    /// Returns the state the dialog is currently in.
1158    pub fn state(&self) -> DialogState {
1159        self.state.clone()
1160    }
1161
1162    /// Get the window Id
1163    pub const fn get_window_id(&self) -> egui::Id {
1164        self.window_id
1165    }
1166}
1167
1168/// UI methods
1169impl FileDialog {
1170    /// Main update method of the UI
1171    ///
1172    /// Takes an optional callback to show a custom right panel.
1173    fn update_ui(
1174        &mut self,
1175        ctx: &egui::Context,
1176        right_panel_fn: Option<&mut FileDialogUiCallback>,
1177    ) {
1178        let mut is_open = true;
1179
1180        if self.config.as_modal {
1181            let re = self.ui_update_modal_background(ctx);
1182            ctx.move_to_top(re.response.layer_id);
1183        }
1184
1185        let re = self.create_window(&mut is_open).show(ctx, |ui| {
1186            if !self.modals.is_empty() {
1187                self.ui_update_modals(ui);
1188                return;
1189            }
1190
1191            if self.config.show_top_panel {
1192                egui::TopBottomPanel::top(self.window_id.with("top_panel"))
1193                    .resizable(false)
1194                    .show_inside(ui, |ui| {
1195                        self.ui_update_top_panel(ui);
1196                    });
1197            }
1198
1199            if self.config.show_left_panel {
1200                egui::SidePanel::left(self.window_id.with("left_panel"))
1201                    .resizable(true)
1202                    .default_width(150.0)
1203                    .width_range(90.0..=250.0)
1204                    .show_inside(ui, |ui| {
1205                        self.ui_update_left_panel(ui);
1206                    });
1207            }
1208
1209            // Optionally, show a custom right panel (see `update_with_custom_right_panel`)
1210            if let Some(f) = right_panel_fn {
1211                let mut right_panel = egui::SidePanel::right(self.window_id.with("right_panel"))
1212                    // Unlike the left panel, we have no control over the contents, so
1213                    // we don't restrict the width. It's up to the user to make the UI presentable.
1214                    .resizable(true);
1215                if let Some(width) = self.config.right_panel_width {
1216                    right_panel = right_panel.default_width(width);
1217                }
1218                right_panel.show_inside(ui, |ui| {
1219                    f(ui, self);
1220                });
1221            }
1222
1223            egui::TopBottomPanel::bottom(self.window_id.with("bottom_panel"))
1224                .resizable(false)
1225                .show_inside(ui, |ui| {
1226                    self.ui_update_bottom_panel(ui);
1227                });
1228
1229            egui::CentralPanel::default().show_inside(ui, |ui| {
1230                self.ui_update_central_panel(ui);
1231            });
1232        });
1233
1234        if self.config.as_modal {
1235            if let Some(inner_response) = re {
1236                ctx.move_to_top(inner_response.response.layer_id);
1237            }
1238        }
1239
1240        self.any_focused_last_frame = ctx.memory(egui::Memory::focused).is_some();
1241
1242        // User closed the window without finishing the dialog
1243        if !is_open {
1244            self.cancel();
1245        }
1246
1247        let mut repaint = false;
1248
1249        // Collect dropped files:
1250        ctx.input(|i| {
1251            // Check if files were dropped
1252            if let Some(dropped_file) = i.raw.dropped_files.last() {
1253                if let Some(path) = &dropped_file.path {
1254                    if self.config.file_system.is_dir(path) {
1255                        // If we dropped a directory, go there
1256                        self.load_directory(path.as_path());
1257                        repaint = true;
1258                    } else if let Some(parent) = path.parent() {
1259                        // Else, go to the parent directory
1260                        self.load_directory(parent);
1261                        self.select_item(&mut DirectoryEntry::from_path(
1262                            &self.config,
1263                            path,
1264                            &*self.config.file_system,
1265                        ));
1266                        self.scroll_to_selection = true;
1267                        repaint = true;
1268                    }
1269                }
1270            }
1271        });
1272
1273        // Update GUI if we dropped a file
1274        if repaint {
1275            ctx.request_repaint();
1276        }
1277    }
1278
1279    /// Updates the main modal background of the file dialog window.
1280    fn ui_update_modal_background(&self, ctx: &egui::Context) -> egui::InnerResponse<()> {
1281        egui::Area::new(self.window_id.with("modal_overlay"))
1282            .interactable(true)
1283            .fixed_pos(egui::Pos2::ZERO)
1284            .show(ctx, |ui| {
1285                let screen_rect = ctx.input(|i| i.screen_rect);
1286
1287                ui.allocate_response(screen_rect.size(), egui::Sense::click());
1288
1289                ui.painter().rect_filled(
1290                    screen_rect,
1291                    egui::CornerRadius::ZERO,
1292                    self.config.modal_overlay_color,
1293                );
1294            })
1295    }
1296
1297    fn ui_update_modals(&mut self, ui: &mut egui::Ui) {
1298        // Currently, a rendering error occurs when only a single central panel is rendered
1299        // inside a window. Therefore, when rendering a modal, we render an invisible bottom panel,
1300        // which prevents the error.
1301        // This is currently a bit hacky and should be adjusted again in the future.
1302        egui::TopBottomPanel::bottom(self.window_id.with("modal_bottom_panel"))
1303            .resizable(false)
1304            .show_separator_line(false)
1305            .show_inside(ui, |_| {});
1306
1307        // We need to use a central panel for the modals so that the
1308        // window doesn't resize to the size of the modal.
1309        egui::CentralPanel::default().show_inside(ui, |ui| {
1310            if let Some(modal) = self.modals.last_mut() {
1311                #[allow(clippy::single_match)]
1312                match modal.update(&self.config, ui) {
1313                    ModalState::Close(action) => {
1314                        self.exec_modal_action(action);
1315                        self.modals.pop();
1316                    }
1317                    ModalState::Pending => {}
1318                }
1319            }
1320        });
1321    }
1322
1323    /// Creates a new egui window with the configured options.
1324    fn create_window<'a>(&self, is_open: &'a mut bool) -> egui::Window<'a> {
1325        let mut window = egui::Window::new(self.get_window_title())
1326            .id(self.window_id)
1327            .open(is_open)
1328            .default_size(self.config.default_size)
1329            .min_size(self.config.min_size)
1330            .resizable(self.config.resizable)
1331            .movable(self.config.movable)
1332            .title_bar(self.config.title_bar)
1333            .collapsible(false);
1334
1335        if let Some(pos) = self.config.default_pos {
1336            window = window.default_pos(pos);
1337        }
1338
1339        if let Some(pos) = self.config.fixed_pos {
1340            window = window.fixed_pos(pos);
1341        }
1342
1343        if let Some((anchor, offset)) = self.config.anchor {
1344            window = window.anchor(anchor, offset);
1345        }
1346
1347        if let Some(size) = self.config.max_size {
1348            window = window.max_size(size);
1349        }
1350
1351        window
1352    }
1353
1354    /// Gets the window title to use.
1355    /// This is either one of the default window titles or the configured window title.
1356    const fn get_window_title(&self) -> &String {
1357        match &self.config.title {
1358            Some(title) => title,
1359            None => match &self.mode {
1360                DialogMode::PickDirectory => &self.config.labels.title_select_directory,
1361                DialogMode::PickFile => &self.config.labels.title_select_file,
1362                DialogMode::PickMultiple => &self.config.labels.title_select_multiple,
1363                DialogMode::SaveFile => &self.config.labels.title_save_file,
1364            },
1365        }
1366    }
1367
1368    /// Updates the top panel of the dialog. Including the navigation buttons,
1369    /// the current path display, the reload button and the search field.
1370    fn ui_update_top_panel(&mut self, ui: &mut egui::Ui) {
1371        const BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(25.0, 25.0);
1372
1373        ui.horizontal(|ui| {
1374            self.ui_update_nav_buttons(ui, BUTTON_SIZE);
1375
1376            let mut path_display_width = ui.available_width();
1377
1378            // Leave some area for the menu button and search input
1379            if self.config.show_reload_button {
1380                path_display_width -= ui
1381                    .style()
1382                    .spacing
1383                    .item_spacing
1384                    .x
1385                    .mul_add(2.5, BUTTON_SIZE.x);
1386            }
1387
1388            if self.config.show_search {
1389                path_display_width -= 140.0;
1390            }
1391
1392            if self.config.show_current_path {
1393                self.ui_update_current_path(ui, path_display_width);
1394            }
1395
1396            // Hamburger menu containing different options
1397            if self.config.show_menu_button
1398                && (self.config.show_reload_button
1399                    || self.config.show_working_directory_button
1400                    || self.config.show_hidden_option
1401                    || self.config.show_system_files_option)
1402            {
1403                ui.allocate_ui_with_layout(
1404                    BUTTON_SIZE,
1405                    egui::Layout::centered_and_justified(egui::Direction::LeftToRight),
1406                    |ui| {
1407                        ui.menu_button("☰", |ui| {
1408                            self.ui_update_hamburger_menu(ui);
1409                        });
1410                    },
1411                );
1412            }
1413
1414            if self.config.show_search {
1415                self.ui_update_search(ui);
1416            }
1417        });
1418
1419        ui.add_space(ui.ctx().style().spacing.item_spacing.y);
1420    }
1421
1422    /// Updates the navigation buttons like parent or previous directory
1423    fn ui_update_nav_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
1424        if self.config.show_parent_button {
1425            if let Some(x) = self.current_directory() {
1426                if self.ui_button_sized(ui, x.parent().is_some(), button_size, "⏶", None) {
1427                    self.load_parent_directory();
1428                }
1429            } else {
1430                let _ = self.ui_button_sized(ui, false, button_size, "⏶", None);
1431            }
1432        }
1433
1434        if self.config.show_back_button
1435            && self.ui_button_sized(
1436                ui,
1437                self.directory_offset + 1 < self.directory_stack.len(),
1438                button_size,
1439                "⏴",
1440                None,
1441            )
1442        {
1443            self.load_previous_directory();
1444        }
1445
1446        if self.config.show_forward_button
1447            && self.ui_button_sized(ui, self.directory_offset != 0, button_size, "⏵", None)
1448        {
1449            self.load_next_directory();
1450        }
1451
1452        if self.config.show_new_folder_button
1453            && self.ui_button_sized(
1454                ui,
1455                !self.create_directory_dialog.is_open(),
1456                button_size,
1457                "+",
1458                None,
1459            )
1460        {
1461            self.open_new_folder_dialog();
1462        }
1463    }
1464
1465    /// Updates the view to display the current path.
1466    /// This could be the view for displaying the current path and the individual sections,
1467    /// as well as the view for text editing of the current path.
1468    fn ui_update_current_path(&mut self, ui: &mut egui::Ui, width: f32) {
1469        egui::Frame::default()
1470            .stroke(egui::Stroke::new(
1471                1.0,
1472                ui.ctx().style().visuals.window_stroke.color,
1473            ))
1474            .inner_margin(egui::Margin::from(4))
1475            .corner_radius(egui::CornerRadius::from(4))
1476            .show(ui, |ui| {
1477                const EDIT_BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(22.0, 20.0);
1478
1479                if self.path_edit_visible {
1480                    self.ui_update_path_edit(ui, width, EDIT_BUTTON_SIZE);
1481                } else {
1482                    self.ui_update_path_display(ui, width, EDIT_BUTTON_SIZE);
1483                }
1484            });
1485    }
1486
1487    /// Updates the view when the currently open path with the individual sections is displayed.
1488    fn ui_update_path_display(
1489        &mut self,
1490        ui: &mut egui::Ui,
1491        width: f32,
1492        edit_button_size: egui::Vec2,
1493    ) {
1494        ui.style_mut().always_scroll_the_only_direction = true;
1495        ui.style_mut().spacing.scroll.bar_width = 8.0;
1496
1497        let max_width = if self.config.show_path_edit_button {
1498            ui.style()
1499                .spacing
1500                .item_spacing
1501                .x
1502                .mul_add(-2.0, width - edit_button_size.x)
1503        } else {
1504            width
1505        };
1506
1507        egui::ScrollArea::horizontal()
1508            .auto_shrink([false, false])
1509            .stick_to_right(true)
1510            .max_width(max_width)
1511            .show(ui, |ui| {
1512                ui.horizontal(|ui| {
1513                    ui.style_mut().spacing.item_spacing.x /= 2.5;
1514                    ui.style_mut().spacing.button_padding = egui::Vec2::new(5.0, 3.0);
1515
1516                    let mut path = PathBuf::new();
1517
1518                    if let Some(data) = self.current_directory().map(Path::to_path_buf) {
1519                        for (i, segment) in data.iter().enumerate() {
1520                            path.push(segment);
1521
1522                            let mut segment_str = segment.to_str().unwrap_or_default().to_string();
1523
1524                            if self.is_pinned(&path) {
1525                                segment_str =
1526                                    format!("{} {}", &self.config.pinned_icon, segment_str);
1527                            }
1528
1529                            if i != 0 {
1530                                ui.label(self.config.directory_separator.as_str());
1531                            }
1532
1533                            let re = ui.button(segment_str);
1534
1535                            if re.clicked() {
1536                                self.load_directory(path.as_path());
1537                                return;
1538                            }
1539
1540                            self.ui_update_central_panel_path_context_menu(&re, &path.clone());
1541                        }
1542                    }
1543                });
1544            });
1545
1546        if !self.config.show_path_edit_button {
1547            return;
1548        }
1549
1550        if ui
1551            .add_sized(
1552                edit_button_size,
1553                egui::Button::new("🖊").fill(egui::Color32::TRANSPARENT),
1554            )
1555            .clicked()
1556        {
1557            self.open_path_edit();
1558        }
1559    }
1560
1561    /// Updates the view when the user currently wants to text edit the current path.
1562    fn ui_update_path_edit(&mut self, ui: &mut egui::Ui, width: f32, edit_button_size: egui::Vec2) {
1563        let desired_width: f32 = ui
1564            .style()
1565            .spacing
1566            .item_spacing
1567            .x
1568            .mul_add(-3.0, width - edit_button_size.x);
1569
1570        let response = egui::TextEdit::singleline(&mut self.path_edit_value)
1571            .desired_width(desired_width)
1572            .show(ui)
1573            .response;
1574
1575        if self.path_edit_activate {
1576            response.request_focus();
1577            Self::set_cursor_to_end(&response, &self.path_edit_value);
1578            self.path_edit_activate = false;
1579        }
1580
1581        if self.path_edit_request_focus {
1582            response.request_focus();
1583            self.path_edit_request_focus = false;
1584        }
1585
1586        let btn_response = ui.add_sized(edit_button_size, egui::Button::new("✔"));
1587
1588        if btn_response.clicked() {
1589            self.submit_path_edit();
1590        }
1591
1592        if !response.has_focus() && !btn_response.contains_pointer() {
1593            self.path_edit_visible = false;
1594        }
1595    }
1596
1597    /// Updates the hamburger menu containing different options.
1598    fn ui_update_hamburger_menu(&mut self, ui: &mut egui::Ui) {
1599        const SEPARATOR_SPACING: f32 = 2.0;
1600
1601        if self.config.show_reload_button && ui.button(&self.config.labels.reload).clicked() {
1602            self.refresh();
1603            ui.close();
1604        }
1605
1606        let working_dir = self.config.file_system.current_dir();
1607
1608        if self.config.show_working_directory_button
1609            && working_dir.is_ok()
1610            && ui.button(&self.config.labels.working_directory).clicked()
1611        {
1612            self.load_directory(&working_dir.unwrap_or_default());
1613            ui.close();
1614        }
1615
1616        if (self.config.show_reload_button || self.config.show_working_directory_button)
1617            && (self.config.show_hidden_option || self.config.show_system_files_option)
1618        {
1619            ui.add_space(SEPARATOR_SPACING);
1620            ui.separator();
1621            ui.add_space(SEPARATOR_SPACING);
1622        }
1623
1624        if self.config.show_hidden_option
1625            && ui
1626                .checkbox(
1627                    &mut self.storage.show_hidden,
1628                    &self.config.labels.show_hidden,
1629                )
1630                .clicked()
1631        {
1632            self.refresh();
1633            ui.close();
1634        }
1635
1636        if self.config.show_system_files_option
1637            && ui
1638                .checkbox(
1639                    &mut self.storage.show_system_files,
1640                    &self.config.labels.show_system_files,
1641                )
1642                .clicked()
1643        {
1644            self.refresh();
1645            ui.close();
1646        }
1647    }
1648
1649    /// Updates the search input
1650    fn ui_update_search(&mut self, ui: &mut egui::Ui) {
1651        egui::Frame::default()
1652            .stroke(egui::Stroke::new(
1653                1.0,
1654                ui.ctx().style().visuals.window_stroke.color,
1655            ))
1656            .inner_margin(egui::Margin::symmetric(4, 4))
1657            .corner_radius(egui::CornerRadius::from(4))
1658            .show(ui, |ui| {
1659                ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1660                    ui.add_space(ui.ctx().style().spacing.item_spacing.y);
1661
1662                    ui.label(egui::RichText::from("🔍").size(15.0));
1663
1664                    let re = ui.add_sized(
1665                        egui::Vec2::new(ui.available_width(), 0.0),
1666                        egui::TextEdit::singleline(&mut self.search_value),
1667                    );
1668
1669                    self.edit_search_on_text_input(ui);
1670
1671                    if re.changed() || self.init_search {
1672                        self.selected_item = None;
1673                        self.select_first_visible_item();
1674                    }
1675
1676                    if self.init_search {
1677                        re.request_focus();
1678                        Self::set_cursor_to_end(&re, &self.search_value);
1679                        self.directory_content.reset_multi_selection();
1680
1681                        self.init_search = false;
1682                    }
1683                });
1684            });
1685    }
1686
1687    /// Focuses and types into the search input, if text input without
1688    /// shortcut modifiers is detected, and no other inputs are focused.
1689    ///
1690    /// # Arguments
1691    ///
1692    /// - `re`: The [`egui::Response`] returned by the filter text edit widget
1693    fn edit_search_on_text_input(&mut self, ui: &egui::Ui) {
1694        if ui.memory(|mem| mem.focused().is_some()) {
1695            return;
1696        }
1697
1698        ui.input(|inp| {
1699            // We stop if any modifier is active besides only shift
1700            if inp.modifiers.any() && !inp.modifiers.shift_only() {
1701                return;
1702            }
1703
1704            // If we find any text input event, we append it to the filter string
1705            // and allow proceeding to activating the filter input widget.
1706            for text in inp.events.iter().filter_map(|ev| match ev {
1707                egui::Event::Text(t) => Some(t),
1708                _ => None,
1709            }) {
1710                self.search_value.push_str(text);
1711                self.init_search = true;
1712            }
1713        });
1714    }
1715
1716    /// Updates the left panel of the dialog. Including the list of the user directories (Places)
1717    /// and system disks (Devices, Removable Devices).
1718    fn ui_update_left_panel(&mut self, ui: &mut egui::Ui) {
1719        ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
1720            // Spacing multiplier used between sections in the left sidebar
1721            const SPACING_MULTIPLIER: f32 = 4.0;
1722
1723            egui::containers::ScrollArea::vertical()
1724                .auto_shrink([false, false])
1725                .show(ui, |ui| {
1726                    // Spacing for the first section in the left sidebar
1727                    let mut spacing = ui.ctx().style().spacing.item_spacing.y * 2.0;
1728
1729                    // Update paths pinned to the left sidebar by the user
1730                    if self.config.show_pinned_folders && self.ui_update_pinned_folders(ui, spacing)
1731                    {
1732                        spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1733                    }
1734
1735                    // Update custom quick access sections
1736                    let quick_accesses = std::mem::take(&mut self.config.quick_accesses);
1737
1738                    for quick_access in &quick_accesses {
1739                        ui.add_space(spacing);
1740                        self.ui_update_quick_access(ui, quick_access);
1741                        spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1742                    }
1743
1744                    self.config.quick_accesses = quick_accesses;
1745
1746                    // Update native quick access sections
1747                    if self.config.show_places && self.ui_update_user_directories(ui, spacing) {
1748                        spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1749                    }
1750
1751                    let disks = std::mem::take(&mut self.system_disks);
1752
1753                    if self.config.show_devices && self.ui_update_devices(ui, spacing, &disks) {
1754                        spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1755                    }
1756
1757                    if self.config.show_removable_devices
1758                        && self.ui_update_removable_devices(ui, spacing, &disks)
1759                    {
1760                        // Add this when we add a new section after removable devices
1761                        // spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1762                    }
1763
1764                    self.system_disks = disks;
1765                });
1766        });
1767    }
1768
1769    /// Updates a path entry in the left panel.
1770    ///
1771    /// Returns the response of the selectable label.
1772    fn ui_update_left_panel_entry(
1773        &mut self,
1774        ui: &mut egui::Ui,
1775        display_name: &str,
1776        path: &Path,
1777    ) -> egui::Response {
1778        let response = ui.selectable_label(self.current_directory() == Some(path), display_name);
1779
1780        if response.clicked() {
1781            self.load_directory(path);
1782        }
1783
1784        response
1785    }
1786
1787    /// Updates a custom quick access section added to the left panel.
1788    fn ui_update_quick_access(&mut self, ui: &mut egui::Ui, quick_access: &QuickAccess) {
1789        ui.label(&quick_access.heading);
1790
1791        for entry in &quick_access.paths {
1792            self.ui_update_left_panel_entry(ui, &entry.display_name, &entry.path);
1793        }
1794    }
1795
1796    /// Updates the list of pinned folders.
1797    ///
1798    /// Returns true if at least one directory item was included in the list and the
1799    /// heading is visible. If no item was listed, false is returned.
1800    fn ui_update_pinned_folders(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1801        let mut visible = false;
1802
1803        for (i, pinned) in self.storage.pinned_folders.clone().iter().enumerate() {
1804            if i == 0 {
1805                ui.add_space(spacing);
1806                ui.label(self.config.labels.heading_pinned.as_str());
1807
1808                visible = true;
1809            }
1810
1811            if self.is_pinned_folder_being_renamed(pinned) {
1812                self.ui_update_pinned_folder_rename(ui);
1813                continue;
1814            }
1815
1816            let response = self.ui_update_left_panel_entry(
1817                ui,
1818                &format!("{}  {}", self.config.pinned_icon, &pinned.label),
1819                pinned.path.as_path(),
1820            );
1821
1822            self.ui_update_pinned_folder_context_menu(&response, pinned);
1823        }
1824
1825        visible
1826    }
1827
1828    fn ui_update_pinned_folder_rename(&mut self, ui: &mut egui::Ui) {
1829        if let Some(r) = &mut self.rename_pinned_folder {
1830            let id = self.window_id.with("pinned_folder_rename").with(&r.path);
1831            let mut output = egui::TextEdit::singleline(&mut r.label)
1832                .id(id)
1833                .cursor_at_end(true)
1834                .show(ui);
1835
1836            if self.rename_pinned_folder_request_focus {
1837                output.state.cursor.set_char_range(Some(CCursorRange::two(
1838                    CCursor::new(0),
1839                    CCursor::new(r.label.chars().count()),
1840                )));
1841                output.state.store(ui.ctx(), output.response.id);
1842
1843                output.response.request_focus();
1844
1845                self.rename_pinned_folder_request_focus = false;
1846            }
1847
1848            if output.response.lost_focus() {
1849                self.end_rename_pinned_folder();
1850            }
1851        }
1852    }
1853
1854    fn ui_update_pinned_folder_context_menu(
1855        &mut self,
1856        item: &egui::Response,
1857        pinned: &PinnedFolder,
1858    ) {
1859        item.context_menu(|ui| {
1860            if ui.button(&self.config.labels.unpin_folder).clicked() {
1861                self.unpin_path(&pinned.path);
1862                ui.close();
1863            }
1864
1865            if ui
1866                .button(&self.config.labels.rename_pinned_folder)
1867                .clicked()
1868            {
1869                self.begin_rename_pinned_folder(pinned.clone());
1870                ui.close();
1871            }
1872        });
1873    }
1874
1875    /// Updates the list of user directories (Places).
1876    ///
1877    /// Returns true if at least one directory was included in the list and the
1878    /// heading is visible. If no directory was listed, false is returned.
1879    fn ui_update_user_directories(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1880        // Take temporary ownership of the user directories and configuration.
1881        // This is done so that we don't have to clone the user directories and
1882        // configured display names.
1883        let user_directories = std::mem::take(&mut self.user_directories);
1884        let labels = std::mem::take(&mut self.config.labels);
1885
1886        let mut visible = false;
1887
1888        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            visible = true;
1915        }
1916
1917        self.user_directories = user_directories;
1918        self.config.labels = labels;
1919
1920        visible
1921    }
1922
1923    /// Updates the list of devices like system disks.
1924    ///
1925    /// Returns true if at least one device was included in the list and the
1926    /// heading is visible. If no device was listed, false is returned.
1927    fn ui_update_devices(&mut self, ui: &mut egui::Ui, spacing: f32, disks: &Disks) -> bool {
1928        let mut visible = false;
1929
1930        for (i, disk) in disks.iter().filter(|x| !x.is_removable()).enumerate() {
1931            if i == 0 {
1932                ui.add_space(spacing);
1933                ui.label(self.config.labels.heading_devices.as_str());
1934
1935                visible = true;
1936            }
1937
1938            self.ui_update_device_entry(ui, disk);
1939        }
1940
1941        visible
1942    }
1943
1944    /// Updates the list of removable devices like USB drives.
1945    ///
1946    /// Returns true if at least one device was included in the list and the
1947    /// heading is visible. If no device was listed, false is returned.
1948    fn ui_update_removable_devices(
1949        &mut self,
1950        ui: &mut egui::Ui,
1951        spacing: f32,
1952        disks: &Disks,
1953    ) -> bool {
1954        let mut visible = false;
1955
1956        for (i, disk) in disks.iter().filter(|x| x.is_removable()).enumerate() {
1957            if i == 0 {
1958                ui.add_space(spacing);
1959                ui.label(self.config.labels.heading_removable_devices.as_str());
1960
1961                visible = true;
1962            }
1963
1964            self.ui_update_device_entry(ui, disk);
1965        }
1966
1967        visible
1968    }
1969
1970    /// Updates a device entry of a device list like "Devices" or "Removable Devices".
1971    fn ui_update_device_entry(&mut self, ui: &mut egui::Ui, device: &Disk) {
1972        let label = if device.is_removable() {
1973            format!(
1974                "{}  {}",
1975                self.config.removable_device_icon,
1976                device.display_name()
1977            )
1978        } else {
1979            format!("{}  {}", self.config.device_icon, device.display_name())
1980        };
1981
1982        self.ui_update_left_panel_entry(ui, &label, device.mount_point());
1983    }
1984
1985    /// Updates the bottom panel showing the selected item and main action buttons.
1986    fn ui_update_bottom_panel(&mut self, ui: &mut egui::Ui) {
1987        const BUTTON_HEIGHT: f32 = 20.0;
1988        ui.add_space(5.0);
1989
1990        // Calculate the width of the action buttons
1991        let label_submit_width = match self.mode {
1992            DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
1993                Self::calc_text_width(ui, &self.config.labels.open_button)
1994            }
1995            DialogMode::SaveFile => Self::calc_text_width(ui, &self.config.labels.save_button),
1996        };
1997
1998        let mut btn_width = Self::calc_text_width(ui, &self.config.labels.cancel_button);
1999        if label_submit_width > btn_width {
2000            btn_width = label_submit_width;
2001        }
2002
2003        btn_width += ui.spacing().button_padding.x * 4.0;
2004
2005        // The size of the action buttons "cancel" and "open"/"save"
2006        let button_size: egui::Vec2 = egui::Vec2::new(btn_width, BUTTON_HEIGHT);
2007
2008        self.ui_update_selection_preview(ui, button_size);
2009
2010        if self.mode == DialogMode::SaveFile && self.config.save_extensions.is_empty() {
2011            ui.add_space(ui.style().spacing.item_spacing.y);
2012        }
2013
2014        self.ui_update_action_buttons(ui, button_size);
2015    }
2016
2017    /// Updates the selection preview like "Selected directory: X"
2018    fn ui_update_selection_preview(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2019        const SELECTION_PREVIEW_MIN_WIDTH: f32 = 50.0;
2020        let item_spacing = ui.style().spacing.item_spacing;
2021
2022        let render_filter_selection = (!self.config.file_filters.is_empty()
2023            && (self.mode == DialogMode::PickFile || self.mode == DialogMode::PickMultiple))
2024            || (!self.config.save_extensions.is_empty() && self.mode == DialogMode::SaveFile);
2025
2026        let filter_selection_width = button_size.x.mul_add(2.0, item_spacing.x);
2027        let mut filter_selection_separate_line = false;
2028
2029        ui.horizontal(|ui| {
2030            match &self.mode {
2031                DialogMode::PickDirectory => ui.label(&self.config.labels.selected_directory),
2032                DialogMode::PickFile => ui.label(&self.config.labels.selected_file),
2033                DialogMode::PickMultiple => ui.label(&self.config.labels.selected_items),
2034                DialogMode::SaveFile => ui.label(&self.config.labels.file_name),
2035            };
2036
2037            // Make sure there is enough width for the selection preview. If the available
2038            // width is not enough, render the drop-down menu to select a file filter or
2039            // save extension on a separate line and give the selection preview
2040            // the entire available width.
2041            let mut scroll_bar_width: f32 =
2042                ui.available_width() - filter_selection_width - item_spacing.x;
2043
2044            if scroll_bar_width < SELECTION_PREVIEW_MIN_WIDTH || !render_filter_selection {
2045                filter_selection_separate_line = true;
2046                scroll_bar_width = ui.available_width();
2047            }
2048
2049            match &self.mode {
2050                DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2051                    use egui::containers::scroll_area::ScrollBarVisibility;
2052
2053                    let text = self.get_selection_preview_text();
2054
2055                    egui::containers::ScrollArea::horizontal()
2056                        .auto_shrink([false, false])
2057                        .max_width(scroll_bar_width)
2058                        .stick_to_right(true)
2059                        .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
2060                        .show(ui, |ui| {
2061                            ui.colored_label(ui.style().visuals.selection.bg_fill, text);
2062                        });
2063                }
2064                DialogMode::SaveFile => {
2065                    let mut output = egui::TextEdit::singleline(&mut self.file_name_input)
2066                        .cursor_at_end(false)
2067                        .margin(egui::Margin::symmetric(4, 3))
2068                        .desired_width(scroll_bar_width - item_spacing.x)
2069                        .show(ui);
2070
2071                    if self.file_name_input_request_focus {
2072                        self.highlight_file_name_input(&mut output);
2073                        output.state.store(ui.ctx(), output.response.id);
2074
2075                        output.response.request_focus();
2076                        self.file_name_input_request_focus = false;
2077                    }
2078
2079                    if output.response.changed() {
2080                        self.file_name_input_error = self.validate_file_name_input();
2081                    }
2082
2083                    if output.response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
2084                    {
2085                        self.submit();
2086                    }
2087                }
2088            }
2089
2090            if !filter_selection_separate_line && render_filter_selection {
2091                if self.mode == DialogMode::SaveFile {
2092                    self.ui_update_save_extension_selection(ui, filter_selection_width);
2093                } else {
2094                    self.ui_update_file_filter_selection(ui, filter_selection_width);
2095                }
2096            }
2097        });
2098
2099        if filter_selection_separate_line && render_filter_selection {
2100            ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2101                if self.mode == DialogMode::SaveFile {
2102                    self.ui_update_save_extension_selection(ui, filter_selection_width);
2103                } else {
2104                    self.ui_update_file_filter_selection(ui, filter_selection_width);
2105                }
2106            });
2107        }
2108    }
2109
2110    /// Highlights the characters inside the file name input until the file extension.
2111    /// Do not forget to store these changes after calling this function:
2112    /// `output.state.store(ui.ctx(), output.response.id);`
2113    fn highlight_file_name_input(&self, output: &mut egui::text_edit::TextEditOutput) {
2114        if let Some(pos) = self.file_name_input.rfind('.') {
2115            let range = if pos == 0 {
2116                CCursorRange::two(CCursor::new(0), CCursor::new(0))
2117            } else {
2118                CCursorRange::two(CCursor::new(0), CCursor::new(pos))
2119            };
2120
2121            output.state.cursor.set_char_range(Some(range));
2122        }
2123    }
2124
2125    fn get_selection_preview_text(&self) -> String {
2126        if self.is_selection_valid() {
2127            match &self.mode {
2128                DialogMode::PickDirectory | DialogMode::PickFile => self
2129                    .selected_item
2130                    .as_ref()
2131                    .map_or_else(String::new, |item| item.file_name().to_string()),
2132                DialogMode::PickMultiple => {
2133                    let mut result = String::new();
2134
2135                    for (i, item) in self
2136                        .get_dir_content_filtered_iter()
2137                        .filter(|p| p.selected)
2138                        .enumerate()
2139                    {
2140                        if i == 0 {
2141                            result += item.file_name();
2142                            continue;
2143                        }
2144
2145                        result += format!(", {}", item.file_name()).as_str();
2146                    }
2147
2148                    result
2149                }
2150                DialogMode::SaveFile => String::new(),
2151            }
2152        } else {
2153            String::new()
2154        }
2155    }
2156
2157    fn ui_update_file_filter_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2158        let selected_filter = self.get_selected_file_filter();
2159        let selected_text = match selected_filter {
2160            Some(f) => &f.name,
2161            None => &self.config.labels.file_filter_all_files,
2162        };
2163
2164        // The item that the user selected inside the drop down.
2165        // If none, the user did not change the selected item this frame.
2166        let mut select_filter: Option<Option<FileFilter>> = None;
2167
2168        egui::containers::ComboBox::from_id_salt(self.window_id.with("file_filter_selection"))
2169            .width(width)
2170            .selected_text(selected_text)
2171            .wrap_mode(egui::TextWrapMode::Truncate)
2172            .show_ui(ui, |ui| {
2173                for filter in &self.config.file_filters {
2174                    let selected = selected_filter.is_some_and(|f| f.id == filter.id);
2175
2176                    if ui.selectable_label(selected, &filter.name).clicked() {
2177                        select_filter = Some(Some(filter.clone()));
2178                    }
2179                }
2180
2181                if ui
2182                    .selectable_label(
2183                        selected_filter.is_none(),
2184                        &self.config.labels.file_filter_all_files,
2185                    )
2186                    .clicked()
2187                {
2188                    select_filter = Some(None);
2189                }
2190            });
2191
2192        if let Some(i) = select_filter {
2193            self.select_file_filter(i);
2194        }
2195    }
2196
2197    fn ui_update_save_extension_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2198        let selected_extension = self.get_selected_save_extension();
2199        let selected_text = match selected_extension {
2200            Some(e) => &e.to_string(),
2201            None => &self.config.labels.save_extension_any,
2202        };
2203
2204        // The item that the user selected inside the drop down.
2205        // If none, the user did not change the selected item this frame.
2206        let mut select_extension: Option<Option<SaveExtension>> = None;
2207
2208        egui::containers::ComboBox::from_id_salt(self.window_id.with("save_extension_selection"))
2209            .width(width)
2210            .selected_text(selected_text)
2211            .wrap_mode(egui::TextWrapMode::Truncate)
2212            .show_ui(ui, |ui| {
2213                for extension in &self.config.save_extensions {
2214                    let selected = selected_extension.is_some_and(|s| s.id == extension.id);
2215
2216                    if ui
2217                        .selectable_label(selected, extension.to_string())
2218                        .clicked()
2219                    {
2220                        select_extension = Some(Some(extension.clone()));
2221                    }
2222                }
2223            });
2224
2225        if let Some(i) = select_extension {
2226            self.file_name_input_request_focus = true;
2227            self.select_save_extension(i);
2228        }
2229    }
2230
2231    /// Updates the action buttons like save, open and cancel
2232    fn ui_update_action_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2233        ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2234            let label = match &self.mode {
2235                DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2236                    self.config.labels.open_button.as_str()
2237                }
2238                DialogMode::SaveFile => self.config.labels.save_button.as_str(),
2239            };
2240
2241            if self.ui_button_sized(
2242                ui,
2243                self.is_selection_valid(),
2244                button_size,
2245                label,
2246                self.file_name_input_error.as_deref(),
2247            ) {
2248                self.submit();
2249            }
2250
2251            if ui
2252                .add_sized(
2253                    button_size,
2254                    egui::Button::new(self.config.labels.cancel_button.as_str()),
2255                )
2256                .clicked()
2257            {
2258                self.cancel();
2259            }
2260        });
2261    }
2262
2263    /// Updates the central panel. This is either the contents of the directory
2264    /// or the error message when there was an error loading the current directory.
2265    fn ui_update_central_panel(&mut self, ui: &mut egui::Ui) {
2266        if self.update_directory_content(ui) {
2267            return;
2268        }
2269
2270        self.ui_update_central_panel_content(ui);
2271    }
2272
2273    /// Updates the directory content (Not the UI!).
2274    /// This is required because the contents of the directory might be loaded on a
2275    /// separate thread. This function checks the status of the directory content
2276    /// and updates the UI accordingly.
2277    fn update_directory_content(&mut self, ui: &mut egui::Ui) -> bool {
2278        const SHOW_SPINNER_AFTER: f32 = 0.2;
2279
2280        match self.directory_content.update() {
2281            DirectoryContentState::Pending(timestamp) => {
2282                let now = std::time::SystemTime::now();
2283
2284                if now
2285                    .duration_since(*timestamp)
2286                    .unwrap_or_default()
2287                    .as_secs_f32()
2288                    > SHOW_SPINNER_AFTER
2289                {
2290                    ui.centered_and_justified(egui::Ui::spinner);
2291                }
2292
2293                // Prevent egui from not updating the UI when there is no user input
2294                ui.ctx().request_repaint();
2295
2296                true
2297            }
2298            DirectoryContentState::Errored(err) => {
2299                ui.centered_and_justified(|ui| ui.colored_label(ui.visuals().error_fg_color, err));
2300                true
2301            }
2302            DirectoryContentState::Finished => {
2303                if self.mode == DialogMode::PickDirectory {
2304                    if let Some(dir) = self.current_directory() {
2305                        let mut dir_entry =
2306                            DirectoryEntry::from_path(&self.config, dir, &*self.config.file_system);
2307                        self.select_item(&mut dir_entry);
2308                    }
2309                }
2310
2311                false
2312            }
2313            DirectoryContentState::Success => false,
2314        }
2315    }
2316
2317    /// Updates the contents of the currently open directory.
2318    /// TODO: Refactor
2319    fn ui_update_central_panel_content(&mut self, ui: &mut egui::Ui) {
2320        // Temporarily take ownership of the directory content.
2321        let mut data = std::mem::take(&mut self.directory_content);
2322
2323        // If the multi selection should be reset, excluding the currently
2324        // selected primary item.
2325        let mut reset_multi_selection = false;
2326
2327        // The item the user wants to make a batch selection from.
2328        // The primary selected item is used for item a.
2329        let mut batch_select_item_b: Option<DirectoryEntry> = None;
2330
2331        // If we should return after updating the directory entries.
2332        let mut should_return = false;
2333
2334        ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
2335            let scroll_area = egui::containers::ScrollArea::vertical().auto_shrink([false, false]);
2336
2337            if self.search_value.is_empty()
2338                && !self.create_directory_dialog.is_open()
2339                && !self.scroll_to_selection
2340            {
2341                // Only update visible items when the search value is empty,
2342                // the create directory dialog is closed and we are currently not scrolling
2343                // to the current item.
2344                scroll_area.show_rows(ui, ui.spacing().interact_size.y, data.len(), |ui, range| {
2345                    for item in data.iter_range_mut(range) {
2346                        if self.ui_update_central_panel_entry(
2347                            ui,
2348                            item,
2349                            &mut reset_multi_selection,
2350                            &mut batch_select_item_b,
2351                        ) {
2352                            should_return = true;
2353                        }
2354                    }
2355                });
2356            } else {
2357                // Update each element if the search value is not empty as we apply the
2358                // search value in every frame. We can't use `egui::ScrollArea::show_rows`
2359                // because we don't know how many files the search value applies to.
2360                // We also have to update every item when the create directory dialog is open as
2361                // it's displayed as the last element.
2362                scroll_area.show(ui, |ui| {
2363                    for item in data.filtered_iter_mut(&self.search_value.clone()) {
2364                        if self.ui_update_central_panel_entry(
2365                            ui,
2366                            item,
2367                            &mut reset_multi_selection,
2368                            &mut batch_select_item_b,
2369                        ) {
2370                            should_return = true;
2371                        }
2372                    }
2373
2374                    if let Some(entry) = self.ui_update_create_directory_dialog(ui) {
2375                        data.push(entry);
2376                    }
2377                });
2378            }
2379        });
2380
2381        if should_return {
2382            return;
2383        }
2384
2385        // Reset the multi selection except the currently selected primary item
2386        if reset_multi_selection {
2387            for item in data.filtered_iter_mut(&self.search_value) {
2388                if let Some(selected_item) = &self.selected_item {
2389                    if selected_item.path_eq(item) {
2390                        continue;
2391                    }
2392                }
2393
2394                item.selected = false;
2395            }
2396        }
2397
2398        // Check if we should perform a batch selection
2399        if let Some(item_b) = batch_select_item_b {
2400            if let Some(item_a) = &self.selected_item {
2401                self.batch_select_between(&mut data, item_a, &item_b);
2402            }
2403        }
2404
2405        self.directory_content = data;
2406        self.scroll_to_selection = false;
2407    }
2408
2409    /// Updates a single directory content entry.
2410    /// TODO: Refactor
2411    fn ui_update_central_panel_entry(
2412        &mut self,
2413        ui: &mut egui::Ui,
2414        item: &mut DirectoryEntry,
2415        reset_multi_selection: &mut bool,
2416        batch_select_item_b: &mut Option<DirectoryEntry>,
2417    ) -> bool {
2418        let file_name = item.file_name();
2419        let primary_selected = self.is_primary_selected(item);
2420        let pinned = self.is_pinned(item.as_path());
2421
2422        let icons = if pinned {
2423            format!("{} {} ", item.icon(), self.config.pinned_icon)
2424        } else {
2425            format!("{} ", item.icon())
2426        };
2427
2428        let icons_width = Self::calc_text_width(ui, &icons);
2429
2430        // Calc available width for the file name and include a small margin
2431        let available_width = ui.available_width() - icons_width - 15.0;
2432
2433        let truncate = self.config.truncate_filenames
2434            && available_width < Self::calc_text_width(ui, file_name);
2435
2436        let text = if truncate {
2437            Self::truncate_filename(ui, item, available_width)
2438        } else {
2439            file_name.to_owned()
2440        };
2441
2442        let mut re =
2443            ui.selectable_label(primary_selected || item.selected, format!("{icons}{text}"));
2444
2445        if truncate {
2446            re = re.on_hover_text(file_name);
2447        }
2448
2449        if item.is_dir() {
2450            self.ui_update_central_panel_path_context_menu(&re, item.as_path());
2451
2452            if re.context_menu_opened() {
2453                self.select_item(item);
2454            }
2455        }
2456
2457        if primary_selected && self.scroll_to_selection {
2458            re.scroll_to_me(Some(egui::Align::Center));
2459            self.scroll_to_selection = false;
2460        }
2461
2462        // The user wants to select the item as the primary selected item
2463        if re.clicked()
2464            && !ui.input(|i| i.modifiers.command)
2465            && !ui.input(|i| i.modifiers.shift_only())
2466        {
2467            self.select_item(item);
2468
2469            // Reset the multi selection except the now primary selected item
2470            if self.mode == DialogMode::PickMultiple {
2471                *reset_multi_selection = true;
2472            }
2473        }
2474
2475        // The user wants to select or unselect the item as part of a
2476        // multi selection
2477        if self.mode == DialogMode::PickMultiple
2478            && re.clicked()
2479            && ui.input(|i| i.modifiers.command)
2480        {
2481            if primary_selected {
2482                // If the clicked item is the primary selected item,
2483                // deselect it and remove it from the multi selection
2484                item.selected = false;
2485                self.selected_item = None;
2486            } else {
2487                item.selected = !item.selected;
2488
2489                // If the item was selected, make it the primary selected item
2490                if item.selected {
2491                    self.select_item(item);
2492                }
2493            }
2494        }
2495
2496        // The user wants to select every item between the last selected item
2497        // and the current item
2498        if self.mode == DialogMode::PickMultiple
2499            && re.clicked()
2500            && ui.input(|i| i.modifiers.shift_only())
2501        {
2502            if let Some(selected_item) = self.selected_item.clone() {
2503                // We perform a batch selection from the item that was
2504                // primarily selected before the user clicked on this item.
2505                *batch_select_item_b = Some(selected_item);
2506
2507                // And now make this item the primary selected item
2508                item.selected = true;
2509                self.select_item(item);
2510            }
2511        }
2512
2513        // The user double clicked on the directory entry.
2514        // Either open the directory or submit the dialog.
2515        if re.double_clicked() && !ui.input(|i| i.modifiers.command) {
2516            if item.is_dir() {
2517                self.load_directory(&item.to_path_buf());
2518                return true;
2519            }
2520
2521            self.select_item(item);
2522
2523            self.submit();
2524        }
2525
2526        false
2527    }
2528
2529    fn ui_update_create_directory_dialog(&mut self, ui: &mut egui::Ui) -> Option<DirectoryEntry> {
2530        self.create_directory_dialog
2531            .update(ui, &self.config)
2532            .directory()
2533            .map(|path| self.process_new_folder(&path))
2534    }
2535
2536    /// Selects every item inside the `directory_content` between `item_a` and `item_b`,
2537    /// excluding both given items.
2538    fn batch_select_between(
2539        &self,
2540        directory_content: &mut DirectoryContent,
2541        item_a: &DirectoryEntry,
2542        item_b: &DirectoryEntry,
2543    ) {
2544        // Get the position of item a and item b
2545        let pos_a = directory_content
2546            .filtered_iter(&self.search_value)
2547            .position(|p| p.path_eq(item_a));
2548        let pos_b = directory_content
2549            .filtered_iter(&self.search_value)
2550            .position(|p| p.path_eq(item_b));
2551
2552        // If both items where found inside the directory entry, mark every item between
2553        // them as selected
2554        if let Some(pos_a) = pos_a {
2555            if let Some(pos_b) = pos_b {
2556                if pos_a == pos_b {
2557                    return;
2558                }
2559
2560                // Get the min and max of both positions.
2561                // We will iterate from min to max.
2562                let mut min = pos_a;
2563                let mut max = pos_b;
2564
2565                if min > max {
2566                    min = pos_b;
2567                    max = pos_a;
2568                }
2569
2570                for item in directory_content
2571                    .filtered_iter_mut(&self.search_value)
2572                    .enumerate()
2573                    .filter(|(i, _)| i > &min && i < &max)
2574                    .map(|(_, p)| p)
2575                {
2576                    item.selected = true;
2577                }
2578            }
2579        }
2580    }
2581
2582    /// Helper function to add a sized button that can be enabled or disabled
2583    fn ui_button_sized(
2584        &self,
2585        ui: &mut egui::Ui,
2586        enabled: bool,
2587        size: egui::Vec2,
2588        label: &str,
2589        err_tooltip: Option<&str>,
2590    ) -> bool {
2591        let mut clicked = false;
2592
2593        ui.add_enabled_ui(enabled, |ui| {
2594            let response = ui.add_sized(size, egui::Button::new(label));
2595            clicked = response.clicked();
2596
2597            if let Some(err) = err_tooltip {
2598                response.on_disabled_hover_ui(|ui| {
2599                    ui.horizontal_wrapped(|ui| {
2600                        ui.spacing_mut().item_spacing.x = 0.0;
2601
2602                        ui.colored_label(
2603                            ui.ctx().style().visuals.error_fg_color,
2604                            format!("{} ", self.config.err_icon),
2605                        );
2606
2607                        ui.label(err);
2608                    });
2609                });
2610            }
2611        });
2612
2613        clicked
2614    }
2615
2616    /// Updates the context menu of a path inside the central panel.
2617    ///
2618    /// # Arguments
2619    ///
2620    /// * `item` - The response of the egui item for which the context menu should be opened.
2621    /// * `path` - The path for which the context menu should be opened.
2622    fn ui_update_central_panel_path_context_menu(&mut self, item: &egui::Response, path: &Path) {
2623        // Path context menus are currently only used for pinned folders.
2624        if !self.config.show_pinned_folders {
2625            return;
2626        }
2627
2628        item.context_menu(|ui| {
2629            let pinned = self.is_pinned(path);
2630
2631            if pinned {
2632                if ui.button(&self.config.labels.unpin_folder).clicked() {
2633                    self.unpin_path(path);
2634                    ui.close();
2635                }
2636            } else if ui.button(&self.config.labels.pin_folder).clicked() {
2637                self.pin_path(path.to_path_buf());
2638                ui.close();
2639            }
2640        });
2641    }
2642
2643    /// Sets the cursor position to the end of a text input field.
2644    ///
2645    /// # Arguments
2646    ///
2647    /// * `re` - response of the text input widget
2648    /// * `data` - buffer holding the text of the input widget
2649    fn set_cursor_to_end(re: &egui::Response, data: &str) {
2650        // Set the cursor to the end of the filter input string
2651        if let Some(mut state) = egui::TextEdit::load_state(&re.ctx, re.id) {
2652            state
2653                .cursor
2654                .set_char_range(Some(CCursorRange::one(CCursor::new(data.len()))));
2655            state.store(&re.ctx, re.id);
2656        }
2657    }
2658
2659    /// Calculates the width of a single char.
2660    fn calc_char_width(ui: &egui::Ui, char: char) -> f32 {
2661        ui.fonts(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char))
2662    }
2663
2664    /// Calculates the width of the specified text using the current font configuration.
2665    /// Does not take new lines or text breaks into account!
2666    fn calc_text_width(ui: &egui::Ui, text: &str) -> f32 {
2667        let mut width = 0.0;
2668
2669        for char in text.chars() {
2670            width += Self::calc_char_width(ui, char);
2671        }
2672
2673        width
2674    }
2675
2676    fn truncate_filename(ui: &egui::Ui, item: &DirectoryEntry, max_length: f32) -> String {
2677        const TRUNCATE_STR: &str = "...";
2678
2679        let path = item.as_path();
2680
2681        let file_stem = if item.is_file() {
2682            path.file_stem().and_then(|f| f.to_str()).unwrap_or("")
2683        } else {
2684            item.file_name()
2685        };
2686
2687        let extension = if item.is_file() {
2688            path.extension().map_or(String::new(), |ext| {
2689                format!(".{}", ext.to_str().unwrap_or(""))
2690            })
2691        } else {
2692            String::new()
2693        };
2694
2695        let extension_width = Self::calc_text_width(ui, &extension);
2696        let reserved = extension_width + Self::calc_text_width(ui, TRUNCATE_STR);
2697
2698        if max_length <= reserved {
2699            return format!("{TRUNCATE_STR}{extension}");
2700        }
2701
2702        let mut width = reserved;
2703        let mut front = String::new();
2704        let mut back = String::new();
2705
2706        for (i, char) in file_stem.chars().enumerate() {
2707            let w = Self::calc_char_width(ui, char);
2708
2709            if width + w > max_length {
2710                break;
2711            }
2712
2713            front.push(char);
2714            width += w;
2715
2716            let back_index = file_stem.len() - i - 1;
2717
2718            if back_index <= i {
2719                break;
2720            }
2721
2722            if let Some(char) = file_stem.chars().nth(back_index) {
2723                let w = Self::calc_char_width(ui, char);
2724
2725                if width + w > max_length {
2726                    break;
2727                }
2728
2729                back.push(char);
2730                width += w;
2731            }
2732        }
2733
2734        format!(
2735            "{front}{TRUNCATE_STR}{}{extension}",
2736            back.chars().rev().collect::<String>()
2737        )
2738    }
2739}
2740
2741/// Keybindings
2742impl FileDialog {
2743    /// Checks whether certain keybindings have been pressed and executes the corresponding actions.
2744    fn update_keybindings(&mut self, ctx: &egui::Context) {
2745        // We don't want to execute keybindings if a modal is currently open.
2746        // The modals implement the keybindings themselves.
2747        if let Some(modal) = self.modals.last_mut() {
2748            modal.update_keybindings(&self.config, ctx);
2749            return;
2750        }
2751
2752        let keybindings = std::mem::take(&mut self.config.keybindings);
2753
2754        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.submit, false) {
2755            self.exec_keybinding_submit();
2756        }
2757
2758        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.cancel, false) {
2759            self.exec_keybinding_cancel();
2760        }
2761
2762        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.parent, true) {
2763            self.load_parent_directory();
2764        }
2765
2766        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.back, true) {
2767            self.load_previous_directory();
2768        }
2769
2770        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.forward, true) {
2771            self.load_next_directory();
2772        }
2773
2774        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.reload, true) {
2775            self.refresh();
2776        }
2777
2778        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.new_folder, true) {
2779            self.open_new_folder_dialog();
2780        }
2781
2782        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.edit_path, true) {
2783            self.open_path_edit();
2784        }
2785
2786        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.home_edit_path, true) {
2787            if let Some(dirs) = &self.user_directories {
2788                if let Some(home) = dirs.home_dir() {
2789                    self.load_directory(home.to_path_buf().as_path());
2790                    self.open_path_edit();
2791                }
2792            }
2793        }
2794
2795        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_up, false) {
2796            self.exec_keybinding_selection_up();
2797
2798            // We want to break out of input fields like search when pressing selection keys
2799            if let Some(id) = ctx.memory(egui::Memory::focused) {
2800                ctx.memory_mut(|w| w.surrender_focus(id));
2801            }
2802        }
2803
2804        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_down, false) {
2805            self.exec_keybinding_selection_down();
2806
2807            // We want to break out of input fields like search when pressing selection keys
2808            if let Some(id) = ctx.memory(egui::Memory::focused) {
2809                ctx.memory_mut(|w| w.surrender_focus(id));
2810            }
2811        }
2812
2813        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.select_all, true)
2814            && self.mode == DialogMode::PickMultiple
2815        {
2816            for item in self.directory_content.filtered_iter_mut(&self.search_value) {
2817                item.selected = true;
2818            }
2819        }
2820
2821        self.config.keybindings = keybindings;
2822    }
2823
2824    /// Executes the action when the keybinding `submit` is pressed.
2825    fn exec_keybinding_submit(&mut self) {
2826        if self.path_edit_visible {
2827            self.submit_path_edit();
2828            return;
2829        }
2830
2831        if self.create_directory_dialog.is_open() {
2832            if let Some(dir) = self.create_directory_dialog.submit().directory() {
2833                self.process_new_folder(&dir);
2834            }
2835            return;
2836        }
2837
2838        if self.any_focused_last_frame {
2839            return;
2840        }
2841
2842        // Check if there is a directory selected we can open
2843        if let Some(item) = &self.selected_item {
2844            // Make sure the selected item is visible inside the directory view.
2845            let is_visible = self
2846                .get_dir_content_filtered_iter()
2847                .any(|p| p.path_eq(item));
2848
2849            if is_visible && item.is_dir() {
2850                self.load_directory(&item.to_path_buf());
2851                return;
2852            }
2853        }
2854
2855        self.submit();
2856    }
2857
2858    /// Executes the action when the keybinding `cancel` is pressed.
2859    fn exec_keybinding_cancel(&mut self) {
2860        // We have to check if the `create_directory_dialog` and `path_edit_visible` is open,
2861        // because egui does not consume pressing the escape key inside a text input.
2862        // So when pressing the escape key inside a text input, the text input is closed
2863        // but the keybindings still register the press on the escape key.
2864        // (Although the keybindings are updated before the UI and they check whether another
2865        //  widget is currently in focus!)
2866        //
2867        // This is practical for us because we can close the path edit and
2868        // the create directory dialog.
2869        // However, this causes problems when the user presses escape in other text
2870        // inputs for which we have no status saved. This would then close the entire file dialog.
2871        // To fix this, we check if any item was focused in the last frame.
2872        //
2873        // Note that this only happens with the escape key and not when the enter key is
2874        // used to close a text input. This is why we don't have to check for the
2875        // dialogs in `exec_keybinding_submit`.
2876
2877        if self.create_directory_dialog.is_open() {
2878            self.create_directory_dialog.close();
2879        } else if self.path_edit_visible {
2880            self.close_path_edit();
2881        } else if !self.any_focused_last_frame {
2882            self.cancel();
2883            return;
2884        }
2885    }
2886
2887    /// Executes the action when the keybinding `selection_up` is pressed.
2888    fn exec_keybinding_selection_up(&mut self) {
2889        if self.directory_content.len() == 0 {
2890            return;
2891        }
2892
2893        self.directory_content.reset_multi_selection();
2894
2895        if let Some(item) = &self.selected_item {
2896            if self.select_next_visible_item_before(&item.clone()) {
2897                return;
2898            }
2899        }
2900
2901        // No item is selected or no more items left.
2902        // Select the last item from the directory content.
2903        self.select_last_visible_item();
2904    }
2905
2906    /// Executes the action when the keybinding `selection_down` is pressed.
2907    fn exec_keybinding_selection_down(&mut self) {
2908        if self.directory_content.len() == 0 {
2909            return;
2910        }
2911
2912        self.directory_content.reset_multi_selection();
2913
2914        if let Some(item) = &self.selected_item {
2915            if self.select_next_visible_item_after(&item.clone()) {
2916                return;
2917            }
2918        }
2919
2920        // No item is selected or no more items left.
2921        // Select the last item from the directory content.
2922        self.select_first_visible_item();
2923    }
2924}
2925
2926/// Implementation
2927impl FileDialog {
2928    /// Get the file filter the user currently selected.
2929    fn get_selected_file_filter(&self) -> Option<&FileFilter> {
2930        self.selected_file_filter
2931            .and_then(|id| self.config.file_filters.iter().find(|p| p.id == id))
2932    }
2933
2934    /// Sets the default file filter to use.
2935    fn set_default_file_filter(&mut self) {
2936        if let Some(name) = &self.config.default_file_filter {
2937            for filter in &self.config.file_filters {
2938                if filter.name == name.as_str() {
2939                    self.selected_file_filter = Some(filter.id);
2940                }
2941            }
2942        }
2943    }
2944
2945    /// Selects the given file filter and applies the appropriate filters.
2946    fn select_file_filter(&mut self, filter: Option<FileFilter>) {
2947        self.selected_file_filter = filter.map(|f| f.id);
2948        self.selected_item = None;
2949        self.refresh();
2950    }
2951
2952    /// Get the save extension the user currently selected.
2953    fn get_selected_save_extension(&self) -> Option<&SaveExtension> {
2954        self.selected_save_extension
2955            .and_then(|id| self.config.save_extensions.iter().find(|p| p.id == id))
2956    }
2957
2958    /// Sets the save extension to use.
2959    fn set_default_save_extension(&mut self) {
2960        let config = std::mem::take(&mut self.config);
2961
2962        if let Some(name) = &config.default_save_extension {
2963            for extension in &config.save_extensions {
2964                if extension.name == name.as_str() {
2965                    self.selected_save_extension = Some(extension.id);
2966                    self.set_file_name_extension(&extension.file_extension);
2967                }
2968            }
2969        }
2970
2971        self.config = config;
2972    }
2973
2974    /// Selects the given save extension.
2975    fn select_save_extension(&mut self, extension: Option<SaveExtension>) {
2976        if let Some(ex) = extension {
2977            self.selected_save_extension = Some(ex.id);
2978            self.set_file_name_extension(&ex.file_extension);
2979        }
2980
2981        self.selected_item = None;
2982        self.refresh();
2983    }
2984
2985    /// Updates the extension of `Self::file_name_input`.
2986    fn set_file_name_extension(&mut self, extension: &str) {
2987        // Prevent `PathBuf::set_extension` to append the file extension when there is
2988        // already one without a file name. For example `.png` would be changed to `.png.txt`
2989        // when using `PathBuf::set_extension`.
2990        let dot_count = self.file_name_input.chars().filter(|c| *c == '.').count();
2991        let use_simple = dot_count == 1 && self.file_name_input.chars().nth(0) == Some('.');
2992
2993        let mut p = PathBuf::from(&self.file_name_input);
2994        if !use_simple && p.set_extension(extension) {
2995            self.file_name_input = p.to_string_lossy().into_owned();
2996        } else {
2997            self.file_name_input = format!(".{extension}");
2998        }
2999    }
3000
3001    /// Gets a filtered iterator of the directory content of this object.
3002    fn get_dir_content_filtered_iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
3003        self.directory_content.filtered_iter(&self.search_value)
3004    }
3005
3006    /// Opens the dialog to create a new folder.
3007    fn open_new_folder_dialog(&mut self) {
3008        if let Some(x) = self.current_directory() {
3009            self.create_directory_dialog.open(x.to_path_buf());
3010        }
3011    }
3012
3013    /// Function that processes a newly created folder.
3014    fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry {
3015        let mut entry =
3016            DirectoryEntry::from_path(&self.config, created_dir, &*self.config.file_system);
3017
3018        self.directory_content.push(entry.clone());
3019
3020        self.select_item(&mut entry);
3021
3022        entry
3023    }
3024
3025    /// Opens a new modal window.
3026    fn open_modal(&mut self, modal: Box<dyn FileDialogModal + Send + Sync>) {
3027        self.modals.push(modal);
3028    }
3029
3030    /// Executes the given modal action.
3031    fn exec_modal_action(&mut self, action: ModalAction) {
3032        match action {
3033            ModalAction::None => {}
3034            ModalAction::SaveFile(path) => self.state = DialogState::Picked(path),
3035        }
3036    }
3037
3038    /// Canonicalizes the specified path if canonicalization is enabled.
3039    /// Returns the input path if an error occurs or canonicalization is disabled.
3040    fn canonicalize_path(&self, path: &Path) -> PathBuf {
3041        if self.config.canonicalize_paths {
3042            dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
3043        } else {
3044            path.to_path_buf()
3045        }
3046    }
3047
3048    /// Pins a path to the left sidebar.
3049    fn pin_path(&mut self, path: PathBuf) {
3050        let pinned = PinnedFolder::from_path(path);
3051        self.storage.pinned_folders.push(pinned);
3052    }
3053
3054    /// Unpins a path from the left sidebar.
3055    fn unpin_path(&mut self, path: &Path) {
3056        self.storage
3057            .pinned_folders
3058            .retain(|p| p.path.as_path() != path);
3059    }
3060
3061    /// Checks if the path is pinned to the left sidebar.
3062    fn is_pinned(&self, path: &Path) -> bool {
3063        self.storage
3064            .pinned_folders
3065            .iter()
3066            .any(|p| p.path.as_path() == path)
3067    }
3068
3069    /// Starts to rename a pinned folder by showing the user a text input field.
3070    fn begin_rename_pinned_folder(&mut self, pinned: PinnedFolder) {
3071        self.rename_pinned_folder = Some(pinned);
3072        self.rename_pinned_folder_request_focus = true;
3073    }
3074
3075    /// Ends the renaming of a pinned folder. This updates the real pinned folder
3076    /// in `FileDialogStorage`.
3077    fn end_rename_pinned_folder(&mut self) {
3078        let renamed = std::mem::take(&mut self.rename_pinned_folder);
3079
3080        if let Some(renamed) = renamed {
3081            let old = self
3082                .storage
3083                .pinned_folders
3084                .iter_mut()
3085                .find(|p| p.path == renamed.path);
3086            if let Some(old) = old {
3087                old.label = renamed.label;
3088            }
3089        }
3090    }
3091
3092    /// Checks if the given pinned folder is currently being renamed.
3093    fn is_pinned_folder_being_renamed(&self, pinned: &PinnedFolder) -> bool {
3094        self.rename_pinned_folder
3095            .as_ref()
3096            .is_some_and(|p| p.path == pinned.path)
3097    }
3098
3099    fn is_primary_selected(&self, item: &DirectoryEntry) -> bool {
3100        self.selected_item.as_ref().is_some_and(|x| x.path_eq(item))
3101    }
3102
3103    /// Resets the dialog to use default values.
3104    /// Configuration variables are retained.
3105    fn reset(&mut self) {
3106        let storage = self.storage.clone();
3107        let config = self.config.clone();
3108        *self = Self::with_config(config);
3109        self.storage = storage;
3110    }
3111
3112    /// Refreshes the dialog.
3113    /// Including the user directories, system disks and currently open directory.
3114    fn refresh(&mut self) {
3115        self.user_directories = self
3116            .config
3117            .file_system
3118            .user_dirs(self.config.canonicalize_paths);
3119        self.system_disks = self
3120            .config
3121            .file_system
3122            .get_disks(self.config.canonicalize_paths);
3123
3124        self.reload_directory();
3125    }
3126
3127    /// Submits the current selection and tries to finish the dialog, if the selection is valid.
3128    fn submit(&mut self) {
3129        // Make sure the selected item or entered file name is valid.
3130        if !self.is_selection_valid() {
3131            return;
3132        }
3133
3134        self.storage.last_picked_dir = self.current_directory().map(PathBuf::from);
3135
3136        match &self.mode {
3137            DialogMode::PickDirectory | DialogMode::PickFile => {
3138                // Should always contain a value since `is_selection_valid` is used to
3139                // validate the selection.
3140                if let Some(item) = self.selected_item.clone() {
3141                    self.state = DialogState::Picked(item.to_path_buf());
3142                }
3143            }
3144            DialogMode::PickMultiple => {
3145                let result: Vec<PathBuf> = self
3146                    .selected_entries()
3147                    .map(crate::DirectoryEntry::to_path_buf)
3148                    .collect();
3149
3150                self.state = DialogState::PickedMultiple(result);
3151            }
3152            DialogMode::SaveFile => {
3153                // Should always contain a value since `is_selection_valid` is used to
3154                // validate the selection.
3155                if let Some(path) = self.current_directory() {
3156                    let full_path = path.join(&self.file_name_input);
3157                    self.submit_save_file(full_path);
3158                }
3159            }
3160        }
3161    }
3162
3163    /// Submits the file dialog with the specified path and opens the `OverwriteFileModal`
3164    /// if the path already exists.
3165    fn submit_save_file(&mut self, path: PathBuf) {
3166        if path.exists() {
3167            self.open_modal(Box::new(OverwriteFileModal::new(path)));
3168
3169            return;
3170        }
3171
3172        self.state = DialogState::Picked(path);
3173    }
3174
3175    /// Cancels the dialog.
3176    fn cancel(&mut self) {
3177        self.state = DialogState::Cancelled;
3178    }
3179
3180    /// This function generates the initial directory based on the configuration.
3181    /// The function does the following things:
3182    ///   - Get the path to open based on the opening mode
3183    ///   - Canonicalize the path if enabled
3184    ///   - Attempts to use the parent directory if the path is a file
3185    fn get_initial_directory(&self) -> PathBuf {
3186        let path = match self.config.opening_mode {
3187            OpeningMode::AlwaysInitialDir => &self.config.initial_directory,
3188            OpeningMode::LastVisitedDir => self
3189                .storage
3190                .last_visited_dir
3191                .as_deref()
3192                .unwrap_or(&self.config.initial_directory),
3193            OpeningMode::LastPickedDir => self
3194                .storage
3195                .last_picked_dir
3196                .as_deref()
3197                .unwrap_or(&self.config.initial_directory),
3198        };
3199
3200        let mut path = self.canonicalize_path(path);
3201
3202        if self.config.file_system.is_file(&path) {
3203            if let Some(parent) = path.parent() {
3204                path = parent.to_path_buf();
3205            }
3206        }
3207
3208        path
3209    }
3210
3211    /// Gets the currently open directory.
3212    fn current_directory(&self) -> Option<&Path> {
3213        if let Some(x) = self.directory_stack.iter().nth_back(self.directory_offset) {
3214            return Some(x.as_path());
3215        }
3216
3217        None
3218    }
3219
3220    /// Checks whether the selection or the file name entered is valid.
3221    /// What is checked depends on the mode the dialog is currently in.
3222    fn is_selection_valid(&self) -> bool {
3223        match &self.mode {
3224            DialogMode::PickDirectory => self
3225                .selected_item
3226                .as_ref()
3227                .is_some_and(crate::DirectoryEntry::is_dir),
3228            DialogMode::PickFile => self
3229                .selected_item
3230                .as_ref()
3231                .is_some_and(DirectoryEntry::is_file),
3232            DialogMode::PickMultiple => self.get_dir_content_filtered_iter().any(|p| p.selected),
3233            DialogMode::SaveFile => self.file_name_input_error.is_none(),
3234        }
3235    }
3236
3237    /// Validates the file name entered by the user.
3238    ///
3239    /// Returns None if the file name is valid. Otherwise returns an error message.
3240    fn validate_file_name_input(&self) -> Option<String> {
3241        if self.file_name_input.is_empty() {
3242            return Some(self.config.labels.err_empty_file_name.clone());
3243        }
3244
3245        if let Some(x) = self.current_directory() {
3246            let mut full_path = x.to_path_buf();
3247            full_path.push(self.file_name_input.as_str());
3248
3249            if self.config.file_system.is_dir(&full_path) {
3250                return Some(self.config.labels.err_directory_exists.clone());
3251            }
3252
3253            if !self.config.allow_file_overwrite && self.config.file_system.is_file(&full_path) {
3254                return Some(self.config.labels.err_file_exists.clone());
3255            }
3256        } else {
3257            // There is most likely a bug in the code if we get this error message!
3258            return Some("Currently not in a directory".to_string());
3259        }
3260
3261        None
3262    }
3263
3264    /// Marks the given item as the selected directory item.
3265    /// Also updates the `file_name_input` to the name of the selected item.
3266    fn select_item(&mut self, item: &mut DirectoryEntry) {
3267        if self.mode == DialogMode::PickMultiple {
3268            item.selected = true;
3269        }
3270        self.selected_item = Some(item.clone());
3271
3272        if self.mode == DialogMode::SaveFile && item.is_file() {
3273            self.file_name_input = item.file_name().to_string();
3274            self.file_name_input_error = self.validate_file_name_input();
3275        }
3276    }
3277
3278    /// Attempts to select the last visible item in `directory_content` before the specified item.
3279    ///
3280    /// Returns true if an item is found and selected.
3281    /// Returns false if no visible item is found before the specified item.
3282    fn select_next_visible_item_before(&mut self, item: &DirectoryEntry) -> bool {
3283        let mut return_val = false;
3284
3285        self.directory_content.reset_multi_selection();
3286
3287        let mut directory_content = std::mem::take(&mut self.directory_content);
3288        let search_value = std::mem::take(&mut self.search_value);
3289
3290        let index = directory_content
3291            .filtered_iter(&search_value)
3292            .position(|p| p.path_eq(item));
3293
3294        if let Some(index) = index {
3295            if index != 0 {
3296                if let Some(item) = directory_content
3297                    .filtered_iter_mut(&search_value)
3298                    .nth(index.saturating_sub(1))
3299                {
3300                    self.select_item(item);
3301                    self.scroll_to_selection = true;
3302                    return_val = true;
3303                }
3304            }
3305        }
3306
3307        self.directory_content = directory_content;
3308        self.search_value = search_value;
3309
3310        return_val
3311    }
3312
3313    /// Attempts to select the last visible item in `directory_content` after the specified item.
3314    ///
3315    /// Returns true if an item is found and selected.
3316    /// Returns false if no visible item is found after the specified item.
3317    fn select_next_visible_item_after(&mut self, item: &DirectoryEntry) -> bool {
3318        let mut return_val = false;
3319
3320        self.directory_content.reset_multi_selection();
3321
3322        let mut directory_content = std::mem::take(&mut self.directory_content);
3323        let search_value = std::mem::take(&mut self.search_value);
3324
3325        let index = directory_content
3326            .filtered_iter(&search_value)
3327            .position(|p| p.path_eq(item));
3328
3329        if let Some(index) = index {
3330            if let Some(item) = directory_content
3331                .filtered_iter_mut(&search_value)
3332                .nth(index.saturating_add(1))
3333            {
3334                self.select_item(item);
3335                self.scroll_to_selection = true;
3336                return_val = true;
3337            }
3338        }
3339
3340        self.directory_content = directory_content;
3341        self.search_value = search_value;
3342
3343        return_val
3344    }
3345
3346    /// Tries to select the first visible item inside `directory_content`.
3347    fn select_first_visible_item(&mut self) {
3348        self.directory_content.reset_multi_selection();
3349
3350        let mut directory_content = std::mem::take(&mut self.directory_content);
3351
3352        if let Some(item) = directory_content
3353            .filtered_iter_mut(&self.search_value.clone())
3354            .next()
3355        {
3356            self.select_item(item);
3357            self.scroll_to_selection = true;
3358        }
3359
3360        self.directory_content = directory_content;
3361    }
3362
3363    /// Tries to select the last visible item inside `directory_content`.
3364    fn select_last_visible_item(&mut self) {
3365        self.directory_content.reset_multi_selection();
3366
3367        let mut directory_content = std::mem::take(&mut self.directory_content);
3368
3369        if let Some(item) = directory_content
3370            .filtered_iter_mut(&self.search_value.clone())
3371            .last()
3372        {
3373            self.select_item(item);
3374            self.scroll_to_selection = true;
3375        }
3376
3377        self.directory_content = directory_content;
3378    }
3379
3380    /// Opens the text field in the top panel to text edit the current path.
3381    fn open_path_edit(&mut self) {
3382        let path = self.current_directory().map_or_else(String::new, |path| {
3383            path.to_str().unwrap_or_default().to_string()
3384        });
3385
3386        self.path_edit_value = path;
3387        self.path_edit_activate = true;
3388        self.path_edit_visible = true;
3389    }
3390
3391    /// Loads the directory from the path text edit.
3392    fn submit_path_edit(&mut self) {
3393        self.close_path_edit();
3394
3395        let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value));
3396
3397        if self.mode == DialogMode::PickFile && self.config.file_system.is_file(&path) {
3398            self.state = DialogState::Picked(path);
3399            return;
3400        }
3401
3402        // Assume the user wants to save the given path when
3403        //   - an extension to the file name is given or the path
3404        //     edit is allowed to save a file without extension,
3405        //   - the path is not an existing directory,
3406        //   - and the parent directory exists
3407        // Otherwise we will assume the user wants to open the path as a directory.
3408        if self.mode == DialogMode::SaveFile
3409            && (path.extension().is_some()
3410                || self.config.allow_path_edit_to_save_file_without_extension)
3411            && !self.config.file_system.is_dir(&path)
3412            && path.parent().is_some_and(std::path::Path::exists)
3413        {
3414            self.submit_save_file(path);
3415            return;
3416        }
3417
3418        self.load_directory(&path);
3419    }
3420
3421    /// Closes the text field at the top to edit the current path without loading
3422    /// the entered directory.
3423    const fn close_path_edit(&mut self) {
3424        self.path_edit_visible = false;
3425    }
3426
3427    /// Loads the next directory in the `directory_stack`.
3428    /// If `directory_offset` is 0 and there is no other directory to load, `Ok()` is returned and
3429    /// nothing changes.
3430    /// Otherwise, the result of the directory loading operation is returned.
3431    fn load_next_directory(&mut self) {
3432        if self.directory_offset == 0 {
3433            // There is no next directory that can be loaded
3434            return;
3435        }
3436
3437        self.directory_offset -= 1;
3438
3439        // Copy path and load directory
3440        if let Some(path) = self.current_directory() {
3441            self.load_directory_content(path.to_path_buf().as_path());
3442        }
3443    }
3444
3445    /// Loads the previous directory the user opened.
3446    /// If there is no previous directory left, `Ok()` is returned and nothing changes.
3447    /// Otherwise, the result of the directory loading operation is returned.
3448    fn load_previous_directory(&mut self) {
3449        if self.directory_offset + 1 >= self.directory_stack.len() {
3450            // There is no previous directory that can be loaded
3451            return;
3452        }
3453
3454        self.directory_offset += 1;
3455
3456        // Copy path and load directory
3457        if let Some(path) = self.current_directory() {
3458            self.load_directory_content(path.to_path_buf().as_path());
3459        }
3460    }
3461
3462    /// Loads the parent directory of the currently open directory.
3463    /// If the directory doesn't have a parent, `Ok()` is returned and nothing changes.
3464    /// Otherwise, the result of the directory loading operation is returned.
3465    fn load_parent_directory(&mut self) {
3466        if let Some(x) = self.current_directory() {
3467            if let Some(x) = x.to_path_buf().parent() {
3468                self.load_directory(x);
3469            }
3470        }
3471    }
3472
3473    /// Reloads the currently open directory.
3474    /// If no directory is currently open, `Ok()` will be returned.
3475    /// Otherwise, the result of the directory loading operation is returned.
3476    ///
3477    /// In most cases, this function should not be called directly.
3478    /// Instead, `refresh` should be used to reload all other data like system disks too.
3479    fn reload_directory(&mut self) {
3480        if let Some(x) = self.current_directory() {
3481            self.load_directory_content(x.to_path_buf().as_path());
3482        }
3483    }
3484
3485    /// Loads the given directory and updates the `directory_stack`.
3486    /// The function deletes all directories from the `directory_stack` that are currently
3487    /// stored in the vector before the `directory_offset`.
3488    ///
3489    /// The function also sets the loaded directory as the selected item.
3490    fn load_directory(&mut self, path: &Path) {
3491        // Do not load the same directory again.
3492        // Use reload_directory if the content of the directory should be updated.
3493        if let Some(x) = self.current_directory() {
3494            if x == path {
3495                return;
3496            }
3497        }
3498
3499        if self.directory_offset != 0 && self.directory_stack.len() > self.directory_offset {
3500            self.directory_stack
3501                .drain(self.directory_stack.len() - self.directory_offset..);
3502        }
3503
3504        self.directory_stack.push(path.to_path_buf());
3505        self.directory_offset = 0;
3506
3507        self.load_directory_content(path);
3508
3509        // Clear the entry filter buffer.
3510        // It's unlikely the user wants to keep the current filter when entering a new directory.
3511        self.search_value.clear();
3512    }
3513
3514    /// Loads the directory content of the given path.
3515    fn load_directory_content(&mut self, path: &Path) {
3516        self.storage.last_visited_dir = Some(path.to_path_buf());
3517
3518        let selected_file_filter = match self.mode {
3519            DialogMode::PickFile | DialogMode::PickMultiple => self.get_selected_file_filter(),
3520            _ => None,
3521        };
3522
3523        let selected_save_extension = if self.mode == DialogMode::SaveFile {
3524            self.get_selected_save_extension()
3525                .map(|e| e.file_extension.as_str())
3526        } else {
3527            None
3528        };
3529
3530        let filter = DirectoryFilter {
3531            show_files: self.show_files,
3532            show_hidden: self.storage.show_hidden,
3533            show_system_files: self.storage.show_system_files,
3534            file_filter: selected_file_filter.cloned(),
3535            filter_extension: selected_save_extension.map(str::to_string),
3536        };
3537
3538        self.directory_content = DirectoryContent::from_path(
3539            &self.config,
3540            path,
3541            self.config.file_system.clone(),
3542            filter,
3543        );
3544
3545        self.create_directory_dialog.close();
3546        self.scroll_to_selection = true;
3547
3548        if self.mode == DialogMode::SaveFile {
3549            self.file_name_input_error = self.validate_file_name_input();
3550        }
3551    }
3552}