1use std::any::Any;
2use std::fmt::Debug;
3use std::ops::Mul;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use egui::text::{CCursor, CCursorRange};
8
9use crate::config::{
10 FileDialogConfig, FileDialogKeyBindings, FileDialogLabels, FileFilter, Filter, OpeningMode,
11 PinnedFolder, QuickAccess, SaveExtension,
12};
13use crate::create_directory_dialog::CreateDirectoryDialog;
14use crate::data::{
15 DirectoryContent, DirectoryContentState, DirectoryEntry, DirectoryFilter, Disk, Disks,
16 UserDirectories,
17};
18use crate::modals::{FileDialogModal, ModalAction, ModalState, OverwriteFileModal};
19use crate::{FileSystem, NativeFileSystem};
20
21#[derive(Debug, PartialEq, Eq, Clone, Copy)]
23pub enum DialogMode {
24 PickFile,
26
27 PickDirectory,
29
30 PickMultiple,
32
33 SaveFile,
35}
36
37#[derive(Debug, PartialEq, Eq, Clone)]
39pub enum DialogState {
40 Open,
42
43 Closed,
45
46 Picked(PathBuf),
48
49 PickedMultiple(Vec<PathBuf>),
51
52 Cancelled,
54}
55
56#[derive(Debug, Clone)]
58#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
59pub struct FileDialogStorage {
60 pub pinned_folders: Vec<PinnedFolder>,
62 pub show_hidden: bool,
64 pub show_system_files: bool,
66 pub last_visited_dir: Option<PathBuf>,
68 pub last_picked_dir: Option<PathBuf>,
70}
71
72impl Default for FileDialogStorage {
73 fn default() -> Self {
75 Self {
76 pinned_folders: Vec::new(),
77 show_hidden: false,
78 show_system_files: false,
79 last_visited_dir: None,
80 last_picked_dir: None,
81 }
82 }
83}
84
85#[derive(Debug)]
111pub struct FileDialog {
112 config: FileDialogConfig,
114 storage: FileDialogStorage,
116
117 modals: Vec<Box<dyn FileDialogModal + Send + Sync>>,
120
121 mode: DialogMode,
123 state: DialogState,
125 show_files: bool,
128 user_data: Option<Box<dyn Any + Send + Sync>>,
131 window_id: egui::Id,
133
134 user_directories: Option<UserDirectories>,
137 system_disks: Disks,
140
141 directory_stack: Vec<PathBuf>,
145 directory_offset: usize,
150 directory_content: DirectoryContent,
152
153 create_directory_dialog: CreateDirectoryDialog,
155
156 path_edit_visible: bool,
158 path_edit_value: String,
160 path_edit_activate: bool,
163 path_edit_request_focus: bool,
165
166 selected_item: Option<DirectoryEntry>,
169 file_name_input: String,
171 file_name_input_error: Option<String>,
174 file_name_input_request_focus: bool,
176 selected_file_filter: Option<egui::Id>,
178 selected_save_extension: Option<egui::Id>,
180
181 scroll_to_selection: bool,
183 search_value: String,
185 init_search: bool,
187
188 any_focused_last_frame: bool,
192
193 rename_pinned_folder: Option<PinnedFolder>,
196 rename_pinned_folder_request_focus: bool,
199
200 init_rendering_order: bool,
205}
206
207impl Default for FileDialog {
208 fn default() -> Self {
210 Self::new()
211 }
212}
213
214impl Debug for dyn FileDialogModal + Send + Sync {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 write!(f, "<FileDialogModal>")
217 }
218}
219
220type FileDialogUiCallback<'a> = dyn FnMut(&mut egui::Ui, &mut FileDialog) + 'a;
225
226impl FileDialog {
227 #[must_use]
232 pub fn new() -> Self {
233 let file_system = Arc::new(NativeFileSystem);
234
235 Self {
236 config: FileDialogConfig::default_from_filesystem(file_system.clone()),
237 storage: FileDialogStorage::default(),
238
239 modals: Vec::new(),
240
241 mode: DialogMode::PickDirectory,
242 state: DialogState::Closed,
243 show_files: true,
244 user_data: None,
245
246 window_id: egui::Id::new("file_dialog"),
247
248 user_directories: None,
249 system_disks: Disks::new_empty(),
250
251 directory_stack: Vec::new(),
252 directory_offset: 0,
253 directory_content: DirectoryContent::default(),
254
255 create_directory_dialog: CreateDirectoryDialog::from_filesystem(file_system),
256
257 path_edit_visible: false,
258 path_edit_value: String::new(),
259 path_edit_activate: false,
260 path_edit_request_focus: false,
261
262 selected_item: None,
263 file_name_input: String::new(),
264 file_name_input_error: None,
265 file_name_input_request_focus: true,
266 selected_file_filter: None,
267 selected_save_extension: None,
268
269 scroll_to_selection: false,
270 search_value: String::new(),
271 init_search: false,
272
273 any_focused_last_frame: false,
274
275 rename_pinned_folder: None,
276 rename_pinned_folder_request_focus: false,
277
278 init_rendering_order: true,
279 }
280 }
281
282 pub fn with_config(config: FileDialogConfig) -> Self {
284 let mut obj = Self::new();
285 *obj.config_mut() = config;
286 obj.create_directory_dialog =
287 CreateDirectoryDialog::from_filesystem(obj.config.file_system.clone());
288 obj
289 }
290
291 #[must_use]
293 pub fn with_file_system(file_system: Arc<dyn FileSystem + Send + Sync>) -> Self {
294 let mut obj = Self::new();
295 obj.config.initial_directory = file_system.current_dir().unwrap_or_default();
296 obj.config.file_system = file_system;
297 obj.create_directory_dialog =
298 CreateDirectoryDialog::from_filesystem(obj.config.file_system.clone());
299 obj
300 }
301
302 #[deprecated(
348 since = "0.10.0",
349 note = "Use `pick_file` / `pick_directory` / `pick_multiple` in combination with \
350 `set_user_data` instead"
351 )]
352 pub fn open(&mut self, mode: DialogMode, mut show_files: bool) {
353 self.reset();
354 self.refresh();
355
356 if mode == DialogMode::PickFile {
357 show_files = true;
358 }
359
360 if mode == DialogMode::SaveFile {
361 self.file_name_input_request_focus = true;
362 self.file_name_input
363 .clone_from(&self.config.default_file_name);
364 }
365
366 self.selected_file_filter = None;
367 self.selected_save_extension = None;
368
369 self.set_default_file_filter();
370 self.set_default_save_extension();
371
372 self.mode = mode;
373 self.state = DialogState::Open;
374 self.show_files = show_files;
375
376 self.window_id = self
377 .config
378 .id
379 .unwrap_or_else(|| egui::Id::new(self.get_window_title()));
380
381 self.load_directory(&self.get_initial_directory());
382 }
383
384 pub fn pick_directory(&mut self) {
392 #[allow(deprecated)]
394 self.open(DialogMode::PickDirectory, false);
395 }
396
397 pub fn pick_file(&mut self) {
403 #[allow(deprecated)]
405 self.open(DialogMode::PickFile, true);
406 }
407
408 pub fn pick_multiple(&mut self) {
415 #[allow(deprecated)]
417 self.open(DialogMode::PickMultiple, true);
418 }
419
420 pub fn save_file(&mut self) {
426 #[allow(deprecated)]
428 self.open(DialogMode::SaveFile, true);
429 }
430
431 pub fn update(&mut self, ctx: &egui::Context) -> &Self {
435 if self.state != DialogState::Open {
436 return self;
437 }
438
439 self.update_keybindings(ctx);
440 self.update_ui(ctx, None);
441
442 self
443 }
444
445 pub fn set_right_panel_width(&mut self, width: f32) {
447 self.config.right_panel_width = Some(width);
448 }
449
450 pub fn clear_right_panel_width(&mut self) {
452 self.config.right_panel_width = None;
453 }
454
455 pub fn update_with_right_panel_ui(
467 &mut self,
468 ctx: &egui::Context,
469 f: &mut FileDialogUiCallback,
470 ) -> &Self {
471 if self.state != DialogState::Open {
472 return self;
473 }
474
475 self.update_keybindings(ctx);
476 self.update_ui(ctx, Some(f));
477
478 self
479 }
480
481 pub fn config_mut(&mut self) -> &mut FileDialogConfig {
486 &mut self.config
487 }
488
489 pub fn set_open_directory_filter(&mut self, filter: Filter<Path>) {
493 self.config.open_directory_filter = Some(filter);
494 }
495
496 pub fn clear_open_directory_filter(&mut self) {
498 self.config.open_directory_filter = None;
499 }
500
501 pub fn storage(mut self, storage: FileDialogStorage) -> Self {
505 self.storage = storage;
506 self
507 }
508
509 pub fn storage_mut(&mut self) -> &mut FileDialogStorage {
511 &mut self.storage
512 }
513
514 pub fn keybindings(mut self, keybindings: FileDialogKeyBindings) -> Self {
516 self.config.keybindings = keybindings;
517 self
518 }
519
520 pub fn labels(mut self, labels: FileDialogLabels) -> Self {
526 self.config.labels = labels;
527 self
528 }
529
530 pub fn labels_mut(&mut self) -> &mut FileDialogLabels {
532 &mut self.config.labels
533 }
534
535 pub const fn opening_mode(mut self, opening_mode: OpeningMode) -> Self {
537 self.config.opening_mode = opening_mode;
538 self
539 }
540
541 pub const fn as_modal(mut self, as_modal: bool) -> Self {
546 self.config.as_modal = as_modal;
547 self
548 }
549
550 pub const fn modal_overlay_color(mut self, modal_overlay_color: egui::Color32) -> Self {
552 self.config.modal_overlay_color = modal_overlay_color;
553 self
554 }
555
556 pub fn initial_directory(mut self, directory: PathBuf) -> Self {
565 self.config.initial_directory = directory;
566 self
567 }
568
569 pub fn default_file_name(mut self, name: &str) -> Self {
571 name.clone_into(&mut self.config.default_file_name);
572 self
573 }
574
575 pub const fn allow_file_overwrite(mut self, allow_file_overwrite: bool) -> Self {
581 self.config.allow_file_overwrite = allow_file_overwrite;
582 self
583 }
584
585 pub const fn allow_path_edit_to_save_file_without_extension(mut self, allow: bool) -> Self {
594 self.config.allow_path_edit_to_save_file_without_extension = allow;
595 self
596 }
597
598 pub fn directory_separator(mut self, separator: &str) -> Self {
601 self.config.directory_separator = separator.to_string();
602 self
603 }
604
605 pub const fn canonicalize_paths(mut self, canonicalize: bool) -> Self {
622 self.config.canonicalize_paths = canonicalize;
623 self
624 }
625
626 pub const fn load_via_thread(mut self, load_via_thread: bool) -> Self {
630 self.config.load_via_thread = load_via_thread;
631 self
632 }
633
634 pub const fn truncate_filenames(mut self, truncate_filenames: bool) -> Self {
640 self.config.truncate_filenames = truncate_filenames;
641 self
642 }
643
644 pub const fn retain_selected_entry(mut self, retain_selected_entry: bool) -> Self {
646 self.config.retain_selected_entry = retain_selected_entry;
647 self
648 }
649
650 pub fn max_selections(mut self, max: usize) -> Self {
652 self.config.max_selections = Some(max);
653 self
654 }
655
656 pub fn err_icon(mut self, icon: &str) -> Self {
658 self.config.err_icon = icon.to_string();
659 self
660 }
661
662 pub fn default_file_icon(mut self, icon: &str) -> Self {
664 self.config.default_file_icon = icon.to_string();
665 self
666 }
667
668 pub fn default_folder_icon(mut self, icon: &str) -> Self {
670 self.config.default_folder_icon = icon.to_string();
671 self
672 }
673
674 pub fn device_icon(mut self, icon: &str) -> Self {
676 self.config.device_icon = icon.to_string();
677 self
678 }
679
680 pub fn removable_device_icon(mut self, icon: &str) -> Self {
682 self.config.removable_device_icon = icon.to_string();
683 self
684 }
685
686 pub fn parent_directory_icon(mut self, icon: &str) -> Self {
688 self.config.parent_directory_icon = icon.to_string();
689 self
690 }
691
692 pub fn back_icon(mut self, icon: &str) -> Self {
694 self.config.back_icon = icon.to_string();
695 self
696 }
697
698 pub fn forward_icon(mut self, icon: &str) -> Self {
700 self.config.forward_icon = icon.to_string();
701 self
702 }
703
704 pub fn new_folder_icon(mut self, icon: &str) -> Self {
706 self.config.new_folder_icon = icon.to_string();
707 self
708 }
709
710 pub fn menu_icon(mut self, icon: &str) -> Self {
712 self.config.menu_icon = icon.to_string();
713 self
714 }
715
716 pub fn search_icon(mut self, icon: &str) -> Self {
718 self.config.search_icon = icon.to_string();
719 self
720 }
721
722 pub fn path_edit_icon(mut self, icon: &str) -> Self {
724 self.config.path_edit_icon = icon.to_string();
725 self
726 }
727
728 pub fn add_file_filter(mut self, name: &str, filter: Filter<Path>) -> Self {
754 self.config = self.config.add_file_filter(name, filter);
755 self
756 }
757
758 pub fn add_file_filter_extensions(mut self, name: &str, extensions: Vec<&'static str>) -> Self {
774 self.config = self.config.add_file_filter_extensions(name, extensions);
775 self
776 }
777
778 pub fn default_file_filter(mut self, name: &str) -> Self {
782 self.config.default_file_filter = Some(name.to_string());
783 self
784 }
785
786 pub fn add_save_extension(mut self, name: &str, file_extension: &str) -> Self {
808 self.config = self.config.add_save_extension(name, file_extension);
809 self
810 }
811
812 pub fn default_save_extension(mut self, name: &str) -> Self {
816 self.config.default_save_extension = Some(name.to_string());
817 self
818 }
819
820 pub fn set_file_icon(mut self, icon: &str, filter: Filter<std::path::Path>) -> Self {
841 self.config = self.config.set_file_icon(icon, filter);
842 self
843 }
844
845 pub fn add_quick_access(
861 mut self,
862 heading: &str,
863 builder: impl FnOnce(&mut QuickAccess),
864 ) -> Self {
865 self.config = self.config.add_quick_access(heading, builder);
866 self
867 }
868
869 pub fn title(mut self, title: &str) -> Self {
874 self.config.title = Some(title.to_string());
875 self
876 }
877
878 pub fn id(mut self, id: impl Into<egui::Id>) -> Self {
880 self.config.id = Some(id.into());
881 self
882 }
883
884 pub fn default_pos(mut self, default_pos: impl Into<egui::Pos2>) -> Self {
886 self.config.default_pos = Some(default_pos.into());
887 self
888 }
889
890 pub fn fixed_pos(mut self, pos: impl Into<egui::Pos2>) -> Self {
892 self.config.fixed_pos = Some(pos.into());
893 self
894 }
895
896 pub fn default_size(mut self, size: impl Into<egui::Vec2>) -> Self {
898 self.config.default_size = size.into();
899 self
900 }
901
902 pub fn max_size(mut self, max_size: impl Into<egui::Vec2>) -> Self {
904 self.config.max_size = Some(max_size.into());
905 self
906 }
907
908 pub fn min_size(mut self, min_size: impl Into<egui::Vec2>) -> Self {
912 self.config.min_size = min_size.into();
913 self
914 }
915
916 pub fn anchor(mut self, align: egui::Align2, offset: impl Into<egui::Vec2>) -> Self {
918 self.config.anchor = Some((align, offset.into()));
919 self
920 }
921
922 pub const fn resizable(mut self, resizable: bool) -> Self {
924 self.config.resizable = resizable;
925 self
926 }
927
928 pub const fn movable(mut self, movable: bool) -> Self {
932 self.config.movable = movable;
933 self
934 }
935
936 pub const fn title_bar(mut self, title_bar: bool) -> Self {
938 self.config.title_bar = title_bar;
939 self
940 }
941
942 pub const fn show_top_panel(mut self, show_top_panel: bool) -> Self {
945 self.config.show_top_panel = show_top_panel;
946 self
947 }
948
949 pub const fn show_parent_button(mut self, show_parent_button: bool) -> Self {
953 self.config.show_parent_button = show_parent_button;
954 self
955 }
956
957 pub const fn show_back_button(mut self, show_back_button: bool) -> Self {
961 self.config.show_back_button = show_back_button;
962 self
963 }
964
965 pub const fn show_forward_button(mut self, show_forward_button: bool) -> Self {
969 self.config.show_forward_button = show_forward_button;
970 self
971 }
972
973 pub const fn show_new_folder_button(mut self, show_new_folder_button: bool) -> Self {
977 self.config.show_new_folder_button = show_new_folder_button;
978 self
979 }
980
981 pub const fn show_current_path(mut self, show_current_path: bool) -> Self {
985 self.config.show_current_path = show_current_path;
986 self
987 }
988
989 pub const fn show_path_edit_button(mut self, show_path_edit_button: bool) -> Self {
993 self.config.show_path_edit_button = show_path_edit_button;
994 self
995 }
996
997 pub const fn show_menu_button(mut self, show_menu_button: bool) -> Self {
1002 self.config.show_menu_button = show_menu_button;
1003 self
1004 }
1005
1006 pub const fn show_reload_button(mut self, show_reload_button: bool) -> Self {
1011 self.config.show_reload_button = show_reload_button;
1012 self
1013 }
1014
1015 pub const fn show_working_directory_button(
1022 mut self,
1023 show_working_directory_button: bool,
1024 ) -> Self {
1025 self.config.show_working_directory_button = show_working_directory_button;
1026 self
1027 }
1028
1029 pub const fn show_select_all_button(mut self, show_select_all_button: bool) -> Self {
1035 self.config.show_select_all_button = show_select_all_button;
1036 self
1037 }
1038
1039 pub const fn show_hidden_option(mut self, show_hidden_option: bool) -> Self {
1045 self.config.show_hidden_option = show_hidden_option;
1046 self
1047 }
1048
1049 pub const fn show_system_files_option(mut self, show_system_files_option: bool) -> Self {
1055 self.config.show_system_files_option = show_system_files_option;
1056 self
1057 }
1058
1059 pub const fn show_search(mut self, show_search: bool) -> Self {
1063 self.config.show_search = show_search;
1064 self
1065 }
1066
1067 pub const fn show_all_files_filter(mut self, show_all_files_filter: bool) -> Self {
1076 self.config.show_all_files_filter = show_all_files_filter;
1077 self
1078 }
1079
1080 pub const fn show_left_panel(mut self, show_left_panel: bool) -> Self {
1083 self.config.show_left_panel = show_left_panel;
1084 self
1085 }
1086
1087 pub const fn show_pinned_folders(mut self, show_pinned_folders: bool) -> Self {
1090 self.config.show_pinned_folders = show_pinned_folders;
1091 self
1092 }
1093
1094 pub const fn show_places(mut self, show_places: bool) -> Self {
1099 self.config.show_places = show_places;
1100 self
1101 }
1102
1103 pub const fn show_devices(mut self, show_devices: bool) -> Self {
1108 self.config.show_devices = show_devices;
1109 self
1110 }
1111
1112 pub const fn show_removable_devices(mut self, show_removable_devices: bool) -> Self {
1117 self.config.show_removable_devices = show_removable_devices;
1118 self
1119 }
1120
1121 pub fn picked(&self) -> Option<&Path> {
1129 match &self.state {
1130 DialogState::Picked(path) => Some(path),
1131 _ => None,
1132 }
1133 }
1134
1135 pub fn take_picked(&mut self) -> Option<PathBuf> {
1142 match &mut self.state {
1143 DialogState::Picked(path) => {
1144 let path = std::mem::take(path);
1145 self.state = DialogState::Closed;
1146 Some(path)
1147 }
1148 _ => None,
1149 }
1150 }
1151
1152 pub fn picked_multiple(&self) -> Option<Vec<&Path>> {
1157 match &self.state {
1158 DialogState::PickedMultiple(items) => {
1159 Some(items.iter().map(std::path::PathBuf::as_path).collect())
1160 }
1161 _ => None,
1162 }
1163 }
1164
1165 pub fn take_picked_multiple(&mut self) -> Option<Vec<PathBuf>> {
1172 match &mut self.state {
1173 DialogState::PickedMultiple(items) => {
1174 let items = std::mem::take(items);
1175 self.state = DialogState::Closed;
1176 Some(items)
1177 }
1178 _ => None,
1179 }
1180 }
1181
1182 pub const fn selected_entry(&self) -> Option<&DirectoryEntry> {
1190 self.selected_item.as_ref()
1191 }
1192
1193 pub fn selected_entries(&self) -> impl Iterator<Item = &DirectoryEntry> {
1199 self.get_dir_content_filtered_iter().filter(|p| p.selected)
1200 }
1201
1202 pub fn user_data<U: Any>(&self) -> Option<&U> {
1206 #[allow(clippy::coerce_container_to_any)]
1207 self.user_data.as_ref().and_then(|u| u.downcast_ref())
1208 }
1209
1210 pub fn user_data_mut<U: Any>(&mut self) -> Option<&mut U> {
1214 #[allow(clippy::coerce_container_to_any)]
1215 self.user_data.as_mut().and_then(|u| u.downcast_mut())
1216 }
1217
1218 pub fn set_user_data<U: Any + Send + Sync>(&mut self, user_data: U) {
1242 self.user_data = Some(Box::new(user_data));
1243 }
1244
1245 pub const fn mode(&self) -> DialogMode {
1247 self.mode
1248 }
1249
1250 pub const fn state(&self) -> &DialogState {
1252 &self.state
1253 }
1254
1255 pub const fn get_window_id(&self) -> egui::Id {
1257 self.window_id
1258 }
1259}
1260
1261impl FileDialog {
1263 fn update_ui(
1267 &mut self,
1268 ctx: &egui::Context,
1269 right_panel_fn: Option<&mut FileDialogUiCallback>,
1270 ) {
1271 let mut is_open = true;
1272
1273 let re = self.create_window(&mut is_open).show(ctx, |ui| {
1274 if !self.modals.is_empty() {
1275 self.ui_update_modals(ui);
1276 return;
1277 }
1278
1279 if self.config.show_top_panel {
1280 let mut margin = ctx.global_style().spacing.window_margin;
1281 margin.top = 0;
1282
1283 egui::Panel::top(self.window_id.with("top_panel"))
1284 .resizable(false)
1285 .frame(egui::Frame::new().inner_margin(margin))
1286 .show(ui, |ui| {
1287 self.ui_update_top_panel(ui);
1288 });
1289 }
1290
1291 if self.config.show_left_panel {
1292 egui::Panel::left(self.window_id.with("left_panel"))
1293 .resizable(true)
1294 .default_size(150.0)
1295 .size_range(90.0..=250.0)
1296 .show(ui, |ui| {
1297 self.ui_update_left_panel(ui);
1298 });
1299 }
1300
1301 if let Some(f) = right_panel_fn {
1303 let mut right_panel = egui::Panel::right(self.window_id.with("right_panel"))
1304 .resizable(true);
1307 if let Some(width) = self.config.right_panel_width {
1308 right_panel = right_panel.default_size(width);
1309 }
1310 right_panel.show(ui, |ui| {
1311 f(ui, self);
1312 });
1313 }
1314
1315 egui::Panel::bottom(self.window_id.with("bottom_panel"))
1316 .resizable(false)
1317 .show(ui, |ui| {
1318 self.ui_update_bottom_panel(ui);
1319 });
1320
1321 egui::CentralPanel::default().show(ui, |ui| {
1322 self.ui_update_central_panel(ui);
1323 });
1324 });
1325
1326 if self.config.as_modal {
1327 let modal_re = self.ui_update_modal_background(ctx);
1328
1329 if self.init_rendering_order {
1339 ctx.move_to_top(modal_re.response.layer_id);
1340 self.init_rendering_order = false;
1341 } else if let Some(inner_response) = re {
1342 ctx.move_to_top(inner_response.response.layer_id);
1343 }
1344 }
1345
1346 self.any_focused_last_frame = ctx.memory(egui::Memory::focused).is_some();
1347
1348 if !is_open {
1350 self.cancel();
1351 }
1352
1353 let mut repaint = false;
1354
1355 ctx.input(|i| {
1357 if let Some(dropped_file) = i.raw.dropped_files.last() {
1359 let path = dropped_file.path();
1360 if self.config.file_system.is_dir(path) {
1361 self.load_directory(path);
1363 repaint = true;
1364 } else if let Some(parent) = path.parent() {
1365 self.load_directory(parent);
1367 self.select_item(&mut DirectoryEntry::from_path(
1368 &self.config,
1369 path,
1370 &*self.config.file_system,
1371 ));
1372 self.scroll_to_selection = true;
1373 repaint = true;
1374 }
1375 }
1376 });
1377
1378 if repaint {
1380 ctx.request_repaint();
1381 }
1382 }
1383
1384 fn ui_update_modal_background(&self, ctx: &egui::Context) -> egui::InnerResponse<()> {
1386 egui::Area::new(self.window_id.with("modal_overlay"))
1387 .interactable(true)
1388 .fixed_pos(egui::Pos2::ZERO)
1389 .show(ctx, |ui| {
1390 let content_rect = ctx.input(egui::InputState::content_rect);
1391
1392 ui.allocate_response(content_rect.size(), egui::Sense::click());
1393
1394 ui.painter().rect_filled(
1395 content_rect,
1396 egui::CornerRadius::ZERO,
1397 self.config.modal_overlay_color,
1398 );
1399 })
1400 }
1401
1402 fn ui_update_modals(&mut self, ui: &mut egui::Ui) {
1403 egui::Panel::bottom(self.window_id.with("modal_bottom_panel"))
1408 .resizable(false)
1409 .show_separator_line(false)
1410 .show(ui, |_| {});
1411
1412 egui::CentralPanel::default().show(ui, |ui| {
1415 if let Some(modal) = self.modals.last_mut() {
1416 #[allow(clippy::single_match)]
1417 match modal.update(&self.config, ui) {
1418 ModalState::Close(action) => {
1419 self.exec_modal_action(action);
1420 self.modals.pop();
1421 }
1422 ModalState::Pending => {}
1423 }
1424 }
1425 });
1426 }
1427
1428 fn create_window<'a>(&self, is_open: &'a mut bool) -> egui::Window<'a> {
1430 let mut window = egui::Window::new(self.get_window_title())
1431 .id(self.window_id)
1432 .open(is_open)
1433 .default_size(self.config.default_size)
1434 .min_size(self.config.min_size)
1435 .resizable(self.config.resizable)
1436 .movable(self.config.movable)
1437 .title_bar(self.config.title_bar)
1438 .collapsible(false);
1439
1440 if let Some(pos) = self.config.default_pos {
1441 window = window.default_pos(pos);
1442 }
1443
1444 if let Some(pos) = self.config.fixed_pos {
1445 window = window.fixed_pos(pos);
1446 }
1447
1448 if let Some((anchor, offset)) = self.config.anchor {
1449 window = window.anchor(anchor, offset);
1450 }
1451
1452 if let Some(size) = self.config.max_size {
1453 window = window.max_size(size);
1454 }
1455
1456 window
1457 }
1458
1459 const fn get_window_title(&self) -> &String {
1462 match &self.config.title {
1463 Some(title) => title,
1464 None => match &self.mode {
1465 DialogMode::PickDirectory => &self.config.labels.title_select_directory,
1466 DialogMode::PickFile => &self.config.labels.title_select_file,
1467 DialogMode::PickMultiple => &self.config.labels.title_select_multiple,
1468 DialogMode::SaveFile => &self.config.labels.title_save_file,
1469 },
1470 }
1471 }
1472
1473 fn ui_update_top_panel(&mut self, ui: &mut egui::Ui) {
1476 const STROKE_INNER_MARGIN: i8 = 5;
1477
1478 let text_height = ui.text_style_height(&egui::TextStyle::Body);
1479 let mut button_height = ui.spacing().button_padding.y.mul_add(2.0, text_height);
1480
1481 if button_height < 22.0 {
1482 button_height = 22.0;
1483 }
1484
1485 let content_height = f32::from(STROKE_INNER_MARGIN).mul_add(2.0, button_height);
1486 let square_button_size = egui::Vec2::new(button_height, button_height).mul(1.08);
1487
1488 ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1489 self.ui_update_nav_buttons(ui, square_button_size, content_height);
1490
1491 let mut path_display_width = ui.available_width();
1492
1493 if self.config.show_reload_button {
1495 path_display_width -= ui
1496 .spacing()
1497 .item_spacing
1498 .x
1499 .mul_add(2.0, square_button_size.x);
1500 }
1501
1502 if self.config.show_search {
1504 path_display_width -= 140.0;
1505 }
1506
1507 if path_display_width < 100.0 {
1508 path_display_width = 100.0;
1509 }
1510
1511 if self.config.show_current_path {
1512 self.ui_update_current_path(
1513 ui,
1514 path_display_width,
1515 STROKE_INNER_MARGIN,
1516 button_height,
1517 );
1518 }
1519
1520 let hamburger_menu_contains_items = self.config.show_reload_button
1521 || self.config.show_working_directory_button
1522 || self.config.show_select_all_button
1523 || self.config.show_hidden_option
1524 || self.config.show_system_files_option;
1525
1526 let hamburger_menu_visible =
1527 self.config.show_menu_button && hamburger_menu_contains_items;
1528
1529 if hamburger_menu_visible {
1530 self.ui_update_hamburger_menu(ui, square_button_size, content_height);
1531 }
1532
1533 if self.config.show_search {
1534 self.ui_update_search(ui, STROKE_INNER_MARGIN, button_height);
1535 }
1536 });
1537 }
1538
1539 fn ui_update_nav_buttons(
1540 &mut self,
1541 ui: &mut egui::Ui,
1542 button_size: egui::Vec2,
1543 content_height: f32,
1544 ) {
1545 ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
1546 ui.add_space((content_height - button_size.y) / 2.0);
1548
1549 ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1550 self.ui_update_nav_buttons_content(ui, button_size);
1551 });
1552 });
1553 }
1554
1555 fn ui_update_nav_buttons_content(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
1556 if self.config.show_parent_button {
1557 if let Some(x) = self.current_directory() {
1558 if self.ui_button_sized(
1559 ui,
1560 x.parent().is_some(),
1561 button_size,
1562 self.config.parent_directory_icon.as_str(),
1563 None,
1564 ) {
1565 self.load_parent_directory();
1566 }
1567 } else {
1568 let _ = self.ui_button_sized(
1569 ui,
1570 false,
1571 button_size,
1572 self.config.parent_directory_icon.as_str(),
1573 None,
1574 );
1575 }
1576 }
1577
1578 if self.config.show_back_button
1579 && self.ui_button_sized(
1580 ui,
1581 self.directory_offset + 1 < self.directory_stack.len(),
1582 button_size,
1583 self.config.back_icon.as_str(),
1584 None,
1585 )
1586 {
1587 self.load_previous_directory();
1588 }
1589
1590 if self.config.show_forward_button
1591 && self.ui_button_sized(
1592 ui,
1593 self.directory_offset != 0,
1594 button_size,
1595 self.config.forward_icon.as_str(),
1596 None,
1597 )
1598 {
1599 self.load_next_directory();
1600 }
1601
1602 if self.config.show_new_folder_button
1603 && self.ui_button_sized(
1604 ui,
1605 !self.create_directory_dialog.is_open(),
1606 button_size,
1607 self.config.new_folder_icon.as_str(),
1608 None,
1609 )
1610 {
1611 self.open_new_folder_dialog();
1612 }
1613 }
1614
1615 fn ui_update_current_path(
1619 &mut self,
1620 ui: &mut egui::Ui,
1621 width: f32,
1622 frame_inner_margin: i8,
1623 button_height: f32,
1624 ) {
1625 let stroke = egui::Stroke::new(1.0, ui.style().visuals.window_stroke.color);
1626
1627 egui::Frame::default()
1628 .stroke(stroke)
1629 .inner_margin(egui::Margin::same(frame_inner_margin - 1))
1630 .corner_radius(egui::CornerRadius::from(4))
1631 .show(ui, |ui| {
1632 if self.path_edit_visible {
1633 self.ui_update_path_edit(ui, width, button_height);
1634 } else {
1635 self.ui_update_path_display(ui, width, button_height);
1636 }
1637 });
1638 }
1639
1640 fn ui_update_path_display(&mut self, ui: &mut egui::Ui, mut width: f32, button_height: f32) {
1642 ui.style_mut().always_scroll_the_only_direction = true;
1643 ui.style_mut().spacing.scroll.bar_width = 8.0;
1644
1645 let edit_button_size = egui::Vec2::new(button_height, button_height);
1646
1647 if self.config.show_path_edit_button {
1649 width -= ui.spacing().item_spacing.x.mul_add(2.0, edit_button_size.x);
1650 }
1651
1652 egui::ScrollArea::horizontal()
1653 .auto_shrink([false, true])
1654 .stick_to_right(true)
1655 .max_width(width)
1656 .content_margin(egui::Margin::ZERO)
1657 .show(ui, |ui| {
1658 ui.horizontal(|ui| {
1659 ui.style_mut().spacing.item_spacing.x /= 2.5;
1660
1661 let mut path = PathBuf::new();
1662
1663 if let Some(data) = self.current_directory().map(Path::to_path_buf) {
1664 for (i, segment) in data.iter().enumerate() {
1665 path.push(segment);
1666
1667 let mut segment_str = segment.to_str().unwrap_or_default().to_string();
1668
1669 if self.is_pinned(&path) {
1670 segment_str =
1671 format!("{} {}", self.config.pinned_icon, segment_str);
1672 }
1673
1674 if i != 0 {
1675 ui.label(self.config.directory_separator.as_str());
1676 }
1677
1678 let btn = egui::Button::new(segment_str);
1679 let re = ui.add_sized(egui::Vec2::new(0.0, button_height), btn);
1680
1681 if re.clicked() {
1682 self.load_directory(path.as_path());
1683 return;
1684 }
1685
1686 self.ui_update_central_panel_path_context_menu(&re, &path.clone());
1687 }
1688 }
1689 });
1690 });
1691
1692 if !self.config.show_path_edit_button {
1693 return;
1694 }
1695
1696 let button = egui::Button::new(&self.config.path_edit_icon)
1697 .fill(egui::Color32::TRANSPARENT)
1698 .wrap();
1699
1700 if ui.add_sized(edit_button_size, button).clicked() {
1701 self.open_path_edit();
1702 }
1703 }
1704
1705 fn ui_update_path_edit(&mut self, ui: &mut egui::Ui, mut width: f32, button_height: f32) {
1707 let edit_button_size = egui::Vec2::new(button_height, button_height);
1708 width -= ui.spacing().item_spacing.x.mul_add(2.0, edit_button_size.x);
1709
1710 let empty_space = button_height - ui.text_style_height(&egui::TextStyle::Body);
1712 let padding_top_bottom = empty_space / 2.0;
1713 #[allow(clippy::cast_possible_truncation)]
1714 let margin = egui::Margin::symmetric(4, padding_top_bottom.floor() as i8);
1715
1716 let frame = egui::Frame::dark_canvas(ui.style())
1717 .inner_margin(margin)
1718 .stroke(egui::Stroke::NONE);
1719
1720 let text_edit = egui::TextEdit::singleline(&mut self.path_edit_value)
1721 .desired_width(width)
1722 .frame(frame);
1723
1724 let response = text_edit.show(ui).response;
1725
1726 if self.path_edit_activate {
1727 response.request_focus();
1728 Self::set_cursor_to_end(&response, &self.path_edit_value);
1729 self.path_edit_activate = false;
1730 }
1731
1732 if self.path_edit_request_focus {
1733 response.request_focus();
1734 self.path_edit_request_focus = false;
1735 }
1736
1737 let btn = egui::Button::new("✔").wrap();
1738 let btn_response = ui.add_sized(edit_button_size, btn);
1739
1740 if btn_response.clicked() {
1741 self.submit_path_edit();
1742 }
1743
1744 if !response.has_focus() && !btn_response.contains_pointer() {
1745 self.path_edit_visible = false;
1746 }
1747 }
1748
1749 fn ui_update_hamburger_menu(
1751 &mut self,
1752 ui: &mut egui::Ui,
1753 button_size: egui::Vec2,
1754 content_height: f32,
1755 ) {
1756 use egui::containers::menu::{is_in_menu, MenuButton, SubMenuButton};
1757
1758 ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
1759 ui.add_space((content_height - button_size.y) / 2.0);
1761
1762 ui.horizontal(|ui| {
1763 let btn = egui::Button::new(&self.config.menu_icon).min_size(button_size);
1766
1767 if is_in_menu(ui) {
1768 SubMenuButton::new(&self.config.menu_icon).ui(ui, |ui| {
1769 self.ui_update_hamburger_menu_content(ui);
1770 });
1771 } else {
1772 MenuButton::from_button(btn).ui(ui, |ui| {
1773 self.ui_update_hamburger_menu_content(ui);
1774 });
1775 }
1776 });
1777 });
1778 }
1779
1780 fn ui_update_hamburger_menu_content(&mut self, ui: &mut egui::Ui) {
1782 const SEPARATOR_SPACING: f32 = 2.0;
1783
1784 let working_dir = self.config.file_system.current_dir();
1785
1786 let show_reload = self.config.show_reload_button;
1787 let show_working_dir = self.config.show_working_directory_button && working_dir.is_ok();
1788 let show_select_all =
1789 self.config.show_select_all_button && self.mode == DialogMode::PickMultiple;
1790
1791 let show_hidden = self.config.show_hidden_option;
1792 let show_system_files = self.config.show_system_files_option;
1793
1794 if show_reload && ui.button(&self.config.labels.reload).clicked() {
1795 self.refresh();
1796 ui.close();
1797 }
1798
1799 if show_working_dir && ui.button(&self.config.labels.working_directory).clicked() {
1800 self.load_directory(&working_dir.unwrap_or_default());
1801 ui.close();
1802 }
1803
1804 if show_select_all && ui.button(&self.config.labels.select_all).clicked() {
1805 self.select_all_items();
1806 ui.close();
1807 }
1808
1809 let any_above = show_reload || show_working_dir || show_select_all;
1810 let any_below = show_hidden || show_system_files;
1811
1812 if any_above && any_below {
1813 ui.add_space(SEPARATOR_SPACING);
1814 ui.separator();
1815 ui.add_space(SEPARATOR_SPACING);
1816 }
1817
1818 if show_hidden
1819 && ui
1820 .checkbox(
1821 &mut self.storage.show_hidden,
1822 &self.config.labels.show_hidden,
1823 )
1824 .clicked()
1825 {
1826 self.refresh();
1827 ui.close();
1828 }
1829
1830 if show_system_files
1831 && ui
1832 .checkbox(
1833 &mut self.storage.show_system_files,
1834 &self.config.labels.show_system_files,
1835 )
1836 .clicked()
1837 {
1838 self.refresh();
1839 ui.close();
1840 }
1841 }
1842
1843 fn ui_update_search(&mut self, ui: &mut egui::Ui, frame_inner_margin: i8, button_height: f32) {
1845 let stroke = egui::Stroke::new(1.0, ui.style().visuals.window_stroke.color);
1846
1847 let margin = egui::Margin {
1848 top: frame_inner_margin,
1849 bottom: frame_inner_margin,
1850 #[allow(clippy::cast_possible_truncation)]
1851 left: (f32::from(frame_inner_margin) * 1.5).floor() as i8,
1852 right: frame_inner_margin,
1853 };
1854
1855 egui::Frame::default()
1856 .stroke(stroke)
1857 .inner_margin(margin)
1858 .corner_radius(egui::CornerRadius::from(4))
1859 .show(ui, |ui| {
1860 ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1861 self.ui_update_search_content(ui, button_height);
1862 });
1863 });
1864 }
1865
1866 fn ui_update_search_content(&mut self, ui: &mut egui::Ui, button_height: f32) {
1867 ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
1868 let text_height = ui.text_style_height(&egui::TextStyle::Body);
1870 if text_height <= button_height {
1871 ui.add_space((button_height - text_height) / 2.0);
1872 }
1873
1874 ui.label(&self.config.search_icon);
1875 });
1876
1877 let empty_space = button_height - ui.text_style_height(&egui::TextStyle::Body);
1879 let padding_top_bottom = empty_space / 2.0;
1880 #[allow(clippy::cast_possible_truncation)]
1881 let margin = egui::Margin::symmetric(4, padding_top_bottom.floor() as i8);
1882
1883 let frame = egui::Frame::dark_canvas(ui.style())
1884 .inner_margin(margin)
1885 .stroke(egui::Stroke::NONE);
1886
1887 let text_edit = egui::TextEdit::singleline(&mut self.search_value)
1888 .desired_width(ui.available_width())
1889 .frame(frame);
1890
1891 let re = text_edit.show(ui).response;
1892
1893 self.edit_search_on_text_input(ui);
1894
1895 if re.changed() || self.init_search {
1896 self.selected_item = None;
1897 self.select_first_visible_item();
1898 }
1899
1900 if self.init_search {
1901 re.request_focus();
1902 Self::set_cursor_to_end(&re, &self.search_value);
1903 self.directory_content.reset_multi_selection();
1904
1905 self.init_search = false;
1906 }
1907 }
1908
1909 fn edit_search_on_text_input(&mut self, ui: &egui::Ui) {
1916 if ui.memory(|mem| mem.focused().is_some()) {
1917 return;
1918 }
1919
1920 ui.input(|inp| {
1921 if inp.modifiers.any() && !inp.modifiers.shift_only() {
1923 return;
1924 }
1925
1926 for text in inp.events.iter().filter_map(|ev| match ev {
1929 egui::Event::Text(t) => Some(t),
1930 _ => None,
1931 }) {
1932 self.search_value.push_str(text);
1933 self.init_search = true;
1934 }
1935 });
1936 }
1937
1938 fn ui_update_left_panel(&mut self, ui: &mut egui::Ui) {
1941 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
1942 const SPACING_MULTIPLIER: f32 = 4.0;
1944
1945 egui::containers::ScrollArea::vertical()
1946 .auto_shrink([false, false])
1947 .show(ui, |ui| {
1948 let mut spacing = ui.global_style().spacing.item_spacing.y * 2.0;
1950
1951 if self.config.show_pinned_folders && self.ui_update_pinned_folders(ui, spacing)
1953 {
1954 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1955 }
1956
1957 let quick_accesses = std::mem::take(&mut self.config.quick_accesses);
1959
1960 for quick_access in &quick_accesses {
1961 ui.add_space(spacing);
1962 self.ui_update_quick_access(ui, quick_access);
1963 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1964 }
1965
1966 self.config.quick_accesses = quick_accesses;
1967
1968 if self.config.show_places && self.ui_update_user_directories(ui, spacing) {
1970 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1971 }
1972
1973 let disks = std::mem::take(&mut self.system_disks);
1974
1975 if self.config.show_devices && self.ui_update_devices(ui, spacing, &disks) {
1976 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1977 }
1978
1979 if self.config.show_removable_devices
1980 && self.ui_update_removable_devices(ui, spacing, &disks)
1981 {
1982 }
1985
1986 self.system_disks = disks;
1987 });
1988 });
1989 }
1990
1991 fn ui_update_left_panel_entry(
1995 &mut self,
1996 ui: &mut egui::Ui,
1997 display_name: &str,
1998 path: &Path,
1999 ) -> egui::Response {
2000 let response = ui.selectable_label(self.current_directory() == Some(path), display_name);
2001
2002 if response.clicked() {
2003 self.load_directory(path);
2004 }
2005
2006 response
2007 }
2008
2009 fn ui_update_quick_access(&mut self, ui: &mut egui::Ui, quick_access: &QuickAccess) {
2011 ui.label(&quick_access.heading);
2012
2013 for entry in &quick_access.paths {
2014 self.ui_update_left_panel_entry(ui, &entry.display_name, &entry.path);
2015 }
2016 }
2017
2018 fn ui_update_pinned_folders(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
2023 let mut visible = false;
2024
2025 for (i, pinned) in self.storage.pinned_folders.clone().iter().enumerate() {
2026 if i == 0 {
2027 ui.add_space(spacing);
2028 ui.label(self.config.labels.heading_pinned.as_str());
2029
2030 visible = true;
2031 }
2032
2033 if self.is_pinned_folder_being_renamed(pinned) {
2034 self.ui_update_pinned_folder_rename(ui);
2035 continue;
2036 }
2037
2038 let response = self.ui_update_left_panel_entry(
2039 ui,
2040 &format!("{} {}", self.config.pinned_icon, pinned.label),
2041 pinned.path.as_path(),
2042 );
2043
2044 self.ui_update_pinned_folder_context_menu(&response, pinned);
2045 }
2046
2047 visible
2048 }
2049
2050 fn ui_update_pinned_folder_rename(&mut self, ui: &mut egui::Ui) {
2051 if let Some(r) = &mut self.rename_pinned_folder {
2052 let id = self.window_id.with("pinned_folder_rename").with(&r.path);
2053 let mut output = egui::TextEdit::singleline(&mut r.label)
2054 .id(id)
2055 .cursor_at_end(true)
2056 .show(ui);
2057
2058 if self.rename_pinned_folder_request_focus {
2059 output.state.cursor.set_char_range(Some(CCursorRange::two(
2060 CCursor::new(0),
2061 CCursor::new(r.label.chars().count()),
2062 )));
2063 output.state.store(ui.ctx(), output.response.id);
2064
2065 output.response.request_focus();
2066
2067 self.rename_pinned_folder_request_focus = false;
2068 }
2069
2070 if output.response.lost_focus() {
2071 self.end_rename_pinned_folder();
2072 }
2073 }
2074 }
2075
2076 fn ui_update_pinned_folder_context_menu(
2077 &mut self,
2078 item: &egui::Response,
2079 pinned: &PinnedFolder,
2080 ) {
2081 item.context_menu(|ui| {
2082 if ui.button(&self.config.labels.unpin_folder).clicked() {
2083 self.unpin_path(&pinned.path);
2084 ui.close();
2085 }
2086
2087 if ui
2088 .button(&self.config.labels.rename_pinned_folder)
2089 .clicked()
2090 {
2091 self.begin_rename_pinned_folder(pinned.clone());
2092 ui.close();
2093 }
2094 });
2095 }
2096
2097 fn ui_update_user_directories(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
2102 let user_directories = std::mem::take(&mut self.user_directories);
2106 let labels = std::mem::take(&mut self.config.labels);
2107
2108 let visible = if let Some(dirs) = &user_directories {
2109 ui.add_space(spacing);
2110 ui.label(labels.heading_places.as_str());
2111
2112 if let Some(path) = dirs.home_dir() {
2113 self.ui_update_left_panel_entry(ui, &labels.home_dir, path);
2114 }
2115 if let Some(path) = dirs.desktop_dir() {
2116 self.ui_update_left_panel_entry(ui, &labels.desktop_dir, path);
2117 }
2118 if let Some(path) = dirs.document_dir() {
2119 self.ui_update_left_panel_entry(ui, &labels.documents_dir, path);
2120 }
2121 if let Some(path) = dirs.download_dir() {
2122 self.ui_update_left_panel_entry(ui, &labels.downloads_dir, path);
2123 }
2124 if let Some(path) = dirs.audio_dir() {
2125 self.ui_update_left_panel_entry(ui, &labels.audio_dir, path);
2126 }
2127 if let Some(path) = dirs.picture_dir() {
2128 self.ui_update_left_panel_entry(ui, &labels.pictures_dir, path);
2129 }
2130 if let Some(path) = dirs.video_dir() {
2131 self.ui_update_left_panel_entry(ui, &labels.videos_dir, path);
2132 }
2133
2134 true
2135 } else {
2136 false
2137 };
2138
2139 self.user_directories = user_directories;
2140 self.config.labels = labels;
2141
2142 visible
2143 }
2144
2145 fn ui_update_devices(&mut self, ui: &mut egui::Ui, spacing: f32, disks: &Disks) -> bool {
2150 let mut visible = false;
2151
2152 for (i, disk) in disks.iter().filter(|x| !x.is_removable()).enumerate() {
2153 if i == 0 {
2154 ui.add_space(spacing);
2155 ui.label(self.config.labels.heading_devices.as_str());
2156
2157 visible = true;
2158 }
2159
2160 self.ui_update_device_entry(ui, disk);
2161 }
2162
2163 visible
2164 }
2165
2166 fn ui_update_removable_devices(
2171 &mut self,
2172 ui: &mut egui::Ui,
2173 spacing: f32,
2174 disks: &Disks,
2175 ) -> bool {
2176 let mut visible = false;
2177
2178 for (i, disk) in disks.iter().filter(|x| x.is_removable()).enumerate() {
2179 if i == 0 {
2180 ui.add_space(spacing);
2181 ui.label(self.config.labels.heading_removable_devices.as_str());
2182
2183 visible = true;
2184 }
2185
2186 self.ui_update_device_entry(ui, disk);
2187 }
2188
2189 visible
2190 }
2191
2192 fn ui_update_device_entry(&mut self, ui: &mut egui::Ui, device: &Disk) {
2194 let label = if device.is_removable() {
2195 format!(
2196 "{} {}",
2197 self.config.removable_device_icon,
2198 device.display_name()
2199 )
2200 } else {
2201 format!("{} {}", self.config.device_icon, device.display_name())
2202 };
2203
2204 self.ui_update_left_panel_entry(ui, &label, device.mount_point());
2205 }
2206
2207 fn ui_update_bottom_panel(&mut self, ui: &mut egui::Ui) {
2209 const BUTTON_HEIGHT: f32 = 20.0;
2210 ui.add_space(5.0);
2211
2212 let label_submit_width = match self.mode {
2214 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2215 Self::calc_text_width(ui, &self.config.labels.open_button)
2216 }
2217 DialogMode::SaveFile => Self::calc_text_width(ui, &self.config.labels.save_button),
2218 };
2219
2220 let mut btn_width = Self::calc_text_width(ui, &self.config.labels.cancel_button);
2221 if label_submit_width > btn_width {
2222 btn_width = label_submit_width;
2223 }
2224
2225 btn_width = ui.spacing().button_padding.x.mul_add(4.0, btn_width);
2226
2227 let button_size: egui::Vec2 = egui::Vec2::new(btn_width, BUTTON_HEIGHT);
2229
2230 self.ui_update_selection_preview(ui, button_size);
2231
2232 if self.mode == DialogMode::SaveFile && self.config.save_extensions.is_empty() {
2233 ui.add_space(ui.style().spacing.item_spacing.y);
2234 }
2235
2236 self.ui_update_action_buttons(ui, button_size);
2237 }
2238
2239 fn ui_update_selection_preview(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2242 const SELECTION_PREVIEW_MIN_WIDTH: f32 = 50.0;
2243 let item_spacing = ui.style().spacing.item_spacing;
2244
2245 let render_filter_selection = (!self.config.file_filters.is_empty()
2246 && (self.mode == DialogMode::PickFile || self.mode == DialogMode::PickMultiple))
2247 || (!self.config.save_extensions.is_empty() && self.mode == DialogMode::SaveFile);
2248
2249 let filter_selection_width = button_size.x.mul_add(2.0, item_spacing.y);
2250 let mut filter_selection_separate_line = false;
2251
2252 ui.horizontal(|ui| {
2253 match &self.mode {
2254 DialogMode::PickDirectory => ui.label(&self.config.labels.selected_directory),
2255 DialogMode::PickFile => ui.label(&self.config.labels.selected_file),
2256 DialogMode::PickMultiple => ui.label(&self.config.labels.selected_items),
2257 DialogMode::SaveFile => ui.label(&self.config.labels.file_name),
2258 };
2259
2260 let mut scroll_bar_width: f32 =
2265 ui.available_width() - filter_selection_width - item_spacing.x;
2266
2267 if scroll_bar_width < SELECTION_PREVIEW_MIN_WIDTH || !render_filter_selection {
2268 filter_selection_separate_line = true;
2269 scroll_bar_width = ui.available_width();
2270 }
2271
2272 match &self.mode {
2273 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2274 use egui::containers::scroll_area::ScrollBarVisibility;
2275
2276 let text = self.get_selection_preview_text();
2277
2278 egui::containers::ScrollArea::horizontal()
2279 .auto_shrink([false, false])
2280 .max_width(scroll_bar_width)
2281 .stick_to_right(true)
2282 .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
2283 .show(ui, |ui| {
2284 ui.colored_label(ui.style().visuals.selection.bg_fill, text);
2285 });
2286 }
2287 DialogMode::SaveFile => {
2288 let mut output = egui::TextEdit::singleline(&mut self.file_name_input)
2289 .cursor_at_end(false)
2290 .margin(egui::Margin::symmetric(4, 3))
2291 .desired_width(scroll_bar_width)
2292 .show(ui);
2293
2294 if self.file_name_input_request_focus {
2295 self.highlight_file_name_input(&mut output);
2296 output.state.store(ui.ctx(), output.response.id);
2297
2298 output.response.request_focus();
2299 self.file_name_input_request_focus = false;
2300 }
2301
2302 if output.response.changed() {
2303 self.file_name_input_error = self.validate_file_name_input();
2304 }
2305
2306 if output.response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
2307 {
2308 self.submit();
2309 }
2310 }
2311 }
2312
2313 if !filter_selection_separate_line && render_filter_selection {
2314 if self.mode == DialogMode::SaveFile {
2315 self.ui_update_save_extension_selection(ui, filter_selection_width);
2316 } else {
2317 self.ui_update_file_filter_selection(ui, filter_selection_width);
2318 }
2319 }
2320 });
2321
2322 if filter_selection_separate_line && render_filter_selection {
2323 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2324 if self.mode == DialogMode::SaveFile {
2325 self.ui_update_save_extension_selection(ui, filter_selection_width);
2326 } else {
2327 self.ui_update_file_filter_selection(ui, filter_selection_width);
2328 }
2329 });
2330 }
2331 }
2332
2333 fn highlight_file_name_input(&self, output: &mut egui::text_edit::TextEditOutput) {
2337 if let Some(pos) = self.file_name_input.rfind('.') {
2338 let range = if pos == 0 {
2339 CCursorRange::two(CCursor::new(0), CCursor::new(0))
2340 } else {
2341 CCursorRange::two(CCursor::new(0), CCursor::new(pos))
2342 };
2343
2344 output.state.cursor.set_char_range(Some(range));
2345 }
2346 }
2347
2348 fn get_selection_preview_text(&self) -> String {
2349 if self.is_selection_valid() {
2350 match &self.mode {
2351 DialogMode::PickDirectory | DialogMode::PickFile => self
2352 .selected_item
2353 .as_ref()
2354 .map_or_else(String::new, |item| item.file_name().to_string()),
2355 DialogMode::PickMultiple => {
2356 let mut result = String::new();
2357
2358 for (i, item) in self
2359 .get_dir_content_filtered_iter()
2360 .filter(|p| p.selected)
2361 .enumerate()
2362 {
2363 if i == 0 {
2364 result += item.file_name();
2365 continue;
2366 }
2367
2368 result += format!(", {}", item.file_name()).as_str();
2369 }
2370
2371 result
2372 }
2373 DialogMode::SaveFile => String::new(),
2374 }
2375 } else {
2376 String::new()
2377 }
2378 }
2379
2380 fn ui_update_file_filter_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2381 let selected_filter = self.get_selected_file_filter();
2382 let selected_text = match selected_filter {
2383 Some(f) => &f.name,
2384 None => &self.config.labels.file_filter_all_files,
2385 };
2386
2387 let mut select_filter: Option<Option<FileFilter>> = None;
2390
2391 egui::containers::ComboBox::from_id_salt(self.window_id.with("file_filter_selection"))
2392 .width(width)
2393 .selected_text(selected_text)
2394 .wrap_mode(egui::TextWrapMode::Truncate)
2395 .show_ui(ui, |ui| {
2396 for filter in &self.config.file_filters {
2397 let selected = selected_filter.is_some_and(|f| f.id == filter.id);
2398
2399 if ui.selectable_label(selected, &filter.name).clicked() {
2400 select_filter = Some(Some(filter.clone()));
2401 }
2402 }
2403
2404 if self.config.show_all_files_filter
2405 && ui
2406 .selectable_label(
2407 selected_filter.is_none(),
2408 &self.config.labels.file_filter_all_files,
2409 )
2410 .clicked()
2411 {
2412 select_filter = Some(None);
2413 }
2414 });
2415
2416 if let Some(i) = select_filter {
2417 self.select_file_filter(i);
2418 }
2419 }
2420
2421 fn ui_update_save_extension_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2422 let selected_extension = self.get_selected_save_extension();
2423 let selected_text = match selected_extension {
2424 Some(e) => &e.to_string(),
2425 None => &self.config.labels.save_extension_any,
2426 };
2427
2428 let mut select_extension: Option<Option<SaveExtension>> = None;
2431
2432 egui::containers::ComboBox::from_id_salt(self.window_id.with("save_extension_selection"))
2433 .width(width)
2434 .selected_text(selected_text)
2435 .wrap_mode(egui::TextWrapMode::Truncate)
2436 .show_ui(ui, |ui| {
2437 for extension in &self.config.save_extensions {
2438 let selected = selected_extension.is_some_and(|s| s.id == extension.id);
2439
2440 if ui
2441 .selectable_label(selected, extension.to_string())
2442 .clicked()
2443 {
2444 select_extension = Some(Some(extension.clone()));
2445 }
2446 }
2447 });
2448
2449 if let Some(i) = select_extension {
2450 self.file_name_input_request_focus = true;
2451 self.select_save_extension(i);
2452 }
2453 }
2454
2455 fn ui_update_action_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2457 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2458 let label = match &self.mode {
2459 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2460 self.config.labels.open_button.as_str()
2461 }
2462 DialogMode::SaveFile => self.config.labels.save_button.as_str(),
2463 };
2464
2465 ui.spacing_mut().item_spacing.x = ui.spacing_mut().item_spacing.y;
2466
2467 if self.ui_button_sized(
2468 ui,
2469 self.is_selection_valid(),
2470 button_size,
2471 label,
2472 self.file_name_input_error.as_deref(),
2473 ) {
2474 self.submit();
2475 }
2476
2477 if ui
2478 .add_sized(
2479 button_size,
2480 egui::Button::new(self.config.labels.cancel_button.as_str()),
2481 )
2482 .clicked()
2483 {
2484 self.cancel();
2485 }
2486 });
2487 }
2488
2489 fn ui_update_central_panel(&mut self, ui: &mut egui::Ui) {
2492 if self.update_directory_content(ui) {
2493 return;
2494 }
2495
2496 self.ui_update_central_panel_content(ui);
2497 }
2498
2499 fn update_directory_content(&mut self, ui: &mut egui::Ui) -> bool {
2504 const SHOW_SPINNER_AFTER: f32 = 0.2;
2505
2506 match self.directory_content.update() {
2507 DirectoryContentState::Pending(timestamp) => {
2508 let now = std::time::SystemTime::now();
2509
2510 if now
2511 .duration_since(*timestamp)
2512 .unwrap_or_default()
2513 .as_secs_f32()
2514 > SHOW_SPINNER_AFTER
2515 {
2516 ui.centered_and_justified(egui::Ui::spinner);
2517 }
2518
2519 ui.ctx().request_repaint();
2521
2522 true
2523 }
2524 DirectoryContentState::Errored(err) => {
2525 ui.centered_and_justified(|ui| ui.colored_label(ui.visuals().error_fg_color, err));
2526 true
2527 }
2528 DirectoryContentState::Finished => {
2529 if self.mode == DialogMode::PickDirectory {
2530 if let Some(dir) = self.current_directory() {
2531 let mut dir_entry =
2532 DirectoryEntry::from_path(&self.config, dir, &*self.config.file_system);
2533 self.select_item(&mut dir_entry);
2534 }
2535 }
2536
2537 false
2538 }
2539 DirectoryContentState::Success => false,
2540 }
2541 }
2542
2543 fn ui_update_central_panel_content(&mut self, ui: &mut egui::Ui) {
2546 let mut data = std::mem::take(&mut self.directory_content);
2548
2549 let mut selected_count = data
2552 .filtered_iter(&self.search_value)
2553 .filter(|item| item.selected)
2554 .count();
2555
2556 let mut reset_multi_selection = false;
2559
2560 let mut batch_select_item_b: Option<DirectoryEntry> = None;
2563
2564 let mut should_return = false;
2566
2567 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
2568 let scroll_area = egui::containers::ScrollArea::vertical().auto_shrink([false, false]);
2569
2570 if self.search_value.is_empty()
2571 && !self.create_directory_dialog.is_open()
2572 && !self.scroll_to_selection
2573 {
2574 let row_height = ui
2579 .spacing()
2580 .button_padding
2581 .y
2582 .mul_add(2.0, ui.text_style_height(&egui::TextStyle::Body));
2583
2584 scroll_area.show_rows(ui, row_height, data.len(), |ui, range| {
2585 for item in data.iter_range_mut(range) {
2586 if self.ui_update_central_panel_entry(
2587 ui,
2588 item,
2589 &mut reset_multi_selection,
2590 &mut batch_select_item_b,
2591 &mut selected_count,
2592 ) {
2593 should_return = true;
2594 }
2595 }
2596 });
2597 } else {
2598 scroll_area.show(ui, |ui| {
2604 for item in data.filtered_iter_mut(&self.search_value.clone()) {
2605 if self.ui_update_central_panel_entry(
2606 ui,
2607 item,
2608 &mut reset_multi_selection,
2609 &mut batch_select_item_b,
2610 &mut selected_count,
2611 ) {
2612 should_return = true;
2613 }
2614 }
2615
2616 if let Some(entry) = self.ui_update_create_directory_dialog(ui) {
2617 data.push(entry);
2618 }
2619 });
2620 }
2621 });
2622
2623 if should_return {
2624 return;
2625 }
2626
2627 if reset_multi_selection {
2629 for item in data.filtered_iter_mut(&self.search_value) {
2630 if let Some(selected_item) = &self.selected_item {
2631 if selected_item.path_eq(item) {
2632 continue;
2633 }
2634 }
2635
2636 item.selected = false;
2637 }
2638 }
2639
2640 if let Some(item_b) = batch_select_item_b {
2642 if let Some(item_a) = &self.selected_item {
2643 self.batch_select_between(&mut data, item_a, &item_b);
2644 }
2645 }
2646
2647 self.directory_content = data;
2648 self.scroll_to_selection = false;
2649 }
2650
2651 fn ui_update_central_panel_entry(
2654 &mut self,
2655 ui: &mut egui::Ui,
2656 item: &mut DirectoryEntry,
2657 reset_multi_selection: &mut bool,
2658 batch_select_item_b: &mut Option<DirectoryEntry>,
2659 selected_count: &mut usize,
2660 ) -> bool {
2661 let file_name = item.file_name();
2662 let primary_selected = self.is_primary_selected(item);
2663 let pinned = self.is_pinned(item.as_path());
2664
2665 let icons = if pinned {
2666 format!("{} {} ", item.icon(), self.config.pinned_icon)
2667 } else {
2668 format!("{} ", item.icon())
2669 };
2670
2671 let icons_width = Self::calc_text_width(ui, &icons);
2672
2673 let available_width = ui.available_width() - icons_width - 15.0;
2675
2676 let truncate = self.config.truncate_filenames
2677 && available_width < Self::calc_text_width(ui, file_name);
2678
2679 let text = if truncate {
2680 Self::truncate_filename(ui, item, available_width)
2681 } else {
2682 file_name.to_owned()
2683 };
2684
2685 let mut re =
2686 ui.selectable_label(primary_selected || item.selected, format!("{icons}{text}"));
2687
2688 if truncate {
2689 re = re.on_hover_text(file_name);
2690 }
2691
2692 if item.is_dir() {
2693 self.ui_update_central_panel_path_context_menu(&re, item.as_path());
2694
2695 if re.context_menu_opened() {
2696 self.select_item(item);
2697 }
2698 }
2699
2700 if primary_selected && self.scroll_to_selection {
2701 re.scroll_to_me(Some(egui::Align::Center));
2702 self.scroll_to_selection = false;
2703 }
2704
2705 if re.clicked()
2707 && !ui.input(|i| i.modifiers.command)
2708 && !ui.input(|i| i.modifiers.shift_only())
2709 {
2710 self.select_item(item);
2711
2712 if self.mode == DialogMode::PickMultiple {
2714 *reset_multi_selection = true;
2715 }
2716 }
2717
2718 if self.mode == DialogMode::PickMultiple
2721 && re.clicked()
2722 && ui.input(|i| i.modifiers.command)
2723 {
2724 if primary_selected {
2725 item.selected = false;
2728 self.selected_item = None;
2729 *selected_count = selected_count.saturating_sub(1);
2730 } else if !item.selected && self.selection_limit_reached_with(*selected_count) {
2731 } else {
2733 let was_selected = item.selected;
2734 item.selected = !item.selected;
2735
2736 if item.selected {
2737 *selected_count += 1;
2738 self.select_item(item);
2740 } else if was_selected {
2741 *selected_count = selected_count.saturating_sub(1);
2742 }
2743 }
2744 }
2745
2746 if self.mode == DialogMode::PickMultiple
2749 && re.clicked()
2750 && ui.input(|i| i.modifiers.shift_only())
2751 {
2752 if self.selection_limit_reached_with(*selected_count) && !item.selected {
2753 } else if let Some(selected_item) = self.selected_item.clone() {
2755 *batch_select_item_b = Some(selected_item);
2758
2759 if !item.selected {
2761 *selected_count += 1;
2762 }
2763 item.selected = true;
2764 self.select_item(item);
2765 }
2766 }
2767
2768 if re.double_clicked() && !ui.input(|i| i.modifiers.command) {
2771 if item.is_dir() {
2772 if self.should_open_directory(item.as_path()) {
2775 self.load_directory(&item.to_path_buf());
2776 return true;
2777 }
2778 }
2780
2781 self.select_item(item);
2782
2783 self.submit();
2784 }
2785
2786 false
2787 }
2788
2789 fn ui_update_create_directory_dialog(&mut self, ui: &mut egui::Ui) -> Option<DirectoryEntry> {
2790 self.create_directory_dialog
2791 .update(ui, &self.config)
2792 .directory()
2793 .map(|path| self.process_new_folder(&path))
2794 }
2795
2796 fn batch_select_between(
2799 &self,
2800 directory_content: &mut DirectoryContent,
2801 item_a: &DirectoryEntry,
2802 item_b: &DirectoryEntry,
2803 ) {
2804 let pos_a = directory_content
2806 .filtered_iter(&self.search_value)
2807 .position(|p| p.path_eq(item_a));
2808 let pos_b = directory_content
2809 .filtered_iter(&self.search_value)
2810 .position(|p| p.path_eq(item_b));
2811
2812 if let Some(pos_a) = pos_a {
2815 if let Some(pos_b) = pos_b {
2816 if pos_a == pos_b {
2817 return;
2818 }
2819
2820 let mut min = pos_a;
2823 let mut max = pos_b;
2824
2825 if min > max {
2826 min = pos_b;
2827 max = pos_a;
2828 }
2829
2830 let mut current_selected = directory_content
2833 .filtered_iter(&self.search_value)
2834 .filter(|item| item.selected)
2835 .count();
2836
2837 for item in directory_content
2838 .filtered_iter_mut(&self.search_value)
2839 .enumerate()
2840 .filter(|(i, _)| i > &min && i < &max)
2841 .map(|(_, p)| p)
2842 {
2843 if self.selection_limit_reached_with(current_selected) {
2844 break;
2845 }
2846 if !item.selected {
2847 current_selected += 1;
2848 }
2849 item.selected = true;
2850 }
2851 }
2852 }
2853 }
2854
2855 fn ui_button_sized(
2857 &self,
2858 ui: &mut egui::Ui,
2859 enabled: bool,
2860 size: egui::Vec2,
2861 label: &str,
2862 err_tooltip: Option<&str>,
2863 ) -> bool {
2864 let mut clicked = false;
2865
2866 ui.add_enabled_ui(enabled, |ui| {
2867 let response = ui.add_sized(size, egui::Button::new(label));
2868 clicked = response.clicked();
2869
2870 if let Some(err) = err_tooltip {
2871 response.on_disabled_hover_ui(|ui| {
2872 ui.horizontal_wrapped(|ui| {
2873 ui.spacing_mut().item_spacing.x = 0.0;
2874
2875 ui.colored_label(
2876 ui.global_style().visuals.error_fg_color,
2877 format!("{} ", self.config.err_icon),
2878 );
2879
2880 ui.label(err);
2881 });
2882 });
2883 }
2884 });
2885
2886 clicked
2887 }
2888
2889 fn ui_update_central_panel_path_context_menu(&mut self, item: &egui::Response, path: &Path) {
2896 if !self.config.show_pinned_folders {
2898 return;
2899 }
2900
2901 item.context_menu(|ui| {
2902 let pinned = self.is_pinned(path);
2903
2904 if pinned {
2905 if ui.button(&self.config.labels.unpin_folder).clicked() {
2906 self.unpin_path(path);
2907 ui.close();
2908 }
2909 } else if ui.button(&self.config.labels.pin_folder).clicked() {
2910 self.pin_path(path.to_path_buf());
2911 ui.close();
2912 }
2913 });
2914 }
2915
2916 fn set_cursor_to_end(re: &egui::Response, data: &str) {
2923 if let Some(mut state) = egui::TextEdit::load_state(&re.ctx, re.id) {
2925 state
2926 .cursor
2927 .set_char_range(Some(CCursorRange::one(CCursor::new(data.len()))));
2928 state.store(&re.ctx, re.id);
2929 }
2930 }
2931
2932 fn calc_char_width(ui: &egui::Ui, char: char) -> f32 {
2934 ui.fonts_mut(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char))
2935 }
2936
2937 fn calc_text_width(ui: &egui::Ui, text: &str) -> f32 {
2940 let mut width = 0.0;
2941
2942 for char in text.chars() {
2943 width += Self::calc_char_width(ui, char);
2944 }
2945
2946 width
2947 }
2948
2949 fn truncate_filename(ui: &egui::Ui, item: &DirectoryEntry, max_length: f32) -> String {
2950 const TRUNCATE_STR: &str = "...";
2951
2952 let path = item.as_path();
2953
2954 let file_stem = if item.is_file() {
2955 path.file_stem().and_then(|f| f.to_str()).unwrap_or("")
2956 } else {
2957 item.file_name()
2958 };
2959
2960 let extension = if item.is_file() {
2961 path.extension().map_or(String::new(), |ext| {
2962 format!(".{}", ext.to_str().unwrap_or(""))
2963 })
2964 } else {
2965 String::new()
2966 };
2967
2968 let extension_width = Self::calc_text_width(ui, &extension);
2969 let reserved = extension_width + Self::calc_text_width(ui, TRUNCATE_STR);
2970
2971 if max_length <= reserved {
2972 return format!("{TRUNCATE_STR}{extension}");
2973 }
2974
2975 let mut width = reserved;
2976 let mut front = String::new();
2977 let mut back = String::new();
2978
2979 for (i, char) in file_stem.chars().enumerate() {
2980 let w = Self::calc_char_width(ui, char);
2981
2982 if width + w > max_length {
2983 break;
2984 }
2985
2986 front.push(char);
2987 width += w;
2988
2989 let back_index = file_stem.len() - i - 1;
2990
2991 if back_index <= i {
2992 break;
2993 }
2994
2995 if let Some(char) = file_stem.chars().nth(back_index) {
2996 let w = Self::calc_char_width(ui, char);
2997
2998 if width + w > max_length {
2999 break;
3000 }
3001
3002 back.push(char);
3003 width += w;
3004 }
3005 }
3006
3007 format!(
3008 "{front}{TRUNCATE_STR}{}{extension}",
3009 back.chars().rev().collect::<String>()
3010 )
3011 }
3012}
3013
3014impl FileDialog {
3016 fn update_keybindings(&mut self, ctx: &egui::Context) {
3018 if let Some(modal) = self.modals.last_mut() {
3021 modal.update_keybindings(&self.config, ctx);
3022 return;
3023 }
3024
3025 let keybindings = std::mem::take(&mut self.config.keybindings);
3026
3027 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.submit, false) {
3028 self.exec_keybinding_submit();
3029 }
3030
3031 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.cancel, false) {
3032 self.exec_keybinding_cancel();
3033 }
3034
3035 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.parent, true) {
3036 self.load_parent_directory();
3037 }
3038
3039 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.back, true) {
3040 self.load_previous_directory();
3041 }
3042
3043 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.forward, true) {
3044 self.load_next_directory();
3045 }
3046
3047 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.reload, true) {
3048 self.refresh();
3049 }
3050
3051 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.new_folder, true) {
3052 self.open_new_folder_dialog();
3053 }
3054
3055 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.edit_path, true) {
3056 self.open_path_edit();
3057 }
3058
3059 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.home_edit_path, true) {
3060 if let Some(dirs) = &self.user_directories {
3061 if let Some(home) = dirs.home_dir() {
3062 self.load_directory(home.to_path_buf().as_path());
3063 self.open_path_edit();
3064 }
3065 }
3066 }
3067
3068 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_up, false) {
3069 self.exec_keybinding_selection_up();
3070
3071 if let Some(id) = ctx.memory(egui::Memory::focused) {
3073 ctx.memory_mut(|w| w.surrender_focus(id));
3074 }
3075 }
3076
3077 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_down, false) {
3078 self.exec_keybinding_selection_down();
3079
3080 if let Some(id) = ctx.memory(egui::Memory::focused) {
3082 ctx.memory_mut(|w| w.surrender_focus(id));
3083 }
3084 }
3085
3086 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.select_all, true)
3087 && self.mode == DialogMode::PickMultiple
3088 {
3089 self.select_all_items();
3090 }
3091
3092 self.config.keybindings = keybindings;
3093 }
3094
3095 fn exec_keybinding_submit(&mut self) {
3097 if self.path_edit_visible {
3098 self.submit_path_edit();
3099 return;
3100 }
3101
3102 if self.create_directory_dialog.is_open() {
3103 if let Some(dir) = self.create_directory_dialog.submit().directory() {
3104 self.process_new_folder(&dir);
3105 }
3106 return;
3107 }
3108
3109 if self.any_focused_last_frame {
3110 return;
3111 }
3112
3113 if let Some(item) = &self.selected_item {
3115 let is_visible = self
3117 .get_dir_content_filtered_iter()
3118 .any(|p| p.path_eq(item));
3119
3120 if is_visible && item.is_dir() {
3121 self.load_directory(&item.to_path_buf());
3122 return;
3123 }
3124 }
3125
3126 self.submit();
3127 }
3128
3129 fn exec_keybinding_cancel(&mut self) {
3131 if self.create_directory_dialog.is_open() {
3149 self.create_directory_dialog.close();
3150 } else if self.path_edit_visible {
3151 self.close_path_edit();
3152 } else if !self.any_focused_last_frame {
3153 self.cancel();
3154 }
3155 }
3156
3157 fn exec_keybinding_selection_up(&mut self) {
3159 if self.directory_content.len() == 0 {
3160 return;
3161 }
3162
3163 self.directory_content.reset_multi_selection();
3164
3165 if let Some(item) = &self.selected_item {
3166 if self.select_next_visible_item_before(&item.clone()) {
3167 return;
3168 }
3169 }
3170
3171 self.select_last_visible_item();
3174 }
3175
3176 fn exec_keybinding_selection_down(&mut self) {
3178 if self.directory_content.len() == 0 {
3179 return;
3180 }
3181
3182 self.directory_content.reset_multi_selection();
3183
3184 if let Some(item) = &self.selected_item {
3185 if self.select_next_visible_item_after(&item.clone()) {
3186 return;
3187 }
3188 }
3189
3190 self.select_first_visible_item();
3193 }
3194}
3195
3196impl FileDialog {
3198 fn get_selected_file_filter(&self) -> Option<&FileFilter> {
3200 self.selected_file_filter
3201 .and_then(|id| self.config.file_filters.iter().find(|p| p.id == id))
3202 }
3203
3204 fn set_default_file_filter(&mut self) {
3206 if let Some(name) = &self.config.default_file_filter {
3207 for filter in &self.config.file_filters {
3208 if filter.name == name.as_str() {
3209 self.selected_file_filter = Some(filter.id);
3210 }
3211 }
3212 }
3213 }
3214
3215 fn select_file_filter(&mut self, filter: Option<FileFilter>) {
3217 self.selected_file_filter = filter.map(|f| f.id);
3218 self.selected_item = None;
3219 self.refresh();
3220 }
3221
3222 fn get_selected_save_extension(&self) -> Option<&SaveExtension> {
3224 self.selected_save_extension
3225 .and_then(|id| self.config.save_extensions.iter().find(|p| p.id == id))
3226 }
3227
3228 fn set_default_save_extension(&mut self) {
3230 let config = std::mem::take(&mut self.config);
3231
3232 if let Some(name) = &config.default_save_extension {
3233 for extension in &config.save_extensions {
3234 if extension.name == name.as_str() {
3235 self.selected_save_extension = Some(extension.id);
3236 self.set_file_name_extension(&extension.file_extension);
3237 }
3238 }
3239 }
3240
3241 self.config = config;
3242 }
3243
3244 fn select_save_extension(&mut self, extension: Option<SaveExtension>) {
3246 if let Some(ex) = extension {
3247 self.selected_save_extension = Some(ex.id);
3248 self.set_file_name_extension(&ex.file_extension);
3249 }
3250
3251 self.selected_item = None;
3252 self.refresh();
3253 }
3254
3255 fn set_file_name_extension(&mut self, extension: &str) {
3257 let dot_count = self.file_name_input.chars().filter(|c| *c == '.').count();
3261 let use_simple = dot_count == 1 && self.file_name_input.chars().nth(0) == Some('.');
3262
3263 let mut p = PathBuf::from(&self.file_name_input);
3264 if !use_simple && p.set_extension(extension) {
3265 self.file_name_input = p.to_string_lossy().into_owned();
3266 } else {
3267 self.file_name_input = format!(".{extension}");
3268 }
3269 }
3270
3271 fn get_dir_content_filtered_iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
3273 self.directory_content.filtered_iter(&self.search_value)
3274 }
3275
3276 fn open_new_folder_dialog(&mut self) {
3278 if let Some(x) = self.current_directory() {
3279 self.create_directory_dialog.open(x.to_path_buf());
3280 }
3281 }
3282
3283 fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry {
3285 let mut entry =
3286 DirectoryEntry::from_path(&self.config, created_dir, &*self.config.file_system);
3287
3288 self.directory_content.push(entry.clone());
3289
3290 self.select_item(&mut entry);
3291
3292 entry
3293 }
3294
3295 fn open_modal(&mut self, modal: Box<dyn FileDialogModal + Send + Sync>) {
3297 self.modals.push(modal);
3298 }
3299
3300 fn exec_modal_action(&mut self, action: ModalAction) {
3302 match action {
3303 ModalAction::None => {}
3304 ModalAction::SaveFile(path) => self.state = DialogState::Picked(path),
3305 }
3306 }
3307
3308 fn canonicalize_path(&self, path: &Path) -> PathBuf {
3311 if self.config.canonicalize_paths {
3312 dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
3313 } else {
3314 path.to_path_buf()
3315 }
3316 }
3317
3318 fn pin_path(&mut self, path: PathBuf) {
3320 let pinned = PinnedFolder::from_path(path);
3321 self.storage.pinned_folders.push(pinned);
3322 }
3323
3324 fn unpin_path(&mut self, path: &Path) {
3326 self.storage
3327 .pinned_folders
3328 .retain(|p| p.path.as_path() != path);
3329 }
3330
3331 fn is_pinned(&self, path: &Path) -> bool {
3333 self.storage
3334 .pinned_folders
3335 .iter()
3336 .any(|p| p.path.as_path() == path)
3337 }
3338
3339 fn begin_rename_pinned_folder(&mut self, pinned: PinnedFolder) {
3341 self.rename_pinned_folder = Some(pinned);
3342 self.rename_pinned_folder_request_focus = true;
3343 }
3344
3345 fn end_rename_pinned_folder(&mut self) {
3348 let renamed = std::mem::take(&mut self.rename_pinned_folder);
3349
3350 if let Some(renamed) = renamed {
3351 let old = self
3352 .storage
3353 .pinned_folders
3354 .iter_mut()
3355 .find(|p| p.path == renamed.path);
3356 if let Some(old) = old {
3357 old.label = renamed.label;
3358 }
3359 }
3360 }
3361
3362 fn is_pinned_folder_being_renamed(&self, pinned: &PinnedFolder) -> bool {
3364 self.rename_pinned_folder
3365 .as_ref()
3366 .is_some_and(|p| p.path == pinned.path)
3367 }
3368
3369 fn is_primary_selected(&self, item: &DirectoryEntry) -> bool {
3370 self.selected_item.as_ref().is_some_and(|x| x.path_eq(item))
3371 }
3372
3373 fn reset(&mut self) {
3376 let user_data = std::mem::take(&mut self.user_data);
3377 let storage = self.storage.clone();
3378 let config = self.config.clone();
3379 let selected = self.selected_item.clone();
3380
3381 *self = Self::with_config(config);
3382 if self.config.retain_selected_entry {
3383 self.selected_item = selected;
3384 }
3385 self.storage = storage;
3386 self.user_data = user_data;
3387 }
3388
3389 fn refresh(&mut self) {
3392 self.user_directories = self
3393 .config
3394 .file_system
3395 .user_dirs(self.config.canonicalize_paths);
3396 self.system_disks = self
3397 .config
3398 .file_system
3399 .get_disks(self.config.canonicalize_paths);
3400
3401 self.reload_directory();
3402 }
3403
3404 fn submit(&mut self) {
3406 if !self.is_selection_valid() {
3408 return;
3409 }
3410
3411 self.storage.last_picked_dir = self.current_directory().map(PathBuf::from);
3412
3413 match &self.mode {
3414 DialogMode::PickDirectory | DialogMode::PickFile => {
3415 if let Some(item) = self.selected_item.clone() {
3418 self.state = DialogState::Picked(item.to_path_buf());
3419 }
3420 }
3421 DialogMode::PickMultiple => {
3422 let result: Vec<PathBuf> = self
3423 .selected_entries()
3424 .map(crate::DirectoryEntry::to_path_buf)
3425 .collect();
3426
3427 self.state = DialogState::PickedMultiple(result);
3428 }
3429 DialogMode::SaveFile => {
3430 if let Some(path) = self.current_directory() {
3433 let full_path = path.join(&self.file_name_input);
3434 self.submit_save_file(full_path);
3435 }
3436 }
3437 }
3438 }
3439
3440 fn submit_save_file(&mut self, path: PathBuf) {
3443 if path.exists() {
3444 self.open_modal(Box::new(OverwriteFileModal::new(path)));
3445
3446 return;
3447 }
3448
3449 self.state = DialogState::Picked(path);
3450 }
3451
3452 fn cancel(&mut self) {
3454 self.state = DialogState::Cancelled;
3455 }
3456
3457 fn get_initial_directory(&self) -> PathBuf {
3463 let path = match self.config.opening_mode {
3464 OpeningMode::AlwaysInitialDir => &self.config.initial_directory,
3465 OpeningMode::LastVisitedDir => self
3466 .storage
3467 .last_visited_dir
3468 .as_deref()
3469 .unwrap_or(&self.config.initial_directory),
3470 OpeningMode::LastPickedDir => self
3471 .storage
3472 .last_picked_dir
3473 .as_deref()
3474 .unwrap_or(&self.config.initial_directory),
3475 };
3476
3477 let mut path = self.canonicalize_path(path);
3478
3479 if self.config.file_system.is_file(&path) {
3480 if let Some(parent) = path.parent() {
3481 path = parent.to_path_buf();
3482 }
3483 }
3484
3485 path
3486 }
3487
3488 fn current_directory(&self) -> Option<&Path> {
3490 if let Some(x) = self.directory_stack.iter().nth_back(self.directory_offset) {
3491 return Some(x.as_path());
3492 }
3493
3494 None
3495 }
3496
3497 fn is_selection_valid(&self) -> bool {
3500 match &self.mode {
3501 DialogMode::PickDirectory => self
3502 .selected_item
3503 .as_ref()
3504 .is_some_and(crate::DirectoryEntry::is_dir),
3505 DialogMode::PickFile => self
3506 .selected_item
3507 .as_ref()
3508 .is_some_and(DirectoryEntry::is_file),
3509 DialogMode::PickMultiple => self.get_dir_content_filtered_iter().any(|p| p.selected),
3510 DialogMode::SaveFile => self.file_name_input_error.is_none(),
3511 }
3512 }
3513
3514 fn validate_file_name_input(&self) -> Option<String> {
3518 if self.file_name_input.is_empty() {
3519 return Some(self.config.labels.err_empty_file_name.clone());
3520 }
3521
3522 if let Some(x) = self.current_directory() {
3523 let mut full_path = x.to_path_buf();
3524 full_path.push(self.file_name_input.as_str());
3525
3526 if self.config.file_system.is_dir(&full_path) {
3527 return Some(self.config.labels.err_directory_exists.clone());
3528 }
3529
3530 if !self.config.allow_file_overwrite && self.config.file_system.is_file(&full_path) {
3531 return Some(self.config.labels.err_file_exists.clone());
3532 }
3533 } else {
3534 return Some("Currently not in a directory".to_string());
3536 }
3537
3538 None
3539 }
3540
3541 fn select_item(&mut self, item: &mut DirectoryEntry) {
3544 if self.mode == DialogMode::PickMultiple {
3545 item.selected = true;
3546 }
3547 self.selected_item = Some(item.clone());
3548
3549 if self.mode == DialogMode::SaveFile && item.is_file() {
3550 self.file_name_input = item.file_name().to_string();
3551 self.file_name_input_error = self.validate_file_name_input();
3552 }
3553 }
3554
3555 fn select_next_visible_item_before(&mut self, item: &DirectoryEntry) -> bool {
3560 let mut return_val = false;
3561
3562 self.directory_content.reset_multi_selection();
3563
3564 let mut directory_content = std::mem::take(&mut self.directory_content);
3565 let search_value = std::mem::take(&mut self.search_value);
3566
3567 let index = directory_content
3568 .filtered_iter(&search_value)
3569 .position(|p| p.path_eq(item));
3570
3571 if let Some(index) = index {
3572 if index != 0 {
3573 if let Some(item) = directory_content
3574 .filtered_iter_mut(&search_value)
3575 .nth(index.saturating_sub(1))
3576 {
3577 self.select_item(item);
3578 self.scroll_to_selection = true;
3579 return_val = true;
3580 }
3581 }
3582 }
3583
3584 self.directory_content = directory_content;
3585 self.search_value = search_value;
3586
3587 return_val
3588 }
3589
3590 fn select_next_visible_item_after(&mut self, item: &DirectoryEntry) -> bool {
3595 let mut return_val = false;
3596
3597 self.directory_content.reset_multi_selection();
3598
3599 let mut directory_content = std::mem::take(&mut self.directory_content);
3600 let search_value = std::mem::take(&mut self.search_value);
3601
3602 let index = directory_content
3603 .filtered_iter(&search_value)
3604 .position(|p| p.path_eq(item));
3605
3606 if let Some(index) = index {
3607 if let Some(item) = directory_content
3608 .filtered_iter_mut(&search_value)
3609 .nth(index.saturating_add(1))
3610 {
3611 self.select_item(item);
3612 self.scroll_to_selection = true;
3613 return_val = true;
3614 }
3615 }
3616
3617 self.directory_content = directory_content;
3618 self.search_value = search_value;
3619
3620 return_val
3621 }
3622
3623 fn select_first_visible_item(&mut self) {
3625 self.directory_content.reset_multi_selection();
3626
3627 let mut directory_content = std::mem::take(&mut self.directory_content);
3628
3629 if let Some(item) = directory_content
3630 .filtered_iter_mut(&self.search_value.clone())
3631 .next()
3632 {
3633 self.select_item(item);
3634 self.scroll_to_selection = true;
3635 }
3636
3637 self.directory_content = directory_content;
3638 }
3639
3640 fn select_last_visible_item(&mut self) {
3642 self.directory_content.reset_multi_selection();
3643
3644 let mut directory_content = std::mem::take(&mut self.directory_content);
3645
3646 if let Some(item) = directory_content
3647 .filtered_iter_mut(&self.search_value.clone())
3648 .last()
3649 {
3650 self.select_item(item);
3651 self.scroll_to_selection = true;
3652 }
3653
3654 self.directory_content = directory_content;
3655 }
3656
3657 fn selection_limit_reached_with(&self, selected_count: usize) -> bool {
3659 self.config
3660 .max_selections
3661 .is_some_and(|max| selected_count >= max)
3662 }
3663
3664 fn select_all_items(&mut self) {
3666 let mut selected_count = self
3667 .directory_content
3668 .filtered_iter(&self.search_value)
3669 .filter(|p| p.selected)
3670 .count();
3671
3672 for item in self.directory_content.filtered_iter_mut(&self.search_value) {
3673 if item.selected {
3674 continue; }
3676 if self
3677 .config
3678 .max_selections
3679 .is_some_and(|max| selected_count >= max)
3680 {
3681 break;
3682 }
3683 item.selected = true;
3684 selected_count += 1;
3685 }
3686 }
3687
3688 fn open_path_edit(&mut self) {
3690 let path = self.current_directory().map_or_else(String::new, |path| {
3691 path.to_str().unwrap_or_default().to_string()
3692 });
3693
3694 self.path_edit_value = path;
3695 self.path_edit_activate = true;
3696 self.path_edit_visible = true;
3697 }
3698
3699 fn submit_path_edit(&mut self) {
3701 self.close_path_edit();
3702
3703 let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value));
3704
3705 if self.mode == DialogMode::PickFile && self.config.file_system.is_file(&path) {
3706 self.state = DialogState::Picked(path);
3707 return;
3708 }
3709
3710 if self.mode == DialogMode::SaveFile
3717 && (path.extension().is_some()
3718 || self.config.allow_path_edit_to_save_file_without_extension)
3719 && !self.config.file_system.is_dir(&path)
3720 && path.parent().is_some_and(std::path::Path::exists)
3721 {
3722 self.submit_save_file(path);
3723 return;
3724 }
3725
3726 self.load_directory(&path);
3727 }
3728
3729 const fn close_path_edit(&mut self) {
3732 self.path_edit_visible = false;
3733 }
3734
3735 fn load_next_directory(&mut self) {
3740 if self.directory_offset == 0 {
3741 return;
3743 }
3744
3745 self.directory_offset -= 1;
3746
3747 if let Some(path) = self.current_directory() {
3749 self.load_directory_content(path.to_path_buf().as_path());
3750 }
3751 }
3752
3753 fn load_previous_directory(&mut self) {
3757 if self.directory_offset + 1 >= self.directory_stack.len() {
3758 return;
3760 }
3761
3762 self.directory_offset += 1;
3763
3764 if let Some(path) = self.current_directory() {
3766 self.load_directory_content(path.to_path_buf().as_path());
3767 }
3768 }
3769
3770 fn load_parent_directory(&mut self) {
3774 if let Some(x) = self.current_directory() {
3775 if let Some(x) = x.to_path_buf().parent() {
3776 self.load_directory(x);
3777 }
3778 }
3779 }
3780
3781 fn reload_directory(&mut self) {
3788 if let Some(x) = self.current_directory() {
3789 self.load_directory_content(x.to_path_buf().as_path());
3790 }
3791 }
3792
3793 fn load_directory(&mut self, path: &Path) {
3799 if let Some(x) = self.current_directory() {
3802 if x == path {
3803 return;
3804 }
3805 }
3806
3807 if self.directory_offset != 0 && self.directory_stack.len() > self.directory_offset {
3808 self.directory_stack
3809 .drain(self.directory_stack.len() - self.directory_offset..);
3810 }
3811
3812 self.directory_stack.push(path.to_path_buf());
3813 self.directory_offset = 0;
3814
3815 self.load_directory_content(path);
3816
3817 self.search_value.clear();
3820 }
3821
3822 fn load_directory_content(&mut self, path: &Path) {
3824 self.storage.last_visited_dir = Some(path.to_path_buf());
3825
3826 let selected_file_filter = match self.mode {
3827 DialogMode::PickFile | DialogMode::PickMultiple => self.get_selected_file_filter(),
3828 _ => None,
3829 };
3830
3831 let selected_save_extension = if self.mode == DialogMode::SaveFile {
3832 self.get_selected_save_extension()
3833 .map(|e| e.file_extension.as_str())
3834 } else {
3835 None
3836 };
3837
3838 let filter = DirectoryFilter {
3839 show_files: self.show_files,
3840 show_hidden: self.storage.show_hidden,
3841 show_system_files: self.storage.show_system_files,
3842 file_filter: selected_file_filter.cloned(),
3843 filter_extension: selected_save_extension.map(str::to_string),
3844 };
3845
3846 self.directory_content = DirectoryContent::from_path(
3847 &self.config,
3848 path,
3849 self.config.file_system.clone(),
3850 filter,
3851 );
3852
3853 self.create_directory_dialog.close();
3854 self.scroll_to_selection = true;
3855
3856 if self.mode == DialogMode::SaveFile {
3857 self.file_name_input_error = self.validate_file_name_input();
3858 }
3859 }
3860
3861 fn should_open_directory(&self, path: &std::path::Path) -> bool {
3865 self.config
3866 .open_directory_filter
3867 .as_ref()
3868 .is_none_or(|f| f.matches(path))
3869 }
3870}
3871
3872#[cfg(test)]
3874const fn test_prop<T: Send + Sync>() {}
3875
3876#[test]
3877const fn test() {
3878 test_prop::<FileDialog>();
3879}
3880
3881#[cfg(test)]
3882mod open_directory_filter_tests {
3883 use std::path::Path;
3884
3885 use super::*;
3886
3887 #[test]
3888 fn filter_is_none_by_default() {
3889 let dialog = FileDialog::new();
3890 assert!(dialog.config.open_directory_filter.is_none());
3891 }
3892
3893 #[test]
3894 fn set_open_directory_filter_stores_filter() {
3895 let mut dialog = FileDialog::new();
3896 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3897 assert!(dialog.config.open_directory_filter.is_some());
3898 }
3899
3900 #[test]
3901 fn clear_open_directory_filter_removes_filter() {
3902 let mut dialog = FileDialog::new();
3903 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3904 assert!(dialog.config.open_directory_filter.is_some());
3905 dialog.clear_open_directory_filter();
3906 assert!(dialog.config.open_directory_filter.is_none());
3907 }
3908
3909 #[test]
3912 fn no_filter_always_navigates() {
3913 let dialog = FileDialog::new();
3914 assert!(dialog.should_open_directory(Path::new("/any/dir")));
3915 }
3916
3917 #[test]
3919 fn filter_returning_false_prevents_navigation() {
3920 let mut dialog = FileDialog::new();
3921 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3922 assert!(!dialog.should_open_directory(Path::new("/any/dir")));
3923 }
3924
3925 #[test]
3927 fn filter_returning_true_allows_navigation() {
3928 let mut dialog = FileDialog::new();
3929 dialog.set_open_directory_filter(Filter::new(|_: &Path| true));
3930 assert!(dialog.should_open_directory(Path::new("/any/dir")));
3931 }
3932
3933 #[test]
3935 fn cleared_filter_restores_default_navigation() {
3936 let mut dialog = FileDialog::new();
3937 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3938 assert!(!dialog.should_open_directory(Path::new("/any/dir")));
3939 dialog.clear_open_directory_filter();
3940 assert!(dialog.should_open_directory(Path::new("/any/dir")));
3941 }
3942
3943 #[test]
3947 fn filter_based_on_sentinel_file() -> Result<(), Box<dyn std::error::Error>> {
3948 use tempdir::TempDir;
3949 let tmp = TempDir::new("egui_fd_test")?;
3950 let project_dir = tmp.path().join("project");
3951 std::fs::create_dir_all(&project_dir)?;
3952 let sentinel = project_dir.join("project.json");
3953 std::fs::write(&sentinel, b"{}")?;
3954
3955 let regular_dir = tmp.path().join("regular");
3956 std::fs::create_dir_all(®ular_dir)?;
3957
3958 let mut dialog = FileDialog::new();
3959 dialog.set_open_directory_filter(Filter::new(|path: &Path| {
3961 !path.join("project.json").exists()
3962 }));
3963
3964 assert!(!dialog.should_open_directory(&project_dir));
3966 assert!(dialog.should_open_directory(®ular_dir));
3968 Ok(())
3970 }
3971}