Skip to main content

egui_file_dialog/
file_dialog.rs

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