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                if let Some(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.as_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
1379        // Update GUI if we dropped a file
1380        if repaint {
1381            ctx.request_repaint();
1382        }
1383    }
1384
1385    /// Updates the main modal background of the file dialog window.
1386    fn ui_update_modal_background(&self, ctx: &egui::Context) -> egui::InnerResponse<()> {
1387        egui::Area::new(self.window_id.with("modal_overlay"))
1388            .interactable(true)
1389            .fixed_pos(egui::Pos2::ZERO)
1390            .show(ctx, |ui| {
1391                let content_rect = ctx.input(egui::InputState::content_rect);
1392
1393                ui.allocate_response(content_rect.size(), egui::Sense::click());
1394
1395                ui.painter().rect_filled(
1396                    content_rect,
1397                    egui::CornerRadius::ZERO,
1398                    self.config.modal_overlay_color,
1399                );
1400            })
1401    }
1402
1403    fn ui_update_modals(&mut self, ui: &mut egui::Ui) {
1404        // Currently, a rendering error occurs when only a single central panel is rendered
1405        // inside a window. Therefore, when rendering a modal, we render an invisible bottom panel,
1406        // which prevents the error.
1407        // This is currently a bit hacky and should be adjusted again in the future.
1408        egui::Panel::bottom(self.window_id.with("modal_bottom_panel"))
1409            .resizable(false)
1410            .show_separator_line(false)
1411            .show(ui, |_| {});
1412
1413        // We need to use a central panel for the modals so that the
1414        // window doesn't resize to the size of the modal.
1415        egui::CentralPanel::default().show(ui, |ui| {
1416            if let Some(modal) = self.modals.last_mut() {
1417                #[allow(clippy::single_match)]
1418                match modal.update(&self.config, ui) {
1419                    ModalState::Close(action) => {
1420                        self.exec_modal_action(action);
1421                        self.modals.pop();
1422                    }
1423                    ModalState::Pending => {}
1424                }
1425            }
1426        });
1427    }
1428
1429    /// Creates a new egui window with the configured options.
1430    fn create_window<'a>(&self, is_open: &'a mut bool) -> egui::Window<'a> {
1431        let mut window = egui::Window::new(self.get_window_title())
1432            .id(self.window_id)
1433            .open(is_open)
1434            .default_size(self.config.default_size)
1435            .min_size(self.config.min_size)
1436            .resizable(self.config.resizable)
1437            .movable(self.config.movable)
1438            .title_bar(self.config.title_bar)
1439            .collapsible(false);
1440
1441        if let Some(pos) = self.config.default_pos {
1442            window = window.default_pos(pos);
1443        }
1444
1445        if let Some(pos) = self.config.fixed_pos {
1446            window = window.fixed_pos(pos);
1447        }
1448
1449        if let Some((anchor, offset)) = self.config.anchor {
1450            window = window.anchor(anchor, offset);
1451        }
1452
1453        if let Some(size) = self.config.max_size {
1454            window = window.max_size(size);
1455        }
1456
1457        window
1458    }
1459
1460    /// Gets the window title to use.
1461    /// This is either one of the default window titles or the configured window title.
1462    const fn get_window_title(&self) -> &String {
1463        match &self.config.title {
1464            Some(title) => title,
1465            None => match &self.mode {
1466                DialogMode::PickDirectory => &self.config.labels.title_select_directory,
1467                DialogMode::PickFile => &self.config.labels.title_select_file,
1468                DialogMode::PickMultiple => &self.config.labels.title_select_multiple,
1469                DialogMode::SaveFile => &self.config.labels.title_save_file,
1470            },
1471        }
1472    }
1473
1474    /// Updates the top panel of the dialog. Including the navigation buttons,
1475    /// the current path display, the reload button and the search field.
1476    fn ui_update_top_panel(&mut self, ui: &mut egui::Ui) {
1477        const STROKE_INNER_MARGIN: i8 = 5;
1478
1479        let text_height = ui.text_style_height(&egui::TextStyle::Body);
1480        let mut button_height = ui.spacing().button_padding.y.mul_add(2.0, text_height);
1481
1482        if button_height < 22.0 {
1483            button_height = 22.0;
1484        }
1485
1486        let content_height = f32::from(STROKE_INNER_MARGIN).mul_add(2.0, button_height);
1487        let square_button_size = egui::Vec2::new(button_height, button_height).mul(1.08);
1488
1489        ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1490            self.ui_update_nav_buttons(ui, square_button_size, content_height);
1491
1492            let mut path_display_width = ui.available_width();
1493
1494            // Leave some space for the menu button
1495            if self.config.show_reload_button {
1496                path_display_width -= ui
1497                    .spacing()
1498                    .item_spacing
1499                    .x
1500                    .mul_add(2.0, square_button_size.x);
1501            }
1502
1503            // Leave some space for the search input
1504            if self.config.show_search {
1505                path_display_width -= 140.0;
1506            }
1507
1508            if path_display_width < 100.0 {
1509                path_display_width = 100.0;
1510            }
1511
1512            if self.config.show_current_path {
1513                self.ui_update_current_path(
1514                    ui,
1515                    path_display_width,
1516                    STROKE_INNER_MARGIN,
1517                    button_height,
1518                );
1519            }
1520
1521            let hamburger_menu_contains_items = self.config.show_reload_button
1522                || self.config.show_working_directory_button
1523                || self.config.show_select_all_button
1524                || self.config.show_hidden_option
1525                || self.config.show_system_files_option;
1526
1527            let hamburger_menu_visible =
1528                self.config.show_menu_button && hamburger_menu_contains_items;
1529
1530            if hamburger_menu_visible {
1531                self.ui_update_hamburger_menu(ui, square_button_size, content_height);
1532            }
1533
1534            if self.config.show_search {
1535                self.ui_update_search(ui, STROKE_INNER_MARGIN, button_height);
1536            }
1537        });
1538    }
1539
1540    fn ui_update_nav_buttons(
1541        &mut self,
1542        ui: &mut egui::Ui,
1543        button_size: egui::Vec2,
1544        content_height: f32,
1545    ) {
1546        ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
1547            // Add some space so the buttons are in the center of the top panel.
1548            ui.add_space((content_height - button_size.y) / 2.0);
1549
1550            ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1551                self.ui_update_nav_buttons_content(ui, button_size);
1552            });
1553        });
1554    }
1555
1556    fn ui_update_nav_buttons_content(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
1557        if self.config.show_parent_button {
1558            if let Some(x) = self.current_directory() {
1559                if self.ui_button_sized(
1560                    ui,
1561                    x.parent().is_some(),
1562                    button_size,
1563                    self.config.parent_directory_icon.as_str(),
1564                    None,
1565                ) {
1566                    self.load_parent_directory();
1567                }
1568            } else {
1569                let _ = self.ui_button_sized(
1570                    ui,
1571                    false,
1572                    button_size,
1573                    self.config.parent_directory_icon.as_str(),
1574                    None,
1575                );
1576            }
1577        }
1578
1579        if self.config.show_back_button
1580            && self.ui_button_sized(
1581                ui,
1582                self.directory_offset + 1 < self.directory_stack.len(),
1583                button_size,
1584                self.config.back_icon.as_str(),
1585                None,
1586            )
1587        {
1588            self.load_previous_directory();
1589        }
1590
1591        if self.config.show_forward_button
1592            && self.ui_button_sized(
1593                ui,
1594                self.directory_offset != 0,
1595                button_size,
1596                self.config.forward_icon.as_str(),
1597                None,
1598            )
1599        {
1600            self.load_next_directory();
1601        }
1602
1603        if self.config.show_new_folder_button
1604            && self.ui_button_sized(
1605                ui,
1606                !self.create_directory_dialog.is_open(),
1607                button_size,
1608                self.config.new_folder_icon.as_str(),
1609                None,
1610            )
1611        {
1612            self.open_new_folder_dialog();
1613        }
1614    }
1615
1616    /// Updates the view to display the current path.
1617    /// This could be the view for displaying the current path and the individual sections,
1618    /// as well as the view for text editing of the current path.
1619    fn ui_update_current_path(
1620        &mut self,
1621        ui: &mut egui::Ui,
1622        width: f32,
1623        frame_inner_margin: i8,
1624        button_height: f32,
1625    ) {
1626        let stroke = egui::Stroke::new(1.0, ui.style().visuals.window_stroke.color);
1627
1628        egui::Frame::default()
1629            .stroke(stroke)
1630            .inner_margin(egui::Margin::same(frame_inner_margin - 1))
1631            .corner_radius(egui::CornerRadius::from(4))
1632            .show(ui, |ui| {
1633                if self.path_edit_visible {
1634                    self.ui_update_path_edit(ui, width, button_height);
1635                } else {
1636                    self.ui_update_path_display(ui, width, button_height);
1637                }
1638            });
1639    }
1640
1641    /// Updates the view when the currently open path with the individual sections is displayed.
1642    fn ui_update_path_display(&mut self, ui: &mut egui::Ui, mut width: f32, button_height: f32) {
1643        ui.style_mut().always_scroll_the_only_direction = true;
1644        ui.style_mut().spacing.scroll.bar_width = 8.0;
1645
1646        let edit_button_size = egui::Vec2::new(button_height, button_height);
1647
1648        // Leave some space for the edit button
1649        if self.config.show_path_edit_button {
1650            width -= ui.spacing().item_spacing.x.mul_add(2.0, edit_button_size.x);
1651        }
1652
1653        egui::ScrollArea::horizontal()
1654            .auto_shrink([false, true])
1655            .stick_to_right(true)
1656            .max_width(width)
1657            .content_margin(egui::Margin::ZERO)
1658            .show(ui, |ui| {
1659                ui.horizontal(|ui| {
1660                    ui.style_mut().spacing.item_spacing.x /= 2.5;
1661
1662                    let mut path = PathBuf::new();
1663
1664                    if let Some(data) = self.current_directory().map(Path::to_path_buf) {
1665                        for (i, segment) in data.iter().enumerate() {
1666                            path.push(segment);
1667
1668                            let mut segment_str = segment.to_str().unwrap_or_default().to_string();
1669
1670                            if self.is_pinned(&path) {
1671                                segment_str =
1672                                    format!("{} {}", &self.config.pinned_icon, segment_str);
1673                            }
1674
1675                            if i != 0 {
1676                                ui.label(self.config.directory_separator.as_str());
1677                            }
1678
1679                            let btn = egui::Button::new(segment_str);
1680                            let re = ui.add_sized(egui::Vec2::new(0.0, button_height), btn);
1681
1682                            if re.clicked() {
1683                                self.load_directory(path.as_path());
1684                                return;
1685                            }
1686
1687                            self.ui_update_central_panel_path_context_menu(&re, &path.clone());
1688                        }
1689                    }
1690                });
1691            });
1692
1693        if !self.config.show_path_edit_button {
1694            return;
1695        }
1696
1697        let button = egui::Button::new(&self.config.path_edit_icon)
1698            .fill(egui::Color32::TRANSPARENT)
1699            .wrap();
1700
1701        if ui.add_sized(edit_button_size, button).clicked() {
1702            self.open_path_edit();
1703        }
1704    }
1705
1706    /// Updates the view when the user currently wants to text edit the current path.
1707    fn ui_update_path_edit(&mut self, ui: &mut egui::Ui, mut width: f32, button_height: f32) {
1708        let edit_button_size = egui::Vec2::new(button_height, button_height);
1709        width -= ui.spacing().item_spacing.x.mul_add(2.0, edit_button_size.x);
1710
1711        // Calculate the required margin to fill the entire height
1712        let empty_space = button_height - ui.text_style_height(&egui::TextStyle::Body);
1713        let padding_top_bottom = empty_space / 2.0;
1714        #[allow(clippy::cast_possible_truncation)]
1715        let margin = egui::Margin::symmetric(4, padding_top_bottom.floor() as i8);
1716
1717        let frame = egui::Frame::dark_canvas(ui.style())
1718            .inner_margin(margin)
1719            .stroke(egui::Stroke::NONE);
1720
1721        let text_edit = egui::TextEdit::singleline(&mut self.path_edit_value)
1722            .desired_width(width)
1723            .frame(frame);
1724
1725        let response = text_edit.show(ui).response;
1726
1727        if self.path_edit_activate {
1728            response.request_focus();
1729            Self::set_cursor_to_end(&response, &self.path_edit_value);
1730            self.path_edit_activate = false;
1731        }
1732
1733        if self.path_edit_request_focus {
1734            response.request_focus();
1735            self.path_edit_request_focus = false;
1736        }
1737
1738        let btn = egui::Button::new("✔").wrap();
1739        let btn_response = ui.add_sized(edit_button_size, btn);
1740
1741        if btn_response.clicked() {
1742            self.submit_path_edit();
1743        }
1744
1745        if !response.has_focus() && !btn_response.contains_pointer() {
1746            self.path_edit_visible = false;
1747        }
1748    }
1749
1750    /// Updates the hamburger menu containing different options.
1751    fn ui_update_hamburger_menu(
1752        &mut self,
1753        ui: &mut egui::Ui,
1754        button_size: egui::Vec2,
1755        content_height: f32,
1756    ) {
1757        use egui::containers::menu::{is_in_menu, MenuButton, SubMenuButton};
1758
1759        ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
1760            // Add some space so the button is placed in the center of the top panel.
1761            ui.add_space((content_height - button_size.y) / 2.0);
1762
1763            ui.horizontal(|ui| {
1764                // TODO: min_size is not correct, we should set the exact size of the button.
1765                //   The build-in menu buttons seem to be a bit limit regarding custom sizes.
1766                let btn = egui::Button::new(&self.config.menu_icon).min_size(button_size);
1767
1768                if is_in_menu(ui) {
1769                    SubMenuButton::new(&self.config.menu_icon).ui(ui, |ui| {
1770                        self.ui_update_hamburger_menu_content(ui);
1771                    });
1772                } else {
1773                    MenuButton::from_button(btn).ui(ui, |ui| {
1774                        self.ui_update_hamburger_menu_content(ui);
1775                    });
1776                }
1777            });
1778        });
1779    }
1780
1781    /// Updates the contents of the hamburger menu when it is open.
1782    fn ui_update_hamburger_menu_content(&mut self, ui: &mut egui::Ui) {
1783        const SEPARATOR_SPACING: f32 = 2.0;
1784
1785        let working_dir = self.config.file_system.current_dir();
1786
1787        let show_reload = self.config.show_reload_button;
1788        let show_working_dir = self.config.show_working_directory_button && working_dir.is_ok();
1789        let show_select_all =
1790            self.config.show_select_all_button && self.mode == DialogMode::PickMultiple;
1791
1792        let show_hidden = self.config.show_hidden_option;
1793        let show_system_files = self.config.show_system_files_option;
1794
1795        if show_reload && ui.button(&self.config.labels.reload).clicked() {
1796            self.refresh();
1797            ui.close();
1798        }
1799
1800        if show_working_dir && ui.button(&self.config.labels.working_directory).clicked() {
1801            self.load_directory(&working_dir.unwrap_or_default());
1802            ui.close();
1803        }
1804
1805        if show_select_all && ui.button(&self.config.labels.select_all).clicked() {
1806            self.select_all_items();
1807            ui.close();
1808        }
1809
1810        let any_above = show_reload || show_working_dir || show_select_all;
1811        let any_below = show_hidden || show_system_files;
1812
1813        if any_above && any_below {
1814            ui.add_space(SEPARATOR_SPACING);
1815            ui.separator();
1816            ui.add_space(SEPARATOR_SPACING);
1817        }
1818
1819        if show_hidden
1820            && ui
1821                .checkbox(
1822                    &mut self.storage.show_hidden,
1823                    &self.config.labels.show_hidden,
1824                )
1825                .clicked()
1826        {
1827            self.refresh();
1828            ui.close();
1829        }
1830
1831        if show_system_files
1832            && ui
1833                .checkbox(
1834                    &mut self.storage.show_system_files,
1835                    &self.config.labels.show_system_files,
1836                )
1837                .clicked()
1838        {
1839            self.refresh();
1840            ui.close();
1841        }
1842    }
1843
1844    /// Updates the search input
1845    fn ui_update_search(&mut self, ui: &mut egui::Ui, frame_inner_margin: i8, button_height: f32) {
1846        let stroke = egui::Stroke::new(1.0, ui.style().visuals.window_stroke.color);
1847
1848        let margin = egui::Margin {
1849            top: frame_inner_margin,
1850            bottom: frame_inner_margin,
1851            #[allow(clippy::cast_possible_truncation)]
1852            left: (f32::from(frame_inner_margin) * 1.5).floor() as i8,
1853            right: frame_inner_margin,
1854        };
1855
1856        egui::Frame::default()
1857            .stroke(stroke)
1858            .inner_margin(margin)
1859            .corner_radius(egui::CornerRadius::from(4))
1860            .show(ui, |ui| {
1861                ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1862                    self.ui_update_search_content(ui, button_height);
1863                });
1864            });
1865    }
1866
1867    fn ui_update_search_content(&mut self, ui: &mut egui::Ui, button_height: f32) {
1868        ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
1869            // Add some space so the search icon is in the center
1870            let text_height = ui.text_style_height(&egui::TextStyle::Body);
1871            if text_height <= button_height {
1872                ui.add_space((button_height - text_height) / 2.0);
1873            }
1874
1875            ui.label(&self.config.search_icon);
1876        });
1877
1878        // Calculate the required margin to fill the entire height with the text edit
1879        let empty_space = button_height - ui.text_style_height(&egui::TextStyle::Body);
1880        let padding_top_bottom = empty_space / 2.0;
1881        #[allow(clippy::cast_possible_truncation)]
1882        let margin = egui::Margin::symmetric(4, padding_top_bottom.floor() as i8);
1883
1884        let frame = egui::Frame::dark_canvas(ui.style())
1885            .inner_margin(margin)
1886            .stroke(egui::Stroke::NONE);
1887
1888        let text_edit = egui::TextEdit::singleline(&mut self.search_value)
1889            .desired_width(ui.available_width())
1890            .frame(frame);
1891
1892        let re = text_edit.show(ui).response;
1893
1894        self.edit_search_on_text_input(ui);
1895
1896        if re.changed() || self.init_search {
1897            self.selected_item = None;
1898            self.select_first_visible_item();
1899        }
1900
1901        if self.init_search {
1902            re.request_focus();
1903            Self::set_cursor_to_end(&re, &self.search_value);
1904            self.directory_content.reset_multi_selection();
1905
1906            self.init_search = false;
1907        }
1908    }
1909
1910    /// Focuses and types into the search input, if text input without
1911    /// shortcut modifiers is detected, and no other inputs are focused.
1912    ///
1913    /// # Arguments
1914    ///
1915    /// - `re`: The [`egui::Response`] returned by the filter text edit widget
1916    fn edit_search_on_text_input(&mut self, ui: &egui::Ui) {
1917        if ui.memory(|mem| mem.focused().is_some()) {
1918            return;
1919        }
1920
1921        ui.input(|inp| {
1922            // We stop if any modifier is active besides only shift
1923            if inp.modifiers.any() && !inp.modifiers.shift_only() {
1924                return;
1925            }
1926
1927            // If we find any text input event, we append it to the filter string
1928            // and allow proceeding to activating the filter input widget.
1929            for text in inp.events.iter().filter_map(|ev| match ev {
1930                egui::Event::Text(t) => Some(t),
1931                _ => None,
1932            }) {
1933                self.search_value.push_str(text);
1934                self.init_search = true;
1935            }
1936        });
1937    }
1938
1939    /// Updates the left panel of the dialog. Including the list of the user directories (Places)
1940    /// and system disks (Devices, Removable Devices).
1941    fn ui_update_left_panel(&mut self, ui: &mut egui::Ui) {
1942        ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
1943            // Spacing multiplier used between sections in the left sidebar
1944            const SPACING_MULTIPLIER: f32 = 4.0;
1945
1946            egui::containers::ScrollArea::vertical()
1947                .auto_shrink([false, false])
1948                .show(ui, |ui| {
1949                    // Spacing for the first section in the left sidebar
1950                    let mut spacing = ui.global_style().spacing.item_spacing.y * 2.0;
1951
1952                    // Update paths pinned to the left sidebar by the user
1953                    if self.config.show_pinned_folders && self.ui_update_pinned_folders(ui, spacing)
1954                    {
1955                        spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1956                    }
1957
1958                    // Update custom quick access sections
1959                    let quick_accesses = std::mem::take(&mut self.config.quick_accesses);
1960
1961                    for quick_access in &quick_accesses {
1962                        ui.add_space(spacing);
1963                        self.ui_update_quick_access(ui, quick_access);
1964                        spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1965                    }
1966
1967                    self.config.quick_accesses = quick_accesses;
1968
1969                    // Update native quick access sections
1970                    if self.config.show_places && self.ui_update_user_directories(ui, spacing) {
1971                        spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1972                    }
1973
1974                    let disks = std::mem::take(&mut self.system_disks);
1975
1976                    if self.config.show_devices && self.ui_update_devices(ui, spacing, &disks) {
1977                        spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1978                    }
1979
1980                    if self.config.show_removable_devices
1981                        && self.ui_update_removable_devices(ui, spacing, &disks)
1982                    {
1983                        // Add this when we add a new section after removable devices
1984                        // spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1985                    }
1986
1987                    self.system_disks = disks;
1988                });
1989        });
1990    }
1991
1992    /// Updates a path entry in the left panel.
1993    ///
1994    /// Returns the response of the selectable label.
1995    fn ui_update_left_panel_entry(
1996        &mut self,
1997        ui: &mut egui::Ui,
1998        display_name: &str,
1999        path: &Path,
2000    ) -> egui::Response {
2001        let response = ui.selectable_label(self.current_directory() == Some(path), display_name);
2002
2003        if response.clicked() {
2004            self.load_directory(path);
2005        }
2006
2007        response
2008    }
2009
2010    /// Updates a custom quick access section added to the left panel.
2011    fn ui_update_quick_access(&mut self, ui: &mut egui::Ui, quick_access: &QuickAccess) {
2012        ui.label(&quick_access.heading);
2013
2014        for entry in &quick_access.paths {
2015            self.ui_update_left_panel_entry(ui, &entry.display_name, &entry.path);
2016        }
2017    }
2018
2019    /// Updates the list of pinned folders.
2020    ///
2021    /// Returns true if at least one directory item was included in the list and the
2022    /// heading is visible. If no item was listed, false is returned.
2023    fn ui_update_pinned_folders(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
2024        let mut visible = false;
2025
2026        for (i, pinned) in self.storage.pinned_folders.clone().iter().enumerate() {
2027            if i == 0 {
2028                ui.add_space(spacing);
2029                ui.label(self.config.labels.heading_pinned.as_str());
2030
2031                visible = true;
2032            }
2033
2034            if self.is_pinned_folder_being_renamed(pinned) {
2035                self.ui_update_pinned_folder_rename(ui);
2036                continue;
2037            }
2038
2039            let response = self.ui_update_left_panel_entry(
2040                ui,
2041                &format!("{}  {}", self.config.pinned_icon, &pinned.label),
2042                pinned.path.as_path(),
2043            );
2044
2045            self.ui_update_pinned_folder_context_menu(&response, pinned);
2046        }
2047
2048        visible
2049    }
2050
2051    fn ui_update_pinned_folder_rename(&mut self, ui: &mut egui::Ui) {
2052        if let Some(r) = &mut self.rename_pinned_folder {
2053            let id = self.window_id.with("pinned_folder_rename").with(&r.path);
2054            let mut output = egui::TextEdit::singleline(&mut r.label)
2055                .id(id)
2056                .cursor_at_end(true)
2057                .show(ui);
2058
2059            if self.rename_pinned_folder_request_focus {
2060                output.state.cursor.set_char_range(Some(CCursorRange::two(
2061                    CCursor::new(0),
2062                    CCursor::new(r.label.chars().count()),
2063                )));
2064                output.state.store(ui.ctx(), output.response.id);
2065
2066                output.response.request_focus();
2067
2068                self.rename_pinned_folder_request_focus = false;
2069            }
2070
2071            if output.response.lost_focus() {
2072                self.end_rename_pinned_folder();
2073            }
2074        }
2075    }
2076
2077    fn ui_update_pinned_folder_context_menu(
2078        &mut self,
2079        item: &egui::Response,
2080        pinned: &PinnedFolder,
2081    ) {
2082        item.context_menu(|ui| {
2083            if ui.button(&self.config.labels.unpin_folder).clicked() {
2084                self.unpin_path(&pinned.path);
2085                ui.close();
2086            }
2087
2088            if ui
2089                .button(&self.config.labels.rename_pinned_folder)
2090                .clicked()
2091            {
2092                self.begin_rename_pinned_folder(pinned.clone());
2093                ui.close();
2094            }
2095        });
2096    }
2097
2098    /// Updates the list of user directories (Places).
2099    ///
2100    /// Returns true if at least one directory was included in the list and the
2101    /// heading is visible. If no directory was listed, false is returned.
2102    fn ui_update_user_directories(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
2103        // Take temporary ownership of the user directories and configuration.
2104        // This is done so that we don't have to clone the user directories and
2105        // configured display names.
2106        let user_directories = std::mem::take(&mut self.user_directories);
2107        let labels = std::mem::take(&mut self.config.labels);
2108
2109        let visible = if let Some(dirs) = &user_directories {
2110            ui.add_space(spacing);
2111            ui.label(labels.heading_places.as_str());
2112
2113            if let Some(path) = dirs.home_dir() {
2114                self.ui_update_left_panel_entry(ui, &labels.home_dir, path);
2115            }
2116            if let Some(path) = dirs.desktop_dir() {
2117                self.ui_update_left_panel_entry(ui, &labels.desktop_dir, path);
2118            }
2119            if let Some(path) = dirs.document_dir() {
2120                self.ui_update_left_panel_entry(ui, &labels.documents_dir, path);
2121            }
2122            if let Some(path) = dirs.download_dir() {
2123                self.ui_update_left_panel_entry(ui, &labels.downloads_dir, path);
2124            }
2125            if let Some(path) = dirs.audio_dir() {
2126                self.ui_update_left_panel_entry(ui, &labels.audio_dir, path);
2127            }
2128            if let Some(path) = dirs.picture_dir() {
2129                self.ui_update_left_panel_entry(ui, &labels.pictures_dir, path);
2130            }
2131            if let Some(path) = dirs.video_dir() {
2132                self.ui_update_left_panel_entry(ui, &labels.videos_dir, path);
2133            }
2134
2135            true
2136        } else {
2137            false
2138        };
2139
2140        self.user_directories = user_directories;
2141        self.config.labels = labels;
2142
2143        visible
2144    }
2145
2146    /// Updates the list of devices like system disks.
2147    ///
2148    /// Returns true if at least one device was included in the list and the
2149    /// heading is visible. If no device was listed, false is returned.
2150    fn ui_update_devices(&mut self, ui: &mut egui::Ui, spacing: f32, disks: &Disks) -> bool {
2151        let mut visible = false;
2152
2153        for (i, disk) in disks.iter().filter(|x| !x.is_removable()).enumerate() {
2154            if i == 0 {
2155                ui.add_space(spacing);
2156                ui.label(self.config.labels.heading_devices.as_str());
2157
2158                visible = true;
2159            }
2160
2161            self.ui_update_device_entry(ui, disk);
2162        }
2163
2164        visible
2165    }
2166
2167    /// Updates the list of removable devices like USB drives.
2168    ///
2169    /// Returns true if at least one device was included in the list and the
2170    /// heading is visible. If no device was listed, false is returned.
2171    fn ui_update_removable_devices(
2172        &mut self,
2173        ui: &mut egui::Ui,
2174        spacing: f32,
2175        disks: &Disks,
2176    ) -> bool {
2177        let mut visible = false;
2178
2179        for (i, disk) in disks.iter().filter(|x| x.is_removable()).enumerate() {
2180            if i == 0 {
2181                ui.add_space(spacing);
2182                ui.label(self.config.labels.heading_removable_devices.as_str());
2183
2184                visible = true;
2185            }
2186
2187            self.ui_update_device_entry(ui, disk);
2188        }
2189
2190        visible
2191    }
2192
2193    /// Updates a device entry of a device list like "Devices" or "Removable Devices".
2194    fn ui_update_device_entry(&mut self, ui: &mut egui::Ui, device: &Disk) {
2195        let label = if device.is_removable() {
2196            format!(
2197                "{}  {}",
2198                self.config.removable_device_icon,
2199                device.display_name()
2200            )
2201        } else {
2202            format!("{}  {}", self.config.device_icon, device.display_name())
2203        };
2204
2205        self.ui_update_left_panel_entry(ui, &label, device.mount_point());
2206    }
2207
2208    /// Updates the bottom panel showing the selected item and main action buttons.
2209    fn ui_update_bottom_panel(&mut self, ui: &mut egui::Ui) {
2210        const BUTTON_HEIGHT: f32 = 20.0;
2211        ui.add_space(5.0);
2212
2213        // Calculate the width of the action buttons
2214        let label_submit_width = match self.mode {
2215            DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2216                Self::calc_text_width(ui, &self.config.labels.open_button)
2217            }
2218            DialogMode::SaveFile => Self::calc_text_width(ui, &self.config.labels.save_button),
2219        };
2220
2221        let mut btn_width = Self::calc_text_width(ui, &self.config.labels.cancel_button);
2222        if label_submit_width > btn_width {
2223            btn_width = label_submit_width;
2224        }
2225
2226        btn_width = ui.spacing().button_padding.x.mul_add(4.0, btn_width);
2227
2228        // The size of the action buttons "cancel" and "open"/"save"
2229        let button_size: egui::Vec2 = egui::Vec2::new(btn_width, BUTTON_HEIGHT);
2230
2231        self.ui_update_selection_preview(ui, button_size);
2232
2233        if self.mode == DialogMode::SaveFile && self.config.save_extensions.is_empty() {
2234            ui.add_space(ui.style().spacing.item_spacing.y);
2235        }
2236
2237        self.ui_update_action_buttons(ui, button_size);
2238    }
2239
2240    /// Updates the selection preview like "Selected directory: X" as well as the
2241    /// filter selection next to it.
2242    fn ui_update_selection_preview(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2243        const SELECTION_PREVIEW_MIN_WIDTH: f32 = 50.0;
2244        let item_spacing = ui.style().spacing.item_spacing;
2245
2246        let render_filter_selection = (!self.config.file_filters.is_empty()
2247            && (self.mode == DialogMode::PickFile || self.mode == DialogMode::PickMultiple))
2248            || (!self.config.save_extensions.is_empty() && self.mode == DialogMode::SaveFile);
2249
2250        let filter_selection_width = button_size.x.mul_add(2.0, item_spacing.y);
2251        let mut filter_selection_separate_line = false;
2252
2253        ui.horizontal(|ui| {
2254            match &self.mode {
2255                DialogMode::PickDirectory => ui.label(&self.config.labels.selected_directory),
2256                DialogMode::PickFile => ui.label(&self.config.labels.selected_file),
2257                DialogMode::PickMultiple => ui.label(&self.config.labels.selected_items),
2258                DialogMode::SaveFile => ui.label(&self.config.labels.file_name),
2259            };
2260
2261            // Make sure there is enough width for the selection preview. If the available
2262            // width is not enough, render the drop-down menu to select a file filter or
2263            // save extension on a separate line and give the selection preview
2264            // the entire available width.
2265            let mut scroll_bar_width: f32 =
2266                ui.available_width() - filter_selection_width - item_spacing.x;
2267
2268            if scroll_bar_width < SELECTION_PREVIEW_MIN_WIDTH || !render_filter_selection {
2269                filter_selection_separate_line = true;
2270                scroll_bar_width = ui.available_width();
2271            }
2272
2273            match &self.mode {
2274                DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2275                    use egui::containers::scroll_area::ScrollBarVisibility;
2276
2277                    let text = self.get_selection_preview_text();
2278
2279                    egui::containers::ScrollArea::horizontal()
2280                        .auto_shrink([false, false])
2281                        .max_width(scroll_bar_width)
2282                        .stick_to_right(true)
2283                        .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
2284                        .show(ui, |ui| {
2285                            ui.colored_label(ui.style().visuals.selection.bg_fill, text);
2286                        });
2287                }
2288                DialogMode::SaveFile => {
2289                    let mut output = egui::TextEdit::singleline(&mut self.file_name_input)
2290                        .cursor_at_end(false)
2291                        .margin(egui::Margin::symmetric(4, 3))
2292                        .desired_width(scroll_bar_width)
2293                        .show(ui);
2294
2295                    if self.file_name_input_request_focus {
2296                        self.highlight_file_name_input(&mut output);
2297                        output.state.store(ui.ctx(), output.response.id);
2298
2299                        output.response.request_focus();
2300                        self.file_name_input_request_focus = false;
2301                    }
2302
2303                    if output.response.changed() {
2304                        self.file_name_input_error = self.validate_file_name_input();
2305                    }
2306
2307                    if output.response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
2308                    {
2309                        self.submit();
2310                    }
2311                }
2312            }
2313
2314            if !filter_selection_separate_line && render_filter_selection {
2315                if self.mode == DialogMode::SaveFile {
2316                    self.ui_update_save_extension_selection(ui, filter_selection_width);
2317                } else {
2318                    self.ui_update_file_filter_selection(ui, filter_selection_width);
2319                }
2320            }
2321        });
2322
2323        if filter_selection_separate_line && render_filter_selection {
2324            ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2325                if self.mode == DialogMode::SaveFile {
2326                    self.ui_update_save_extension_selection(ui, filter_selection_width);
2327                } else {
2328                    self.ui_update_file_filter_selection(ui, filter_selection_width);
2329                }
2330            });
2331        }
2332    }
2333
2334    /// Highlights the characters inside the file name input until the file extension.
2335    /// Do not forget to store these changes after calling this function:
2336    /// `output.state.store(ui.ctx(), output.response.id);`
2337    fn highlight_file_name_input(&self, output: &mut egui::text_edit::TextEditOutput) {
2338        if let Some(pos) = self.file_name_input.rfind('.') {
2339            let range = if pos == 0 {
2340                CCursorRange::two(CCursor::new(0), CCursor::new(0))
2341            } else {
2342                CCursorRange::two(CCursor::new(0), CCursor::new(pos))
2343            };
2344
2345            output.state.cursor.set_char_range(Some(range));
2346        }
2347    }
2348
2349    fn get_selection_preview_text(&self) -> String {
2350        if self.is_selection_valid() {
2351            match &self.mode {
2352                DialogMode::PickDirectory | DialogMode::PickFile => self
2353                    .selected_item
2354                    .as_ref()
2355                    .map_or_else(String::new, |item| item.file_name().to_string()),
2356                DialogMode::PickMultiple => {
2357                    let mut result = String::new();
2358
2359                    for (i, item) in self
2360                        .get_dir_content_filtered_iter()
2361                        .filter(|p| p.selected)
2362                        .enumerate()
2363                    {
2364                        if i == 0 {
2365                            result += item.file_name();
2366                            continue;
2367                        }
2368
2369                        result += format!(", {}", item.file_name()).as_str();
2370                    }
2371
2372                    result
2373                }
2374                DialogMode::SaveFile => String::new(),
2375            }
2376        } else {
2377            String::new()
2378        }
2379    }
2380
2381    fn ui_update_file_filter_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2382        let selected_filter = self.get_selected_file_filter();
2383        let selected_text = match selected_filter {
2384            Some(f) => &f.name,
2385            None => &self.config.labels.file_filter_all_files,
2386        };
2387
2388        // The item that the user selected inside the drop down.
2389        // If none, the user did not change the selected item this frame.
2390        let mut select_filter: Option<Option<FileFilter>> = None;
2391
2392        egui::containers::ComboBox::from_id_salt(self.window_id.with("file_filter_selection"))
2393            .width(width)
2394            .selected_text(selected_text)
2395            .wrap_mode(egui::TextWrapMode::Truncate)
2396            .show_ui(ui, |ui| {
2397                for filter in &self.config.file_filters {
2398                    let selected = selected_filter.is_some_and(|f| f.id == filter.id);
2399
2400                    if ui.selectable_label(selected, &filter.name).clicked() {
2401                        select_filter = Some(Some(filter.clone()));
2402                    }
2403                }
2404
2405                if self.config.show_all_files_filter
2406                    && ui
2407                        .selectable_label(
2408                            selected_filter.is_none(),
2409                            &self.config.labels.file_filter_all_files,
2410                        )
2411                        .clicked()
2412                {
2413                    select_filter = Some(None);
2414                }
2415            });
2416
2417        if let Some(i) = select_filter {
2418            self.select_file_filter(i);
2419        }
2420    }
2421
2422    fn ui_update_save_extension_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2423        let selected_extension = self.get_selected_save_extension();
2424        let selected_text = match selected_extension {
2425            Some(e) => &e.to_string(),
2426            None => &self.config.labels.save_extension_any,
2427        };
2428
2429        // The item that the user selected inside the drop down.
2430        // If none, the user did not change the selected item this frame.
2431        let mut select_extension: Option<Option<SaveExtension>> = None;
2432
2433        egui::containers::ComboBox::from_id_salt(self.window_id.with("save_extension_selection"))
2434            .width(width)
2435            .selected_text(selected_text)
2436            .wrap_mode(egui::TextWrapMode::Truncate)
2437            .show_ui(ui, |ui| {
2438                for extension in &self.config.save_extensions {
2439                    let selected = selected_extension.is_some_and(|s| s.id == extension.id);
2440
2441                    if ui
2442                        .selectable_label(selected, extension.to_string())
2443                        .clicked()
2444                    {
2445                        select_extension = Some(Some(extension.clone()));
2446                    }
2447                }
2448            });
2449
2450        if let Some(i) = select_extension {
2451            self.file_name_input_request_focus = true;
2452            self.select_save_extension(i);
2453        }
2454    }
2455
2456    /// Updates the action buttons like save, open and cancel
2457    fn ui_update_action_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2458        ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2459            let label = match &self.mode {
2460                DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2461                    self.config.labels.open_button.as_str()
2462                }
2463                DialogMode::SaveFile => self.config.labels.save_button.as_str(),
2464            };
2465
2466            ui.spacing_mut().item_spacing.x = ui.spacing_mut().item_spacing.y;
2467
2468            if self.ui_button_sized(
2469                ui,
2470                self.is_selection_valid(),
2471                button_size,
2472                label,
2473                self.file_name_input_error.as_deref(),
2474            ) {
2475                self.submit();
2476            }
2477
2478            if ui
2479                .add_sized(
2480                    button_size,
2481                    egui::Button::new(self.config.labels.cancel_button.as_str()),
2482                )
2483                .clicked()
2484            {
2485                self.cancel();
2486            }
2487        });
2488    }
2489
2490    /// Updates the central panel. This is either the contents of the directory
2491    /// or the error message when there was an error loading the current directory.
2492    fn ui_update_central_panel(&mut self, ui: &mut egui::Ui) {
2493        if self.update_directory_content(ui) {
2494            return;
2495        }
2496
2497        self.ui_update_central_panel_content(ui);
2498    }
2499
2500    /// Updates the directory content (Not the UI!).
2501    /// This is required because the contents of the directory might be loaded on a
2502    /// separate thread. This function checks the status of the directory content
2503    /// and updates the UI accordingly.
2504    fn update_directory_content(&mut self, ui: &mut egui::Ui) -> bool {
2505        const SHOW_SPINNER_AFTER: f32 = 0.2;
2506
2507        match self.directory_content.update() {
2508            DirectoryContentState::Pending(timestamp) => {
2509                let now = std::time::SystemTime::now();
2510
2511                if now
2512                    .duration_since(*timestamp)
2513                    .unwrap_or_default()
2514                    .as_secs_f32()
2515                    > SHOW_SPINNER_AFTER
2516                {
2517                    ui.centered_and_justified(egui::Ui::spinner);
2518                }
2519
2520                // Prevent egui from not updating the UI when there is no user input
2521                ui.ctx().request_repaint();
2522
2523                true
2524            }
2525            DirectoryContentState::Errored(err) => {
2526                ui.centered_and_justified(|ui| ui.colored_label(ui.visuals().error_fg_color, err));
2527                true
2528            }
2529            DirectoryContentState::Finished => {
2530                if self.mode == DialogMode::PickDirectory {
2531                    if let Some(dir) = self.current_directory() {
2532                        let mut dir_entry =
2533                            DirectoryEntry::from_path(&self.config, dir, &*self.config.file_system);
2534                        self.select_item(&mut dir_entry);
2535                    }
2536                }
2537
2538                false
2539            }
2540            DirectoryContentState::Success => false,
2541        }
2542    }
2543
2544    /// Updates the contents of the currently open directory.
2545    /// TODO: Refactor
2546    fn ui_update_central_panel_content(&mut self, ui: &mut egui::Ui) {
2547        // Temporarily take ownership of the directory content.
2548        let mut data = std::mem::take(&mut self.directory_content);
2549
2550        // Count how many items are currently selected (before the UI loop),
2551        // so we can enforce max_selections limits during interaction.
2552        let mut selected_count = data
2553            .filtered_iter(&self.search_value)
2554            .filter(|item| item.selected)
2555            .count();
2556
2557        // If the multi selection should be reset, excluding the currently
2558        // selected primary item.
2559        let mut reset_multi_selection = false;
2560
2561        // The item the user wants to make a batch selection from.
2562        // The primary selected item is used for item a.
2563        let mut batch_select_item_b: Option<DirectoryEntry> = None;
2564
2565        // If we should return after updating the directory entries.
2566        let mut should_return = false;
2567
2568        ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
2569            let scroll_area = egui::containers::ScrollArea::vertical().auto_shrink([false, false]);
2570
2571            if self.search_value.is_empty()
2572                && !self.create_directory_dialog.is_open()
2573                && !self.scroll_to_selection
2574            {
2575                // Only update visible items when the search value is empty,
2576                // the create directory dialog is closed and we are currently not scrolling
2577                // to the current item.
2578
2579                let row_height = ui
2580                    .spacing()
2581                    .button_padding
2582                    .y
2583                    .mul_add(2.0, ui.text_style_height(&egui::TextStyle::Body));
2584
2585                scroll_area.show_rows(ui, row_height, data.len(), |ui, range| {
2586                    for item in data.iter_range_mut(range) {
2587                        if self.ui_update_central_panel_entry(
2588                            ui,
2589                            item,
2590                            &mut reset_multi_selection,
2591                            &mut batch_select_item_b,
2592                            &mut selected_count,
2593                        ) {
2594                            should_return = true;
2595                        }
2596                    }
2597                });
2598            } else {
2599                // Update each element if the search value is not empty as we apply the
2600                // search value in every frame. We can't use `egui::ScrollArea::show_rows`
2601                // because we don't know how many files the search value applies to.
2602                // We also have to update every item when the create directory dialog is open as
2603                // it's displayed as the last element.
2604                scroll_area.show(ui, |ui| {
2605                    for item in data.filtered_iter_mut(&self.search_value.clone()) {
2606                        if self.ui_update_central_panel_entry(
2607                            ui,
2608                            item,
2609                            &mut reset_multi_selection,
2610                            &mut batch_select_item_b,
2611                            &mut selected_count,
2612                        ) {
2613                            should_return = true;
2614                        }
2615                    }
2616
2617                    if let Some(entry) = self.ui_update_create_directory_dialog(ui) {
2618                        data.push(entry);
2619                    }
2620                });
2621            }
2622        });
2623
2624        if should_return {
2625            return;
2626        }
2627
2628        // Reset the multi selection except the currently selected primary item
2629        if reset_multi_selection {
2630            for item in data.filtered_iter_mut(&self.search_value) {
2631                if let Some(selected_item) = &self.selected_item {
2632                    if selected_item.path_eq(item) {
2633                        continue;
2634                    }
2635                }
2636
2637                item.selected = false;
2638            }
2639        }
2640
2641        // Check if we should perform a batch selection
2642        if let Some(item_b) = batch_select_item_b {
2643            if let Some(item_a) = &self.selected_item {
2644                self.batch_select_between(&mut data, item_a, &item_b);
2645            }
2646        }
2647
2648        self.directory_content = data;
2649        self.scroll_to_selection = false;
2650    }
2651
2652    /// Updates a single directory content entry.
2653    /// TODO: Refactor
2654    fn ui_update_central_panel_entry(
2655        &mut self,
2656        ui: &mut egui::Ui,
2657        item: &mut DirectoryEntry,
2658        reset_multi_selection: &mut bool,
2659        batch_select_item_b: &mut Option<DirectoryEntry>,
2660        selected_count: &mut usize,
2661    ) -> bool {
2662        let file_name = item.file_name();
2663        let primary_selected = self.is_primary_selected(item);
2664        let pinned = self.is_pinned(item.as_path());
2665
2666        let icons = if pinned {
2667            format!("{} {} ", item.icon(), self.config.pinned_icon)
2668        } else {
2669            format!("{} ", item.icon())
2670        };
2671
2672        let icons_width = Self::calc_text_width(ui, &icons);
2673
2674        // Calc available width for the file name and include a small margin
2675        let available_width = ui.available_width() - icons_width - 15.0;
2676
2677        let truncate = self.config.truncate_filenames
2678            && available_width < Self::calc_text_width(ui, file_name);
2679
2680        let text = if truncate {
2681            Self::truncate_filename(ui, item, available_width)
2682        } else {
2683            file_name.to_owned()
2684        };
2685
2686        let mut re =
2687            ui.selectable_label(primary_selected || item.selected, format!("{icons}{text}"));
2688
2689        if truncate {
2690            re = re.on_hover_text(file_name);
2691        }
2692
2693        if item.is_dir() {
2694            self.ui_update_central_panel_path_context_menu(&re, item.as_path());
2695
2696            if re.context_menu_opened() {
2697                self.select_item(item);
2698            }
2699        }
2700
2701        if primary_selected && self.scroll_to_selection {
2702            re.scroll_to_me(Some(egui::Align::Center));
2703            self.scroll_to_selection = false;
2704        }
2705
2706        // The user wants to select the item as the primary selected item
2707        if re.clicked()
2708            && !ui.input(|i| i.modifiers.command)
2709            && !ui.input(|i| i.modifiers.shift_only())
2710        {
2711            self.select_item(item);
2712
2713            // Reset the multi selection except the now primary selected item
2714            if self.mode == DialogMode::PickMultiple {
2715                *reset_multi_selection = true;
2716            }
2717        }
2718
2719        // The user wants to select or unselect the item as part of a
2720        // multi selection
2721        if self.mode == DialogMode::PickMultiple
2722            && re.clicked()
2723            && ui.input(|i| i.modifiers.command)
2724        {
2725            if primary_selected {
2726                // If the clicked item is the primary selected item,
2727                // deselect it and remove it from the multi selection
2728                item.selected = false;
2729                self.selected_item = None;
2730                *selected_count = selected_count.saturating_sub(1);
2731            } else if !item.selected && self.selection_limit_reached_with(*selected_count) {
2732                // Selection limit reached; silently ignore.
2733            } else {
2734                let was_selected = item.selected;
2735                item.selected = !item.selected;
2736
2737                if item.selected {
2738                    *selected_count += 1;
2739                    // If the item was selected, make it the primary selected item
2740                    self.select_item(item);
2741                } else if was_selected {
2742                    *selected_count = selected_count.saturating_sub(1);
2743                }
2744            }
2745        }
2746
2747        // The user wants to select every item between the last selected item
2748        // and the current item
2749        if self.mode == DialogMode::PickMultiple
2750            && re.clicked()
2751            && ui.input(|i| i.modifiers.shift_only())
2752        {
2753            if self.selection_limit_reached_with(*selected_count) && !item.selected {
2754                // Selection limit reached; silently ignore.
2755            } else if let Some(selected_item) = self.selected_item.clone() {
2756                // We perform a batch selection from the item that was
2757                // primarily selected before the user clicked on this item.
2758                *batch_select_item_b = Some(selected_item);
2759
2760                // And now make this item the primary selected item
2761                if !item.selected {
2762                    *selected_count += 1;
2763                }
2764                item.selected = true;
2765                self.select_item(item);
2766            }
2767        }
2768
2769        // The user double clicked on the directory entry.
2770        // Either open the directory or submit the dialog.
2771        if re.double_clicked() && !ui.input(|i| i.modifiers.command) {
2772            if item.is_dir() {
2773                // If a filter is configured, check whether we should navigate
2774                // into the directory or treat it as the picked path instead.
2775                if self.should_open_directory(item.as_path()) {
2776                    self.load_directory(&item.to_path_buf());
2777                    return true;
2778                }
2779                // Fall through to submit the directory as the picked path.
2780            }
2781
2782            self.select_item(item);
2783
2784            self.submit();
2785        }
2786
2787        false
2788    }
2789
2790    fn ui_update_create_directory_dialog(&mut self, ui: &mut egui::Ui) -> Option<DirectoryEntry> {
2791        self.create_directory_dialog
2792            .update(ui, &self.config)
2793            .directory()
2794            .map(|path| self.process_new_folder(&path))
2795    }
2796
2797    /// Selects every item inside the `directory_content` between `item_a` and `item_b`,
2798    /// excluding both given items.
2799    fn batch_select_between(
2800        &self,
2801        directory_content: &mut DirectoryContent,
2802        item_a: &DirectoryEntry,
2803        item_b: &DirectoryEntry,
2804    ) {
2805        // Get the position of item a and item b
2806        let pos_a = directory_content
2807            .filtered_iter(&self.search_value)
2808            .position(|p| p.path_eq(item_a));
2809        let pos_b = directory_content
2810            .filtered_iter(&self.search_value)
2811            .position(|p| p.path_eq(item_b));
2812
2813        // If both items where found inside the directory entry, mark every item between
2814        // them as selected
2815        if let Some(pos_a) = pos_a {
2816            if let Some(pos_b) = pos_b {
2817                if pos_a == pos_b {
2818                    return;
2819                }
2820
2821                // Get the min and max of both positions.
2822                // We will iterate from min to max.
2823                let mut min = pos_a;
2824                let mut max = pos_b;
2825
2826                if min > max {
2827                    min = pos_b;
2828                    max = pos_a;
2829                }
2830
2831                // Count how many items are already selected so we can
2832                // respect the max_selections limit.
2833                let mut current_selected = directory_content
2834                    .filtered_iter(&self.search_value)
2835                    .filter(|item| item.selected)
2836                    .count();
2837
2838                for item in directory_content
2839                    .filtered_iter_mut(&self.search_value)
2840                    .enumerate()
2841                    .filter(|(i, _)| i > &min && i < &max)
2842                    .map(|(_, p)| p)
2843                {
2844                    if self.selection_limit_reached_with(current_selected) {
2845                        break;
2846                    }
2847                    if !item.selected {
2848                        current_selected += 1;
2849                    }
2850                    item.selected = true;
2851                }
2852            }
2853        }
2854    }
2855
2856    /// Helper function to add a sized button that can be enabled or disabled
2857    fn ui_button_sized(
2858        &self,
2859        ui: &mut egui::Ui,
2860        enabled: bool,
2861        size: egui::Vec2,
2862        label: &str,
2863        err_tooltip: Option<&str>,
2864    ) -> bool {
2865        let mut clicked = false;
2866
2867        ui.add_enabled_ui(enabled, |ui| {
2868            let response = ui.add_sized(size, egui::Button::new(label));
2869            clicked = response.clicked();
2870
2871            if let Some(err) = err_tooltip {
2872                response.on_disabled_hover_ui(|ui| {
2873                    ui.horizontal_wrapped(|ui| {
2874                        ui.spacing_mut().item_spacing.x = 0.0;
2875
2876                        ui.colored_label(
2877                            ui.global_style().visuals.error_fg_color,
2878                            format!("{} ", self.config.err_icon),
2879                        );
2880
2881                        ui.label(err);
2882                    });
2883                });
2884            }
2885        });
2886
2887        clicked
2888    }
2889
2890    /// Updates the context menu of a path inside the central panel.
2891    ///
2892    /// # Arguments
2893    ///
2894    /// * `item` - The response of the egui item for which the context menu should be opened.
2895    /// * `path` - The path for which the context menu should be opened.
2896    fn ui_update_central_panel_path_context_menu(&mut self, item: &egui::Response, path: &Path) {
2897        // Path context menus are currently only used for pinned folders.
2898        if !self.config.show_pinned_folders {
2899            return;
2900        }
2901
2902        item.context_menu(|ui| {
2903            let pinned = self.is_pinned(path);
2904
2905            if pinned {
2906                if ui.button(&self.config.labels.unpin_folder).clicked() {
2907                    self.unpin_path(path);
2908                    ui.close();
2909                }
2910            } else if ui.button(&self.config.labels.pin_folder).clicked() {
2911                self.pin_path(path.to_path_buf());
2912                ui.close();
2913            }
2914        });
2915    }
2916
2917    /// Sets the cursor position to the end of a text input field.
2918    ///
2919    /// # Arguments
2920    ///
2921    /// * `re` - response of the text input widget
2922    /// * `data` - buffer holding the text of the input widget
2923    fn set_cursor_to_end(re: &egui::Response, data: &str) {
2924        // Set the cursor to the end of the filter input string
2925        if let Some(mut state) = egui::TextEdit::load_state(&re.ctx, re.id) {
2926            state
2927                .cursor
2928                .set_char_range(Some(CCursorRange::one(CCursor::new(data.len()))));
2929            state.store(&re.ctx, re.id);
2930        }
2931    }
2932
2933    /// Calculates the width of a single char.
2934    fn calc_char_width(ui: &egui::Ui, char: char) -> f32 {
2935        ui.fonts_mut(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char))
2936    }
2937
2938    /// Calculates the width of the specified text using the current font configuration.
2939    /// Does not take new lines or text breaks into account!
2940    fn calc_text_width(ui: &egui::Ui, text: &str) -> f32 {
2941        let mut width = 0.0;
2942
2943        for char in text.chars() {
2944            width += Self::calc_char_width(ui, char);
2945        }
2946
2947        width
2948    }
2949
2950    fn truncate_filename(ui: &egui::Ui, item: &DirectoryEntry, max_length: f32) -> String {
2951        const TRUNCATE_STR: &str = "...";
2952
2953        let path = item.as_path();
2954
2955        let file_stem = if item.is_file() {
2956            path.file_stem().and_then(|f| f.to_str()).unwrap_or("")
2957        } else {
2958            item.file_name()
2959        };
2960
2961        let extension = if item.is_file() {
2962            path.extension().map_or(String::new(), |ext| {
2963                format!(".{}", ext.to_str().unwrap_or(""))
2964            })
2965        } else {
2966            String::new()
2967        };
2968
2969        let extension_width = Self::calc_text_width(ui, &extension);
2970        let reserved = extension_width + Self::calc_text_width(ui, TRUNCATE_STR);
2971
2972        if max_length <= reserved {
2973            return format!("{TRUNCATE_STR}{extension}");
2974        }
2975
2976        let mut width = reserved;
2977        let mut front = String::new();
2978        let mut back = String::new();
2979
2980        for (i, char) in file_stem.chars().enumerate() {
2981            let w = Self::calc_char_width(ui, char);
2982
2983            if width + w > max_length {
2984                break;
2985            }
2986
2987            front.push(char);
2988            width += w;
2989
2990            let back_index = file_stem.len() - i - 1;
2991
2992            if back_index <= i {
2993                break;
2994            }
2995
2996            if let Some(char) = file_stem.chars().nth(back_index) {
2997                let w = Self::calc_char_width(ui, char);
2998
2999                if width + w > max_length {
3000                    break;
3001                }
3002
3003                back.push(char);
3004                width += w;
3005            }
3006        }
3007
3008        format!(
3009            "{front}{TRUNCATE_STR}{}{extension}",
3010            back.chars().rev().collect::<String>()
3011        )
3012    }
3013}
3014
3015/// Keybindings
3016impl FileDialog {
3017    /// Checks whether certain keybindings have been pressed and executes the corresponding actions.
3018    fn update_keybindings(&mut self, ctx: &egui::Context) {
3019        // We don't want to execute keybindings if a modal is currently open.
3020        // The modals implement the keybindings themselves.
3021        if let Some(modal) = self.modals.last_mut() {
3022            modal.update_keybindings(&self.config, ctx);
3023            return;
3024        }
3025
3026        let keybindings = std::mem::take(&mut self.config.keybindings);
3027
3028        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.submit, false) {
3029            self.exec_keybinding_submit();
3030        }
3031
3032        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.cancel, false) {
3033            self.exec_keybinding_cancel();
3034        }
3035
3036        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.parent, true) {
3037            self.load_parent_directory();
3038        }
3039
3040        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.back, true) {
3041            self.load_previous_directory();
3042        }
3043
3044        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.forward, true) {
3045            self.load_next_directory();
3046        }
3047
3048        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.reload, true) {
3049            self.refresh();
3050        }
3051
3052        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.new_folder, true) {
3053            self.open_new_folder_dialog();
3054        }
3055
3056        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.edit_path, true) {
3057            self.open_path_edit();
3058        }
3059
3060        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.home_edit_path, true) {
3061            if let Some(dirs) = &self.user_directories {
3062                if let Some(home) = dirs.home_dir() {
3063                    self.load_directory(home.to_path_buf().as_path());
3064                    self.open_path_edit();
3065                }
3066            }
3067        }
3068
3069        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_up, false) {
3070            self.exec_keybinding_selection_up();
3071
3072            // We want to break out of input fields like search when pressing selection keys
3073            if let Some(id) = ctx.memory(egui::Memory::focused) {
3074                ctx.memory_mut(|w| w.surrender_focus(id));
3075            }
3076        }
3077
3078        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_down, false) {
3079            self.exec_keybinding_selection_down();
3080
3081            // We want to break out of input fields like search when pressing selection keys
3082            if let Some(id) = ctx.memory(egui::Memory::focused) {
3083                ctx.memory_mut(|w| w.surrender_focus(id));
3084            }
3085        }
3086
3087        if FileDialogKeyBindings::any_pressed(ctx, &keybindings.select_all, true)
3088            && self.mode == DialogMode::PickMultiple
3089        {
3090            self.select_all_items();
3091        }
3092
3093        self.config.keybindings = keybindings;
3094    }
3095
3096    /// Executes the action when the keybinding `submit` is pressed.
3097    fn exec_keybinding_submit(&mut self) {
3098        if self.path_edit_visible {
3099            self.submit_path_edit();
3100            return;
3101        }
3102
3103        if self.create_directory_dialog.is_open() {
3104            if let Some(dir) = self.create_directory_dialog.submit().directory() {
3105                self.process_new_folder(&dir);
3106            }
3107            return;
3108        }
3109
3110        if self.any_focused_last_frame {
3111            return;
3112        }
3113
3114        // Check if there is a directory selected we can open
3115        if let Some(item) = &self.selected_item {
3116            // Make sure the selected item is visible inside the directory view.
3117            let is_visible = self
3118                .get_dir_content_filtered_iter()
3119                .any(|p| p.path_eq(item));
3120
3121            if is_visible && item.is_dir() {
3122                self.load_directory(&item.to_path_buf());
3123                return;
3124            }
3125        }
3126
3127        self.submit();
3128    }
3129
3130    /// Executes the action when the keybinding `cancel` is pressed.
3131    fn exec_keybinding_cancel(&mut self) {
3132        // We have to check if the `create_directory_dialog` and `path_edit_visible` is open,
3133        // because egui does not consume pressing the escape key inside a text input.
3134        // So when pressing the escape key inside a text input, the text input is closed
3135        // but the keybindings still register the press on the escape key.
3136        // (Although the keybindings are updated before the UI and they check whether another
3137        //  widget is currently in focus!)
3138        //
3139        // This is practical for us because we can close the path edit and
3140        // the create directory dialog.
3141        // However, this causes problems when the user presses escape in other text
3142        // inputs for which we have no status saved. This would then close the entire file dialog.
3143        // To fix this, we check if any item was focused in the last frame.
3144        //
3145        // Note that this only happens with the escape key and not when the enter key is
3146        // used to close a text input. This is why we don't have to check for the
3147        // dialogs in `exec_keybinding_submit`.
3148
3149        if self.create_directory_dialog.is_open() {
3150            self.create_directory_dialog.close();
3151        } else if self.path_edit_visible {
3152            self.close_path_edit();
3153        } else if !self.any_focused_last_frame {
3154            self.cancel();
3155        }
3156    }
3157
3158    /// Executes the action when the keybinding `selection_up` is pressed.
3159    fn exec_keybinding_selection_up(&mut self) {
3160        if self.directory_content.len() == 0 {
3161            return;
3162        }
3163
3164        self.directory_content.reset_multi_selection();
3165
3166        if let Some(item) = &self.selected_item {
3167            if self.select_next_visible_item_before(&item.clone()) {
3168                return;
3169            }
3170        }
3171
3172        // No item is selected or no more items left.
3173        // Select the last item from the directory content.
3174        self.select_last_visible_item();
3175    }
3176
3177    /// Executes the action when the keybinding `selection_down` is pressed.
3178    fn exec_keybinding_selection_down(&mut self) {
3179        if self.directory_content.len() == 0 {
3180            return;
3181        }
3182
3183        self.directory_content.reset_multi_selection();
3184
3185        if let Some(item) = &self.selected_item {
3186            if self.select_next_visible_item_after(&item.clone()) {
3187                return;
3188            }
3189        }
3190
3191        // No item is selected or no more items left.
3192        // Select the last item from the directory content.
3193        self.select_first_visible_item();
3194    }
3195}
3196
3197/// Implementation
3198impl FileDialog {
3199    /// Get the file filter the user currently selected.
3200    fn get_selected_file_filter(&self) -> Option<&FileFilter> {
3201        self.selected_file_filter
3202            .and_then(|id| self.config.file_filters.iter().find(|p| p.id == id))
3203    }
3204
3205    /// Sets the default file filter to use.
3206    fn set_default_file_filter(&mut self) {
3207        if let Some(name) = &self.config.default_file_filter {
3208            for filter in &self.config.file_filters {
3209                if filter.name == name.as_str() {
3210                    self.selected_file_filter = Some(filter.id);
3211                }
3212            }
3213        }
3214    }
3215
3216    /// Selects the given file filter and applies the appropriate filters.
3217    fn select_file_filter(&mut self, filter: Option<FileFilter>) {
3218        self.selected_file_filter = filter.map(|f| f.id);
3219        self.selected_item = None;
3220        self.refresh();
3221    }
3222
3223    /// Get the save extension the user currently selected.
3224    fn get_selected_save_extension(&self) -> Option<&SaveExtension> {
3225        self.selected_save_extension
3226            .and_then(|id| self.config.save_extensions.iter().find(|p| p.id == id))
3227    }
3228
3229    /// Sets the save extension to use.
3230    fn set_default_save_extension(&mut self) {
3231        let config = std::mem::take(&mut self.config);
3232
3233        if let Some(name) = &config.default_save_extension {
3234            for extension in &config.save_extensions {
3235                if extension.name == name.as_str() {
3236                    self.selected_save_extension = Some(extension.id);
3237                    self.set_file_name_extension(&extension.file_extension);
3238                }
3239            }
3240        }
3241
3242        self.config = config;
3243    }
3244
3245    /// Selects the given save extension.
3246    fn select_save_extension(&mut self, extension: Option<SaveExtension>) {
3247        if let Some(ex) = extension {
3248            self.selected_save_extension = Some(ex.id);
3249            self.set_file_name_extension(&ex.file_extension);
3250        }
3251
3252        self.selected_item = None;
3253        self.refresh();
3254    }
3255
3256    /// Updates the extension of `Self::file_name_input`.
3257    fn set_file_name_extension(&mut self, extension: &str) {
3258        // Prevent `PathBuf::set_extension` to append the file extension when there is
3259        // already one without a file name. For example `.png` would be changed to `.png.txt`
3260        // when using `PathBuf::set_extension`.
3261        let dot_count = self.file_name_input.chars().filter(|c| *c == '.').count();
3262        let use_simple = dot_count == 1 && self.file_name_input.chars().nth(0) == Some('.');
3263
3264        let mut p = PathBuf::from(&self.file_name_input);
3265        if !use_simple && p.set_extension(extension) {
3266            self.file_name_input = p.to_string_lossy().into_owned();
3267        } else {
3268            self.file_name_input = format!(".{extension}");
3269        }
3270    }
3271
3272    /// Gets a filtered iterator of the directory content of this object.
3273    fn get_dir_content_filtered_iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
3274        self.directory_content.filtered_iter(&self.search_value)
3275    }
3276
3277    /// Opens the dialog to create a new folder.
3278    fn open_new_folder_dialog(&mut self) {
3279        if let Some(x) = self.current_directory() {
3280            self.create_directory_dialog.open(x.to_path_buf());
3281        }
3282    }
3283
3284    /// Function that processes a newly created folder.
3285    fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry {
3286        let mut entry =
3287            DirectoryEntry::from_path(&self.config, created_dir, &*self.config.file_system);
3288
3289        self.directory_content.push(entry.clone());
3290
3291        self.select_item(&mut entry);
3292
3293        entry
3294    }
3295
3296    /// Opens a new modal window.
3297    fn open_modal(&mut self, modal: Box<dyn FileDialogModal + Send + Sync>) {
3298        self.modals.push(modal);
3299    }
3300
3301    /// Executes the given modal action.
3302    fn exec_modal_action(&mut self, action: ModalAction) {
3303        match action {
3304            ModalAction::None => {}
3305            ModalAction::SaveFile(path) => self.state = DialogState::Picked(path),
3306        }
3307    }
3308
3309    /// Canonicalizes the specified path if canonicalization is enabled.
3310    /// Returns the input path if an error occurs or canonicalization is disabled.
3311    fn canonicalize_path(&self, path: &Path) -> PathBuf {
3312        if self.config.canonicalize_paths {
3313            dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
3314        } else {
3315            path.to_path_buf()
3316        }
3317    }
3318
3319    /// Pins a path to the left sidebar.
3320    fn pin_path(&mut self, path: PathBuf) {
3321        let pinned = PinnedFolder::from_path(path);
3322        self.storage.pinned_folders.push(pinned);
3323    }
3324
3325    /// Unpins a path from the left sidebar.
3326    fn unpin_path(&mut self, path: &Path) {
3327        self.storage
3328            .pinned_folders
3329            .retain(|p| p.path.as_path() != path);
3330    }
3331
3332    /// Checks if the path is pinned to the left sidebar.
3333    fn is_pinned(&self, path: &Path) -> bool {
3334        self.storage
3335            .pinned_folders
3336            .iter()
3337            .any(|p| p.path.as_path() == path)
3338    }
3339
3340    /// Starts to rename a pinned folder by showing the user a text input field.
3341    fn begin_rename_pinned_folder(&mut self, pinned: PinnedFolder) {
3342        self.rename_pinned_folder = Some(pinned);
3343        self.rename_pinned_folder_request_focus = true;
3344    }
3345
3346    /// Ends the renaming of a pinned folder. This updates the real pinned folder
3347    /// in `FileDialogStorage`.
3348    fn end_rename_pinned_folder(&mut self) {
3349        let renamed = std::mem::take(&mut self.rename_pinned_folder);
3350
3351        if let Some(renamed) = renamed {
3352            let old = self
3353                .storage
3354                .pinned_folders
3355                .iter_mut()
3356                .find(|p| p.path == renamed.path);
3357            if let Some(old) = old {
3358                old.label = renamed.label;
3359            }
3360        }
3361    }
3362
3363    /// Checks if the given pinned folder is currently being renamed.
3364    fn is_pinned_folder_being_renamed(&self, pinned: &PinnedFolder) -> bool {
3365        self.rename_pinned_folder
3366            .as_ref()
3367            .is_some_and(|p| p.path == pinned.path)
3368    }
3369
3370    fn is_primary_selected(&self, item: &DirectoryEntry) -> bool {
3371        self.selected_item.as_ref().is_some_and(|x| x.path_eq(item))
3372    }
3373
3374    /// Resets the dialog to use default values.
3375    /// The user data and configuration variables are retained.
3376    fn reset(&mut self) {
3377        let user_data = std::mem::take(&mut self.user_data);
3378        let storage = self.storage.clone();
3379        let config = self.config.clone();
3380        let selected = self.selected_item.clone();
3381
3382        *self = Self::with_config(config);
3383        if self.config.retain_selected_entry {
3384            self.selected_item = selected;
3385        }
3386        self.storage = storage;
3387        self.user_data = user_data;
3388    }
3389
3390    /// Refreshes the dialog.
3391    /// Including the user directories, system disks and currently open directory.
3392    fn refresh(&mut self) {
3393        self.user_directories = self
3394            .config
3395            .file_system
3396            .user_dirs(self.config.canonicalize_paths);
3397        self.system_disks = self
3398            .config
3399            .file_system
3400            .get_disks(self.config.canonicalize_paths);
3401
3402        self.reload_directory();
3403    }
3404
3405    /// Submits the current selection and tries to finish the dialog, if the selection is valid.
3406    fn submit(&mut self) {
3407        // Make sure the selected item or entered file name is valid.
3408        if !self.is_selection_valid() {
3409            return;
3410        }
3411
3412        self.storage.last_picked_dir = self.current_directory().map(PathBuf::from);
3413
3414        match &self.mode {
3415            DialogMode::PickDirectory | DialogMode::PickFile => {
3416                // Should always contain a value since `is_selection_valid` is used to
3417                // validate the selection.
3418                if let Some(item) = self.selected_item.clone() {
3419                    self.state = DialogState::Picked(item.to_path_buf());
3420                }
3421            }
3422            DialogMode::PickMultiple => {
3423                let result: Vec<PathBuf> = self
3424                    .selected_entries()
3425                    .map(crate::DirectoryEntry::to_path_buf)
3426                    .collect();
3427
3428                self.state = DialogState::PickedMultiple(result);
3429            }
3430            DialogMode::SaveFile => {
3431                // Should always contain a value since `is_selection_valid` is used to
3432                // validate the selection.
3433                if let Some(path) = self.current_directory() {
3434                    let full_path = path.join(&self.file_name_input);
3435                    self.submit_save_file(full_path);
3436                }
3437            }
3438        }
3439    }
3440
3441    /// Submits the file dialog with the specified path and opens the `OverwriteFileModal`
3442    /// if the path already exists.
3443    fn submit_save_file(&mut self, path: PathBuf) {
3444        if path.exists() {
3445            self.open_modal(Box::new(OverwriteFileModal::new(path)));
3446
3447            return;
3448        }
3449
3450        self.state = DialogState::Picked(path);
3451    }
3452
3453    /// Cancels the dialog.
3454    fn cancel(&mut self) {
3455        self.state = DialogState::Cancelled;
3456    }
3457
3458    /// This function generates the initial directory based on the configuration.
3459    /// The function does the following things:
3460    ///   - Get the path to open based on the opening mode
3461    ///   - Canonicalize the path if enabled
3462    ///   - Attempts to use the parent directory if the path is a file
3463    fn get_initial_directory(&self) -> PathBuf {
3464        let path = match self.config.opening_mode {
3465            OpeningMode::AlwaysInitialDir => &self.config.initial_directory,
3466            OpeningMode::LastVisitedDir => self
3467                .storage
3468                .last_visited_dir
3469                .as_deref()
3470                .unwrap_or(&self.config.initial_directory),
3471            OpeningMode::LastPickedDir => self
3472                .storage
3473                .last_picked_dir
3474                .as_deref()
3475                .unwrap_or(&self.config.initial_directory),
3476        };
3477
3478        let mut path = self.canonicalize_path(path);
3479
3480        if self.config.file_system.is_file(&path) {
3481            if let Some(parent) = path.parent() {
3482                path = parent.to_path_buf();
3483            }
3484        }
3485
3486        path
3487    }
3488
3489    /// Gets the currently open directory.
3490    fn current_directory(&self) -> Option<&Path> {
3491        if let Some(x) = self.directory_stack.iter().nth_back(self.directory_offset) {
3492            return Some(x.as_path());
3493        }
3494
3495        None
3496    }
3497
3498    /// Checks whether the selection or the file name entered is valid.
3499    /// What is checked depends on the mode the dialog is currently in.
3500    fn is_selection_valid(&self) -> bool {
3501        match &self.mode {
3502            DialogMode::PickDirectory => self
3503                .selected_item
3504                .as_ref()
3505                .is_some_and(crate::DirectoryEntry::is_dir),
3506            DialogMode::PickFile => self
3507                .selected_item
3508                .as_ref()
3509                .is_some_and(DirectoryEntry::is_file),
3510            DialogMode::PickMultiple => self.get_dir_content_filtered_iter().any(|p| p.selected),
3511            DialogMode::SaveFile => self.file_name_input_error.is_none(),
3512        }
3513    }
3514
3515    /// Validates the file name entered by the user.
3516    ///
3517    /// Returns None if the file name is valid. Otherwise returns an error message.
3518    fn validate_file_name_input(&self) -> Option<String> {
3519        if self.file_name_input.is_empty() {
3520            return Some(self.config.labels.err_empty_file_name.clone());
3521        }
3522
3523        if let Some(x) = self.current_directory() {
3524            let mut full_path = x.to_path_buf();
3525            full_path.push(self.file_name_input.as_str());
3526
3527            if self.config.file_system.is_dir(&full_path) {
3528                return Some(self.config.labels.err_directory_exists.clone());
3529            }
3530
3531            if !self.config.allow_file_overwrite && self.config.file_system.is_file(&full_path) {
3532                return Some(self.config.labels.err_file_exists.clone());
3533            }
3534        } else {
3535            // There is most likely a bug in the code if we get this error message!
3536            return Some("Currently not in a directory".to_string());
3537        }
3538
3539        None
3540    }
3541
3542    /// Marks the given item as the selected directory item.
3543    /// Also updates the `file_name_input` to the name of the selected item.
3544    fn select_item(&mut self, item: &mut DirectoryEntry) {
3545        if self.mode == DialogMode::PickMultiple {
3546            item.selected = true;
3547        }
3548        self.selected_item = Some(item.clone());
3549
3550        if self.mode == DialogMode::SaveFile && item.is_file() {
3551            self.file_name_input = item.file_name().to_string();
3552            self.file_name_input_error = self.validate_file_name_input();
3553        }
3554    }
3555
3556    /// Attempts to select the last visible item in `directory_content` before the specified item.
3557    ///
3558    /// Returns true if an item is found and selected.
3559    /// Returns false if no visible item is found before the specified item.
3560    fn select_next_visible_item_before(&mut self, item: &DirectoryEntry) -> bool {
3561        let mut return_val = false;
3562
3563        self.directory_content.reset_multi_selection();
3564
3565        let mut directory_content = std::mem::take(&mut self.directory_content);
3566        let search_value = std::mem::take(&mut self.search_value);
3567
3568        let index = directory_content
3569            .filtered_iter(&search_value)
3570            .position(|p| p.path_eq(item));
3571
3572        if let Some(index) = index {
3573            if index != 0 {
3574                if let Some(item) = directory_content
3575                    .filtered_iter_mut(&search_value)
3576                    .nth(index.saturating_sub(1))
3577                {
3578                    self.select_item(item);
3579                    self.scroll_to_selection = true;
3580                    return_val = true;
3581                }
3582            }
3583        }
3584
3585        self.directory_content = directory_content;
3586        self.search_value = search_value;
3587
3588        return_val
3589    }
3590
3591    /// Attempts to select the last visible item in `directory_content` after the specified item.
3592    ///
3593    /// Returns true if an item is found and selected.
3594    /// Returns false if no visible item is found after the specified item.
3595    fn select_next_visible_item_after(&mut self, item: &DirectoryEntry) -> bool {
3596        let mut return_val = false;
3597
3598        self.directory_content.reset_multi_selection();
3599
3600        let mut directory_content = std::mem::take(&mut self.directory_content);
3601        let search_value = std::mem::take(&mut self.search_value);
3602
3603        let index = directory_content
3604            .filtered_iter(&search_value)
3605            .position(|p| p.path_eq(item));
3606
3607        if let Some(index) = index {
3608            if let Some(item) = directory_content
3609                .filtered_iter_mut(&search_value)
3610                .nth(index.saturating_add(1))
3611            {
3612                self.select_item(item);
3613                self.scroll_to_selection = true;
3614                return_val = true;
3615            }
3616        }
3617
3618        self.directory_content = directory_content;
3619        self.search_value = search_value;
3620
3621        return_val
3622    }
3623
3624    /// Tries to select the first visible item inside `directory_content`.
3625    fn select_first_visible_item(&mut self) {
3626        self.directory_content.reset_multi_selection();
3627
3628        let mut directory_content = std::mem::take(&mut self.directory_content);
3629
3630        if let Some(item) = directory_content
3631            .filtered_iter_mut(&self.search_value.clone())
3632            .next()
3633        {
3634            self.select_item(item);
3635            self.scroll_to_selection = true;
3636        }
3637
3638        self.directory_content = directory_content;
3639    }
3640
3641    /// Tries to select the last visible item inside `directory_content`.
3642    fn select_last_visible_item(&mut self) {
3643        self.directory_content.reset_multi_selection();
3644
3645        let mut directory_content = std::mem::take(&mut self.directory_content);
3646
3647        if let Some(item) = directory_content
3648            .filtered_iter_mut(&self.search_value.clone())
3649            .last()
3650        {
3651            self.select_item(item);
3652            self.scroll_to_selection = true;
3653        }
3654
3655        self.directory_content = directory_content;
3656    }
3657
3658    /// Returns `true` if `selected_count` has reached or exceeded `max_selections`.
3659    fn selection_limit_reached_with(&self, selected_count: usize) -> bool {
3660        self.config
3661            .max_selections
3662            .is_some_and(|max| selected_count >= max)
3663    }
3664
3665    /// Selects all items in the current directory.
3666    fn select_all_items(&mut self) {
3667        let mut selected_count = self
3668            .directory_content
3669            .filtered_iter(&self.search_value)
3670            .filter(|p| p.selected)
3671            .count();
3672
3673        for item in self.directory_content.filtered_iter_mut(&self.search_value) {
3674            if item.selected {
3675                continue; // already counted
3676            }
3677            if self
3678                .config
3679                .max_selections
3680                .is_some_and(|max| selected_count >= max)
3681            {
3682                break;
3683            }
3684            item.selected = true;
3685            selected_count += 1;
3686        }
3687    }
3688
3689    /// Opens the text field in the top panel to text edit the current path.
3690    fn open_path_edit(&mut self) {
3691        let path = self.current_directory().map_or_else(String::new, |path| {
3692            path.to_str().unwrap_or_default().to_string()
3693        });
3694
3695        self.path_edit_value = path;
3696        self.path_edit_activate = true;
3697        self.path_edit_visible = true;
3698    }
3699
3700    /// Loads the directory from the path text edit.
3701    fn submit_path_edit(&mut self) {
3702        self.close_path_edit();
3703
3704        let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value));
3705
3706        if self.mode == DialogMode::PickFile && self.config.file_system.is_file(&path) {
3707            self.state = DialogState::Picked(path);
3708            return;
3709        }
3710
3711        // Assume the user wants to save the given path when
3712        //   - an extension to the file name is given or the path
3713        //     edit is allowed to save a file without extension,
3714        //   - the path is not an existing directory,
3715        //   - and the parent directory exists
3716        // Otherwise we will assume the user wants to open the path as a directory.
3717        if self.mode == DialogMode::SaveFile
3718            && (path.extension().is_some()
3719                || self.config.allow_path_edit_to_save_file_without_extension)
3720            && !self.config.file_system.is_dir(&path)
3721            && path.parent().is_some_and(std::path::Path::exists)
3722        {
3723            self.submit_save_file(path);
3724            return;
3725        }
3726
3727        self.load_directory(&path);
3728    }
3729
3730    /// Closes the text field at the top to edit the current path without loading
3731    /// the entered directory.
3732    const fn close_path_edit(&mut self) {
3733        self.path_edit_visible = false;
3734    }
3735
3736    /// Loads the next directory in the `directory_stack`.
3737    /// If `directory_offset` is 0 and there is no other directory to load, `Ok()` is returned and
3738    /// nothing changes.
3739    /// Otherwise, the result of the directory loading operation is returned.
3740    fn load_next_directory(&mut self) {
3741        if self.directory_offset == 0 {
3742            // There is no next directory that can be loaded
3743            return;
3744        }
3745
3746        self.directory_offset -= 1;
3747
3748        // Copy path and load directory
3749        if let Some(path) = self.current_directory() {
3750            self.load_directory_content(path.to_path_buf().as_path());
3751        }
3752    }
3753
3754    /// Loads the previous directory the user opened.
3755    /// If there is no previous directory left, `Ok()` is returned and nothing changes.
3756    /// Otherwise, the result of the directory loading operation is returned.
3757    fn load_previous_directory(&mut self) {
3758        if self.directory_offset + 1 >= self.directory_stack.len() {
3759            // There is no previous directory that can be loaded
3760            return;
3761        }
3762
3763        self.directory_offset += 1;
3764
3765        // Copy path and load directory
3766        if let Some(path) = self.current_directory() {
3767            self.load_directory_content(path.to_path_buf().as_path());
3768        }
3769    }
3770
3771    /// Loads the parent directory of the currently open directory.
3772    /// If the directory doesn't have a parent, `Ok()` is returned and nothing changes.
3773    /// Otherwise, the result of the directory loading operation is returned.
3774    fn load_parent_directory(&mut self) {
3775        if let Some(x) = self.current_directory() {
3776            if let Some(x) = x.to_path_buf().parent() {
3777                self.load_directory(x);
3778            }
3779        }
3780    }
3781
3782    /// Reloads the currently open directory.
3783    /// If no directory is currently open, `Ok()` will be returned.
3784    /// Otherwise, the result of the directory loading operation is returned.
3785    ///
3786    /// In most cases, this function should not be called directly.
3787    /// Instead, `refresh` should be used to reload all other data like system disks too.
3788    fn reload_directory(&mut self) {
3789        if let Some(x) = self.current_directory() {
3790            self.load_directory_content(x.to_path_buf().as_path());
3791        }
3792    }
3793
3794    /// Loads the given directory and updates the `directory_stack`.
3795    /// The function deletes all directories from the `directory_stack` that are currently
3796    /// stored in the vector before the `directory_offset`.
3797    ///
3798    /// The function also sets the loaded directory as the selected item.
3799    fn load_directory(&mut self, path: &Path) {
3800        // Do not load the same directory again.
3801        // Use reload_directory if the content of the directory should be updated.
3802        if let Some(x) = self.current_directory() {
3803            if x == path {
3804                return;
3805            }
3806        }
3807
3808        if self.directory_offset != 0 && self.directory_stack.len() > self.directory_offset {
3809            self.directory_stack
3810                .drain(self.directory_stack.len() - self.directory_offset..);
3811        }
3812
3813        self.directory_stack.push(path.to_path_buf());
3814        self.directory_offset = 0;
3815
3816        self.load_directory_content(path);
3817
3818        // Clear the entry filter buffer.
3819        // It's unlikely the user wants to keep the current filter when entering a new directory.
3820        self.search_value.clear();
3821    }
3822
3823    /// Loads the directory content of the given path.
3824    fn load_directory_content(&mut self, path: &Path) {
3825        self.storage.last_visited_dir = Some(path.to_path_buf());
3826
3827        let selected_file_filter = match self.mode {
3828            DialogMode::PickFile | DialogMode::PickMultiple => self.get_selected_file_filter(),
3829            _ => None,
3830        };
3831
3832        let selected_save_extension = if self.mode == DialogMode::SaveFile {
3833            self.get_selected_save_extension()
3834                .map(|e| e.file_extension.as_str())
3835        } else {
3836            None
3837        };
3838
3839        let filter = DirectoryFilter {
3840            show_files: self.show_files,
3841            show_hidden: self.storage.show_hidden,
3842            show_system_files: self.storage.show_system_files,
3843            file_filter: selected_file_filter.cloned(),
3844            filter_extension: selected_save_extension.map(str::to_string),
3845        };
3846
3847        self.directory_content = DirectoryContent::from_path(
3848            &self.config,
3849            path,
3850            self.config.file_system.clone(),
3851            filter,
3852        );
3853
3854        self.create_directory_dialog.close();
3855        self.scroll_to_selection = true;
3856
3857        if self.mode == DialogMode::SaveFile {
3858            self.file_name_input_error = self.validate_file_name_input();
3859        }
3860    }
3861
3862    /// Returns `true` if the given directory should be navigated into,
3863    /// or `false` if it should be submitted as the picked path instead.
3864    /// When no filter is set, this always returns `true` (the default behaviour).
3865    fn should_open_directory(&self, path: &std::path::Path) -> bool {
3866        self.config
3867            .open_directory_filter
3868            .as_ref()
3869            .is_none_or(|f| f.matches(path))
3870    }
3871}
3872
3873/// This tests if file dialog is send and sync.
3874#[cfg(test)]
3875const fn test_prop<T: Send + Sync>() {}
3876
3877#[test]
3878const fn test() {
3879    test_prop::<FileDialog>();
3880}
3881
3882#[cfg(test)]
3883mod open_directory_filter_tests {
3884    use std::path::Path;
3885
3886    use super::*;
3887
3888    #[test]
3889    fn filter_is_none_by_default() {
3890        let dialog = FileDialog::new();
3891        assert!(dialog.config.open_directory_filter.is_none());
3892    }
3893
3894    #[test]
3895    fn set_open_directory_filter_stores_filter() {
3896        let mut dialog = FileDialog::new();
3897        dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3898        assert!(dialog.config.open_directory_filter.is_some());
3899    }
3900
3901    #[test]
3902    fn clear_open_directory_filter_removes_filter() {
3903        let mut dialog = FileDialog::new();
3904        dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3905        assert!(dialog.config.open_directory_filter.is_some());
3906        dialog.clear_open_directory_filter();
3907        assert!(dialog.config.open_directory_filter.is_none());
3908    }
3909
3910    /// When no filter is set, the dialog should always navigate into directories
3911    /// (the original default behaviour).
3912    #[test]
3913    fn no_filter_always_navigates() {
3914        let dialog = FileDialog::new();
3915        assert!(dialog.should_open_directory(Path::new("/any/dir")));
3916    }
3917
3918    /// A filter that returns `false` (do not navigate) should prevent navigation.
3919    #[test]
3920    fn filter_returning_false_prevents_navigation() {
3921        let mut dialog = FileDialog::new();
3922        dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3923        assert!(!dialog.should_open_directory(Path::new("/any/dir")));
3924    }
3925
3926    /// A filter that returns `true` (navigate) should allow navigation.
3927    #[test]
3928    fn filter_returning_true_allows_navigation() {
3929        let mut dialog = FileDialog::new();
3930        dialog.set_open_directory_filter(Filter::new(|_: &Path| true));
3931        assert!(dialog.should_open_directory(Path::new("/any/dir")));
3932    }
3933
3934    /// After clearing a filter, navigation is allowed again for every path.
3935    #[test]
3936    fn cleared_filter_restores_default_navigation() {
3937        let mut dialog = FileDialog::new();
3938        dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3939        assert!(!dialog.should_open_directory(Path::new("/any/dir")));
3940        dialog.clear_open_directory_filter();
3941        assert!(dialog.should_open_directory(Path::new("/any/dir")));
3942    }
3943
3944    /// A path-sensitive filter: navigation is blocked only when the directory
3945    /// contains a sentinel file (simulating the project-picker use-case).  We
3946    /// use a real temporary directory so the `exists()` call is meaningful.
3947    #[test]
3948    fn filter_based_on_sentinel_file() -> Result<(), Box<dyn std::error::Error>> {
3949        use tempdir::TempDir;
3950        let tmp = TempDir::new("egui_fd_test")?;
3951        let project_dir = tmp.path().join("project");
3952        std::fs::create_dir_all(&project_dir)?;
3953        let sentinel = project_dir.join("project.json");
3954        std::fs::write(&sentinel, b"{}")?;
3955
3956        let regular_dir = tmp.path().join("regular");
3957        std::fs::create_dir_all(&regular_dir)?;
3958
3959        let mut dialog = FileDialog::new();
3960        // Mimic a project picker filter: navigate into dirs that are NOT projects.
3961        dialog.set_open_directory_filter(Filter::new(|path: &Path| {
3962            !path.join("project.json").exists()
3963        }));
3964
3965        // Project directories should NOT be navigated into (filter → false → submit).
3966        assert!(!dialog.should_open_directory(&project_dir));
3967        // Regular directories should be navigated into normally.
3968        assert!(dialog.should_open_directory(&regular_dir));
3969        // tempdir auto-cleans on drop
3970        Ok(())
3971    }
3972}