Skip to main content

egui_file_dialog/
file_dialog.rs

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