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