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 if let Some(path) = &dropped_file.path {
1360 if self.config.file_system.is_dir(path) {
1361 self.load_directory(path.as_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
1379 if repaint {
1381 ctx.request_repaint();
1382 }
1383 }
1384
1385 fn ui_update_modal_background(&self, ctx: &egui::Context) -> egui::InnerResponse<()> {
1387 egui::Area::new(self.window_id.with("modal_overlay"))
1388 .interactable(true)
1389 .fixed_pos(egui::Pos2::ZERO)
1390 .show(ctx, |ui| {
1391 let content_rect = ctx.input(egui::InputState::content_rect);
1392
1393 ui.allocate_response(content_rect.size(), egui::Sense::click());
1394
1395 ui.painter().rect_filled(
1396 content_rect,
1397 egui::CornerRadius::ZERO,
1398 self.config.modal_overlay_color,
1399 );
1400 })
1401 }
1402
1403 fn ui_update_modals(&mut self, ui: &mut egui::Ui) {
1404 egui::Panel::bottom(self.window_id.with("modal_bottom_panel"))
1409 .resizable(false)
1410 .show_separator_line(false)
1411 .show(ui, |_| {});
1412
1413 egui::CentralPanel::default().show(ui, |ui| {
1416 if let Some(modal) = self.modals.last_mut() {
1417 #[allow(clippy::single_match)]
1418 match modal.update(&self.config, ui) {
1419 ModalState::Close(action) => {
1420 self.exec_modal_action(action);
1421 self.modals.pop();
1422 }
1423 ModalState::Pending => {}
1424 }
1425 }
1426 });
1427 }
1428
1429 fn create_window<'a>(&self, is_open: &'a mut bool) -> egui::Window<'a> {
1431 let mut window = egui::Window::new(self.get_window_title())
1432 .id(self.window_id)
1433 .open(is_open)
1434 .default_size(self.config.default_size)
1435 .min_size(self.config.min_size)
1436 .resizable(self.config.resizable)
1437 .movable(self.config.movable)
1438 .title_bar(self.config.title_bar)
1439 .collapsible(false);
1440
1441 if let Some(pos) = self.config.default_pos {
1442 window = window.default_pos(pos);
1443 }
1444
1445 if let Some(pos) = self.config.fixed_pos {
1446 window = window.fixed_pos(pos);
1447 }
1448
1449 if let Some((anchor, offset)) = self.config.anchor {
1450 window = window.anchor(anchor, offset);
1451 }
1452
1453 if let Some(size) = self.config.max_size {
1454 window = window.max_size(size);
1455 }
1456
1457 window
1458 }
1459
1460 const fn get_window_title(&self) -> &String {
1463 match &self.config.title {
1464 Some(title) => title,
1465 None => match &self.mode {
1466 DialogMode::PickDirectory => &self.config.labels.title_select_directory,
1467 DialogMode::PickFile => &self.config.labels.title_select_file,
1468 DialogMode::PickMultiple => &self.config.labels.title_select_multiple,
1469 DialogMode::SaveFile => &self.config.labels.title_save_file,
1470 },
1471 }
1472 }
1473
1474 fn ui_update_top_panel(&mut self, ui: &mut egui::Ui) {
1477 const STROKE_INNER_MARGIN: i8 = 5;
1478
1479 let text_height = ui.text_style_height(&egui::TextStyle::Body);
1480 let mut button_height = ui.spacing().button_padding.y.mul_add(2.0, text_height);
1481
1482 if button_height < 22.0 {
1483 button_height = 22.0;
1484 }
1485
1486 let content_height = f32::from(STROKE_INNER_MARGIN).mul_add(2.0, button_height);
1487 let square_button_size = egui::Vec2::new(button_height, button_height).mul(1.08);
1488
1489 ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1490 self.ui_update_nav_buttons(ui, square_button_size, content_height);
1491
1492 let mut path_display_width = ui.available_width();
1493
1494 if self.config.show_reload_button {
1496 path_display_width -= ui
1497 .spacing()
1498 .item_spacing
1499 .x
1500 .mul_add(2.0, square_button_size.x);
1501 }
1502
1503 if self.config.show_search {
1505 path_display_width -= 140.0;
1506 }
1507
1508 if path_display_width < 100.0 {
1509 path_display_width = 100.0;
1510 }
1511
1512 if self.config.show_current_path {
1513 self.ui_update_current_path(
1514 ui,
1515 path_display_width,
1516 STROKE_INNER_MARGIN,
1517 button_height,
1518 );
1519 }
1520
1521 let hamburger_menu_contains_items = self.config.show_reload_button
1522 || self.config.show_working_directory_button
1523 || self.config.show_select_all_button
1524 || self.config.show_hidden_option
1525 || self.config.show_system_files_option;
1526
1527 let hamburger_menu_visible =
1528 self.config.show_menu_button && hamburger_menu_contains_items;
1529
1530 if hamburger_menu_visible {
1531 self.ui_update_hamburger_menu(ui, square_button_size, content_height);
1532 }
1533
1534 if self.config.show_search {
1535 self.ui_update_search(ui, STROKE_INNER_MARGIN, button_height);
1536 }
1537 });
1538 }
1539
1540 fn ui_update_nav_buttons(
1541 &mut self,
1542 ui: &mut egui::Ui,
1543 button_size: egui::Vec2,
1544 content_height: f32,
1545 ) {
1546 ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
1547 ui.add_space((content_height - button_size.y) / 2.0);
1549
1550 ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1551 self.ui_update_nav_buttons_content(ui, button_size);
1552 });
1553 });
1554 }
1555
1556 fn ui_update_nav_buttons_content(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
1557 if self.config.show_parent_button {
1558 if let Some(x) = self.current_directory() {
1559 if self.ui_button_sized(
1560 ui,
1561 x.parent().is_some(),
1562 button_size,
1563 self.config.parent_directory_icon.as_str(),
1564 None,
1565 ) {
1566 self.load_parent_directory();
1567 }
1568 } else {
1569 let _ = self.ui_button_sized(
1570 ui,
1571 false,
1572 button_size,
1573 self.config.parent_directory_icon.as_str(),
1574 None,
1575 );
1576 }
1577 }
1578
1579 if self.config.show_back_button
1580 && self.ui_button_sized(
1581 ui,
1582 self.directory_offset + 1 < self.directory_stack.len(),
1583 button_size,
1584 self.config.back_icon.as_str(),
1585 None,
1586 )
1587 {
1588 self.load_previous_directory();
1589 }
1590
1591 if self.config.show_forward_button
1592 && self.ui_button_sized(
1593 ui,
1594 self.directory_offset != 0,
1595 button_size,
1596 self.config.forward_icon.as_str(),
1597 None,
1598 )
1599 {
1600 self.load_next_directory();
1601 }
1602
1603 if self.config.show_new_folder_button
1604 && self.ui_button_sized(
1605 ui,
1606 !self.create_directory_dialog.is_open(),
1607 button_size,
1608 self.config.new_folder_icon.as_str(),
1609 None,
1610 )
1611 {
1612 self.open_new_folder_dialog();
1613 }
1614 }
1615
1616 fn ui_update_current_path(
1620 &mut self,
1621 ui: &mut egui::Ui,
1622 width: f32,
1623 frame_inner_margin: i8,
1624 button_height: f32,
1625 ) {
1626 let stroke = egui::Stroke::new(1.0, ui.style().visuals.window_stroke.color);
1627
1628 egui::Frame::default()
1629 .stroke(stroke)
1630 .inner_margin(egui::Margin::same(frame_inner_margin - 1))
1631 .corner_radius(egui::CornerRadius::from(4))
1632 .show(ui, |ui| {
1633 if self.path_edit_visible {
1634 self.ui_update_path_edit(ui, width, button_height);
1635 } else {
1636 self.ui_update_path_display(ui, width, button_height);
1637 }
1638 });
1639 }
1640
1641 fn ui_update_path_display(&mut self, ui: &mut egui::Ui, mut width: f32, button_height: f32) {
1643 ui.style_mut().always_scroll_the_only_direction = true;
1644 ui.style_mut().spacing.scroll.bar_width = 8.0;
1645
1646 let edit_button_size = egui::Vec2::new(button_height, button_height);
1647
1648 if self.config.show_path_edit_button {
1650 width -= ui.spacing().item_spacing.x.mul_add(2.0, edit_button_size.x);
1651 }
1652
1653 egui::ScrollArea::horizontal()
1654 .auto_shrink([false, true])
1655 .stick_to_right(true)
1656 .max_width(width)
1657 .content_margin(egui::Margin::ZERO)
1658 .show(ui, |ui| {
1659 ui.horizontal(|ui| {
1660 ui.style_mut().spacing.item_spacing.x /= 2.5;
1661
1662 let mut path = PathBuf::new();
1663
1664 if let Some(data) = self.current_directory().map(Path::to_path_buf) {
1665 for (i, segment) in data.iter().enumerate() {
1666 path.push(segment);
1667
1668 let mut segment_str = segment.to_str().unwrap_or_default().to_string();
1669
1670 if self.is_pinned(&path) {
1671 segment_str =
1672 format!("{} {}", &self.config.pinned_icon, segment_str);
1673 }
1674
1675 if i != 0 {
1676 ui.label(self.config.directory_separator.as_str());
1677 }
1678
1679 let btn = egui::Button::new(segment_str);
1680 let re = ui.add_sized(egui::Vec2::new(0.0, button_height), btn);
1681
1682 if re.clicked() {
1683 self.load_directory(path.as_path());
1684 return;
1685 }
1686
1687 self.ui_update_central_panel_path_context_menu(&re, &path.clone());
1688 }
1689 }
1690 });
1691 });
1692
1693 if !self.config.show_path_edit_button {
1694 return;
1695 }
1696
1697 let button = egui::Button::new(&self.config.path_edit_icon)
1698 .fill(egui::Color32::TRANSPARENT)
1699 .wrap();
1700
1701 if ui.add_sized(edit_button_size, button).clicked() {
1702 self.open_path_edit();
1703 }
1704 }
1705
1706 fn ui_update_path_edit(&mut self, ui: &mut egui::Ui, mut width: f32, button_height: f32) {
1708 let edit_button_size = egui::Vec2::new(button_height, button_height);
1709 width -= ui.spacing().item_spacing.x.mul_add(2.0, edit_button_size.x);
1710
1711 let empty_space = button_height - ui.text_style_height(&egui::TextStyle::Body);
1713 let padding_top_bottom = empty_space / 2.0;
1714 #[allow(clippy::cast_possible_truncation)]
1715 let margin = egui::Margin::symmetric(4, padding_top_bottom.floor() as i8);
1716
1717 let frame = egui::Frame::dark_canvas(ui.style())
1718 .inner_margin(margin)
1719 .stroke(egui::Stroke::NONE);
1720
1721 let text_edit = egui::TextEdit::singleline(&mut self.path_edit_value)
1722 .desired_width(width)
1723 .frame(frame);
1724
1725 let response = text_edit.show(ui).response;
1726
1727 if self.path_edit_activate {
1728 response.request_focus();
1729 Self::set_cursor_to_end(&response, &self.path_edit_value);
1730 self.path_edit_activate = false;
1731 }
1732
1733 if self.path_edit_request_focus {
1734 response.request_focus();
1735 self.path_edit_request_focus = false;
1736 }
1737
1738 let btn = egui::Button::new("✔").wrap();
1739 let btn_response = ui.add_sized(edit_button_size, btn);
1740
1741 if btn_response.clicked() {
1742 self.submit_path_edit();
1743 }
1744
1745 if !response.has_focus() && !btn_response.contains_pointer() {
1746 self.path_edit_visible = false;
1747 }
1748 }
1749
1750 fn ui_update_hamburger_menu(
1752 &mut self,
1753 ui: &mut egui::Ui,
1754 button_size: egui::Vec2,
1755 content_height: f32,
1756 ) {
1757 use egui::containers::menu::{is_in_menu, MenuButton, SubMenuButton};
1758
1759 ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
1760 ui.add_space((content_height - button_size.y) / 2.0);
1762
1763 ui.horizontal(|ui| {
1764 let btn = egui::Button::new(&self.config.menu_icon).min_size(button_size);
1767
1768 if is_in_menu(ui) {
1769 SubMenuButton::new(&self.config.menu_icon).ui(ui, |ui| {
1770 self.ui_update_hamburger_menu_content(ui);
1771 });
1772 } else {
1773 MenuButton::from_button(btn).ui(ui, |ui| {
1774 self.ui_update_hamburger_menu_content(ui);
1775 });
1776 }
1777 });
1778 });
1779 }
1780
1781 fn ui_update_hamburger_menu_content(&mut self, ui: &mut egui::Ui) {
1783 const SEPARATOR_SPACING: f32 = 2.0;
1784
1785 let working_dir = self.config.file_system.current_dir();
1786
1787 let show_reload = self.config.show_reload_button;
1788 let show_working_dir = self.config.show_working_directory_button && working_dir.is_ok();
1789 let show_select_all =
1790 self.config.show_select_all_button && self.mode == DialogMode::PickMultiple;
1791
1792 let show_hidden = self.config.show_hidden_option;
1793 let show_system_files = self.config.show_system_files_option;
1794
1795 if show_reload && ui.button(&self.config.labels.reload).clicked() {
1796 self.refresh();
1797 ui.close();
1798 }
1799
1800 if show_working_dir && ui.button(&self.config.labels.working_directory).clicked() {
1801 self.load_directory(&working_dir.unwrap_or_default());
1802 ui.close();
1803 }
1804
1805 if show_select_all && ui.button(&self.config.labels.select_all).clicked() {
1806 self.select_all_items();
1807 ui.close();
1808 }
1809
1810 let any_above = show_reload || show_working_dir || show_select_all;
1811 let any_below = show_hidden || show_system_files;
1812
1813 if any_above && any_below {
1814 ui.add_space(SEPARATOR_SPACING);
1815 ui.separator();
1816 ui.add_space(SEPARATOR_SPACING);
1817 }
1818
1819 if show_hidden
1820 && ui
1821 .checkbox(
1822 &mut self.storage.show_hidden,
1823 &self.config.labels.show_hidden,
1824 )
1825 .clicked()
1826 {
1827 self.refresh();
1828 ui.close();
1829 }
1830
1831 if show_system_files
1832 && ui
1833 .checkbox(
1834 &mut self.storage.show_system_files,
1835 &self.config.labels.show_system_files,
1836 )
1837 .clicked()
1838 {
1839 self.refresh();
1840 ui.close();
1841 }
1842 }
1843
1844 fn ui_update_search(&mut self, ui: &mut egui::Ui, frame_inner_margin: i8, button_height: f32) {
1846 let stroke = egui::Stroke::new(1.0, ui.style().visuals.window_stroke.color);
1847
1848 let margin = egui::Margin {
1849 top: frame_inner_margin,
1850 bottom: frame_inner_margin,
1851 #[allow(clippy::cast_possible_truncation)]
1852 left: (f32::from(frame_inner_margin) * 1.5).floor() as i8,
1853 right: frame_inner_margin,
1854 };
1855
1856 egui::Frame::default()
1857 .stroke(stroke)
1858 .inner_margin(margin)
1859 .corner_radius(egui::CornerRadius::from(4))
1860 .show(ui, |ui| {
1861 ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1862 self.ui_update_search_content(ui, button_height);
1863 });
1864 });
1865 }
1866
1867 fn ui_update_search_content(&mut self, ui: &mut egui::Ui, button_height: f32) {
1868 ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
1869 let text_height = ui.text_style_height(&egui::TextStyle::Body);
1871 if text_height <= button_height {
1872 ui.add_space((button_height - text_height) / 2.0);
1873 }
1874
1875 ui.label(&self.config.search_icon);
1876 });
1877
1878 let empty_space = button_height - ui.text_style_height(&egui::TextStyle::Body);
1880 let padding_top_bottom = empty_space / 2.0;
1881 #[allow(clippy::cast_possible_truncation)]
1882 let margin = egui::Margin::symmetric(4, padding_top_bottom.floor() as i8);
1883
1884 let frame = egui::Frame::dark_canvas(ui.style())
1885 .inner_margin(margin)
1886 .stroke(egui::Stroke::NONE);
1887
1888 let text_edit = egui::TextEdit::singleline(&mut self.search_value)
1889 .desired_width(ui.available_width())
1890 .frame(frame);
1891
1892 let re = text_edit.show(ui).response;
1893
1894 self.edit_search_on_text_input(ui);
1895
1896 if re.changed() || self.init_search {
1897 self.selected_item = None;
1898 self.select_first_visible_item();
1899 }
1900
1901 if self.init_search {
1902 re.request_focus();
1903 Self::set_cursor_to_end(&re, &self.search_value);
1904 self.directory_content.reset_multi_selection();
1905
1906 self.init_search = false;
1907 }
1908 }
1909
1910 fn edit_search_on_text_input(&mut self, ui: &egui::Ui) {
1917 if ui.memory(|mem| mem.focused().is_some()) {
1918 return;
1919 }
1920
1921 ui.input(|inp| {
1922 if inp.modifiers.any() && !inp.modifiers.shift_only() {
1924 return;
1925 }
1926
1927 for text in inp.events.iter().filter_map(|ev| match ev {
1930 egui::Event::Text(t) => Some(t),
1931 _ => None,
1932 }) {
1933 self.search_value.push_str(text);
1934 self.init_search = true;
1935 }
1936 });
1937 }
1938
1939 fn ui_update_left_panel(&mut self, ui: &mut egui::Ui) {
1942 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
1943 const SPACING_MULTIPLIER: f32 = 4.0;
1945
1946 egui::containers::ScrollArea::vertical()
1947 .auto_shrink([false, false])
1948 .show(ui, |ui| {
1949 let mut spacing = ui.global_style().spacing.item_spacing.y * 2.0;
1951
1952 if self.config.show_pinned_folders && self.ui_update_pinned_folders(ui, spacing)
1954 {
1955 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1956 }
1957
1958 let quick_accesses = std::mem::take(&mut self.config.quick_accesses);
1960
1961 for quick_access in &quick_accesses {
1962 ui.add_space(spacing);
1963 self.ui_update_quick_access(ui, quick_access);
1964 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1965 }
1966
1967 self.config.quick_accesses = quick_accesses;
1968
1969 if self.config.show_places && self.ui_update_user_directories(ui, spacing) {
1971 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1972 }
1973
1974 let disks = std::mem::take(&mut self.system_disks);
1975
1976 if self.config.show_devices && self.ui_update_devices(ui, spacing, &disks) {
1977 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1978 }
1979
1980 if self.config.show_removable_devices
1981 && self.ui_update_removable_devices(ui, spacing, &disks)
1982 {
1983 }
1986
1987 self.system_disks = disks;
1988 });
1989 });
1990 }
1991
1992 fn ui_update_left_panel_entry(
1996 &mut self,
1997 ui: &mut egui::Ui,
1998 display_name: &str,
1999 path: &Path,
2000 ) -> egui::Response {
2001 let response = ui.selectable_label(self.current_directory() == Some(path), display_name);
2002
2003 if response.clicked() {
2004 self.load_directory(path);
2005 }
2006
2007 response
2008 }
2009
2010 fn ui_update_quick_access(&mut self, ui: &mut egui::Ui, quick_access: &QuickAccess) {
2012 ui.label(&quick_access.heading);
2013
2014 for entry in &quick_access.paths {
2015 self.ui_update_left_panel_entry(ui, &entry.display_name, &entry.path);
2016 }
2017 }
2018
2019 fn ui_update_pinned_folders(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
2024 let mut visible = false;
2025
2026 for (i, pinned) in self.storage.pinned_folders.clone().iter().enumerate() {
2027 if i == 0 {
2028 ui.add_space(spacing);
2029 ui.label(self.config.labels.heading_pinned.as_str());
2030
2031 visible = true;
2032 }
2033
2034 if self.is_pinned_folder_being_renamed(pinned) {
2035 self.ui_update_pinned_folder_rename(ui);
2036 continue;
2037 }
2038
2039 let response = self.ui_update_left_panel_entry(
2040 ui,
2041 &format!("{} {}", self.config.pinned_icon, &pinned.label),
2042 pinned.path.as_path(),
2043 );
2044
2045 self.ui_update_pinned_folder_context_menu(&response, pinned);
2046 }
2047
2048 visible
2049 }
2050
2051 fn ui_update_pinned_folder_rename(&mut self, ui: &mut egui::Ui) {
2052 if let Some(r) = &mut self.rename_pinned_folder {
2053 let id = self.window_id.with("pinned_folder_rename").with(&r.path);
2054 let mut output = egui::TextEdit::singleline(&mut r.label)
2055 .id(id)
2056 .cursor_at_end(true)
2057 .show(ui);
2058
2059 if self.rename_pinned_folder_request_focus {
2060 output.state.cursor.set_char_range(Some(CCursorRange::two(
2061 CCursor::new(0),
2062 CCursor::new(r.label.chars().count()),
2063 )));
2064 output.state.store(ui.ctx(), output.response.id);
2065
2066 output.response.request_focus();
2067
2068 self.rename_pinned_folder_request_focus = false;
2069 }
2070
2071 if output.response.lost_focus() {
2072 self.end_rename_pinned_folder();
2073 }
2074 }
2075 }
2076
2077 fn ui_update_pinned_folder_context_menu(
2078 &mut self,
2079 item: &egui::Response,
2080 pinned: &PinnedFolder,
2081 ) {
2082 item.context_menu(|ui| {
2083 if ui.button(&self.config.labels.unpin_folder).clicked() {
2084 self.unpin_path(&pinned.path);
2085 ui.close();
2086 }
2087
2088 if ui
2089 .button(&self.config.labels.rename_pinned_folder)
2090 .clicked()
2091 {
2092 self.begin_rename_pinned_folder(pinned.clone());
2093 ui.close();
2094 }
2095 });
2096 }
2097
2098 fn ui_update_user_directories(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
2103 let user_directories = std::mem::take(&mut self.user_directories);
2107 let labels = std::mem::take(&mut self.config.labels);
2108
2109 let visible = if let Some(dirs) = &user_directories {
2110 ui.add_space(spacing);
2111 ui.label(labels.heading_places.as_str());
2112
2113 if let Some(path) = dirs.home_dir() {
2114 self.ui_update_left_panel_entry(ui, &labels.home_dir, path);
2115 }
2116 if let Some(path) = dirs.desktop_dir() {
2117 self.ui_update_left_panel_entry(ui, &labels.desktop_dir, path);
2118 }
2119 if let Some(path) = dirs.document_dir() {
2120 self.ui_update_left_panel_entry(ui, &labels.documents_dir, path);
2121 }
2122 if let Some(path) = dirs.download_dir() {
2123 self.ui_update_left_panel_entry(ui, &labels.downloads_dir, path);
2124 }
2125 if let Some(path) = dirs.audio_dir() {
2126 self.ui_update_left_panel_entry(ui, &labels.audio_dir, path);
2127 }
2128 if let Some(path) = dirs.picture_dir() {
2129 self.ui_update_left_panel_entry(ui, &labels.pictures_dir, path);
2130 }
2131 if let Some(path) = dirs.video_dir() {
2132 self.ui_update_left_panel_entry(ui, &labels.videos_dir, path);
2133 }
2134
2135 true
2136 } else {
2137 false
2138 };
2139
2140 self.user_directories = user_directories;
2141 self.config.labels = labels;
2142
2143 visible
2144 }
2145
2146 fn ui_update_devices(&mut self, ui: &mut egui::Ui, spacing: f32, disks: &Disks) -> bool {
2151 let mut visible = false;
2152
2153 for (i, disk) in disks.iter().filter(|x| !x.is_removable()).enumerate() {
2154 if i == 0 {
2155 ui.add_space(spacing);
2156 ui.label(self.config.labels.heading_devices.as_str());
2157
2158 visible = true;
2159 }
2160
2161 self.ui_update_device_entry(ui, disk);
2162 }
2163
2164 visible
2165 }
2166
2167 fn ui_update_removable_devices(
2172 &mut self,
2173 ui: &mut egui::Ui,
2174 spacing: f32,
2175 disks: &Disks,
2176 ) -> bool {
2177 let mut visible = false;
2178
2179 for (i, disk) in disks.iter().filter(|x| x.is_removable()).enumerate() {
2180 if i == 0 {
2181 ui.add_space(spacing);
2182 ui.label(self.config.labels.heading_removable_devices.as_str());
2183
2184 visible = true;
2185 }
2186
2187 self.ui_update_device_entry(ui, disk);
2188 }
2189
2190 visible
2191 }
2192
2193 fn ui_update_device_entry(&mut self, ui: &mut egui::Ui, device: &Disk) {
2195 let label = if device.is_removable() {
2196 format!(
2197 "{} {}",
2198 self.config.removable_device_icon,
2199 device.display_name()
2200 )
2201 } else {
2202 format!("{} {}", self.config.device_icon, device.display_name())
2203 };
2204
2205 self.ui_update_left_panel_entry(ui, &label, device.mount_point());
2206 }
2207
2208 fn ui_update_bottom_panel(&mut self, ui: &mut egui::Ui) {
2210 const BUTTON_HEIGHT: f32 = 20.0;
2211 ui.add_space(5.0);
2212
2213 let label_submit_width = match self.mode {
2215 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2216 Self::calc_text_width(ui, &self.config.labels.open_button)
2217 }
2218 DialogMode::SaveFile => Self::calc_text_width(ui, &self.config.labels.save_button),
2219 };
2220
2221 let mut btn_width = Self::calc_text_width(ui, &self.config.labels.cancel_button);
2222 if label_submit_width > btn_width {
2223 btn_width = label_submit_width;
2224 }
2225
2226 btn_width = ui.spacing().button_padding.x.mul_add(4.0, btn_width);
2227
2228 let button_size: egui::Vec2 = egui::Vec2::new(btn_width, BUTTON_HEIGHT);
2230
2231 self.ui_update_selection_preview(ui, button_size);
2232
2233 if self.mode == DialogMode::SaveFile && self.config.save_extensions.is_empty() {
2234 ui.add_space(ui.style().spacing.item_spacing.y);
2235 }
2236
2237 self.ui_update_action_buttons(ui, button_size);
2238 }
2239
2240 fn ui_update_selection_preview(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2243 const SELECTION_PREVIEW_MIN_WIDTH: f32 = 50.0;
2244 let item_spacing = ui.style().spacing.item_spacing;
2245
2246 let render_filter_selection = (!self.config.file_filters.is_empty()
2247 && (self.mode == DialogMode::PickFile || self.mode == DialogMode::PickMultiple))
2248 || (!self.config.save_extensions.is_empty() && self.mode == DialogMode::SaveFile);
2249
2250 let filter_selection_width = button_size.x.mul_add(2.0, item_spacing.y);
2251 let mut filter_selection_separate_line = false;
2252
2253 ui.horizontal(|ui| {
2254 match &self.mode {
2255 DialogMode::PickDirectory => ui.label(&self.config.labels.selected_directory),
2256 DialogMode::PickFile => ui.label(&self.config.labels.selected_file),
2257 DialogMode::PickMultiple => ui.label(&self.config.labels.selected_items),
2258 DialogMode::SaveFile => ui.label(&self.config.labels.file_name),
2259 };
2260
2261 let mut scroll_bar_width: f32 =
2266 ui.available_width() - filter_selection_width - item_spacing.x;
2267
2268 if scroll_bar_width < SELECTION_PREVIEW_MIN_WIDTH || !render_filter_selection {
2269 filter_selection_separate_line = true;
2270 scroll_bar_width = ui.available_width();
2271 }
2272
2273 match &self.mode {
2274 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2275 use egui::containers::scroll_area::ScrollBarVisibility;
2276
2277 let text = self.get_selection_preview_text();
2278
2279 egui::containers::ScrollArea::horizontal()
2280 .auto_shrink([false, false])
2281 .max_width(scroll_bar_width)
2282 .stick_to_right(true)
2283 .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
2284 .show(ui, |ui| {
2285 ui.colored_label(ui.style().visuals.selection.bg_fill, text);
2286 });
2287 }
2288 DialogMode::SaveFile => {
2289 let mut output = egui::TextEdit::singleline(&mut self.file_name_input)
2290 .cursor_at_end(false)
2291 .margin(egui::Margin::symmetric(4, 3))
2292 .desired_width(scroll_bar_width)
2293 .show(ui);
2294
2295 if self.file_name_input_request_focus {
2296 self.highlight_file_name_input(&mut output);
2297 output.state.store(ui.ctx(), output.response.id);
2298
2299 output.response.request_focus();
2300 self.file_name_input_request_focus = false;
2301 }
2302
2303 if output.response.changed() {
2304 self.file_name_input_error = self.validate_file_name_input();
2305 }
2306
2307 if output.response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
2308 {
2309 self.submit();
2310 }
2311 }
2312 }
2313
2314 if !filter_selection_separate_line && render_filter_selection {
2315 if self.mode == DialogMode::SaveFile {
2316 self.ui_update_save_extension_selection(ui, filter_selection_width);
2317 } else {
2318 self.ui_update_file_filter_selection(ui, filter_selection_width);
2319 }
2320 }
2321 });
2322
2323 if filter_selection_separate_line && render_filter_selection {
2324 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2325 if self.mode == DialogMode::SaveFile {
2326 self.ui_update_save_extension_selection(ui, filter_selection_width);
2327 } else {
2328 self.ui_update_file_filter_selection(ui, filter_selection_width);
2329 }
2330 });
2331 }
2332 }
2333
2334 fn highlight_file_name_input(&self, output: &mut egui::text_edit::TextEditOutput) {
2338 if let Some(pos) = self.file_name_input.rfind('.') {
2339 let range = if pos == 0 {
2340 CCursorRange::two(CCursor::new(0), CCursor::new(0))
2341 } else {
2342 CCursorRange::two(CCursor::new(0), CCursor::new(pos))
2343 };
2344
2345 output.state.cursor.set_char_range(Some(range));
2346 }
2347 }
2348
2349 fn get_selection_preview_text(&self) -> String {
2350 if self.is_selection_valid() {
2351 match &self.mode {
2352 DialogMode::PickDirectory | DialogMode::PickFile => self
2353 .selected_item
2354 .as_ref()
2355 .map_or_else(String::new, |item| item.file_name().to_string()),
2356 DialogMode::PickMultiple => {
2357 let mut result = String::new();
2358
2359 for (i, item) in self
2360 .get_dir_content_filtered_iter()
2361 .filter(|p| p.selected)
2362 .enumerate()
2363 {
2364 if i == 0 {
2365 result += item.file_name();
2366 continue;
2367 }
2368
2369 result += format!(", {}", item.file_name()).as_str();
2370 }
2371
2372 result
2373 }
2374 DialogMode::SaveFile => String::new(),
2375 }
2376 } else {
2377 String::new()
2378 }
2379 }
2380
2381 fn ui_update_file_filter_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2382 let selected_filter = self.get_selected_file_filter();
2383 let selected_text = match selected_filter {
2384 Some(f) => &f.name,
2385 None => &self.config.labels.file_filter_all_files,
2386 };
2387
2388 let mut select_filter: Option<Option<FileFilter>> = None;
2391
2392 egui::containers::ComboBox::from_id_salt(self.window_id.with("file_filter_selection"))
2393 .width(width)
2394 .selected_text(selected_text)
2395 .wrap_mode(egui::TextWrapMode::Truncate)
2396 .show_ui(ui, |ui| {
2397 for filter in &self.config.file_filters {
2398 let selected = selected_filter.is_some_and(|f| f.id == filter.id);
2399
2400 if ui.selectable_label(selected, &filter.name).clicked() {
2401 select_filter = Some(Some(filter.clone()));
2402 }
2403 }
2404
2405 if self.config.show_all_files_filter
2406 && ui
2407 .selectable_label(
2408 selected_filter.is_none(),
2409 &self.config.labels.file_filter_all_files,
2410 )
2411 .clicked()
2412 {
2413 select_filter = Some(None);
2414 }
2415 });
2416
2417 if let Some(i) = select_filter {
2418 self.select_file_filter(i);
2419 }
2420 }
2421
2422 fn ui_update_save_extension_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2423 let selected_extension = self.get_selected_save_extension();
2424 let selected_text = match selected_extension {
2425 Some(e) => &e.to_string(),
2426 None => &self.config.labels.save_extension_any,
2427 };
2428
2429 let mut select_extension: Option<Option<SaveExtension>> = None;
2432
2433 egui::containers::ComboBox::from_id_salt(self.window_id.with("save_extension_selection"))
2434 .width(width)
2435 .selected_text(selected_text)
2436 .wrap_mode(egui::TextWrapMode::Truncate)
2437 .show_ui(ui, |ui| {
2438 for extension in &self.config.save_extensions {
2439 let selected = selected_extension.is_some_and(|s| s.id == extension.id);
2440
2441 if ui
2442 .selectable_label(selected, extension.to_string())
2443 .clicked()
2444 {
2445 select_extension = Some(Some(extension.clone()));
2446 }
2447 }
2448 });
2449
2450 if let Some(i) = select_extension {
2451 self.file_name_input_request_focus = true;
2452 self.select_save_extension(i);
2453 }
2454 }
2455
2456 fn ui_update_action_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2458 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2459 let label = match &self.mode {
2460 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2461 self.config.labels.open_button.as_str()
2462 }
2463 DialogMode::SaveFile => self.config.labels.save_button.as_str(),
2464 };
2465
2466 ui.spacing_mut().item_spacing.x = ui.spacing_mut().item_spacing.y;
2467
2468 if self.ui_button_sized(
2469 ui,
2470 self.is_selection_valid(),
2471 button_size,
2472 label,
2473 self.file_name_input_error.as_deref(),
2474 ) {
2475 self.submit();
2476 }
2477
2478 if ui
2479 .add_sized(
2480 button_size,
2481 egui::Button::new(self.config.labels.cancel_button.as_str()),
2482 )
2483 .clicked()
2484 {
2485 self.cancel();
2486 }
2487 });
2488 }
2489
2490 fn ui_update_central_panel(&mut self, ui: &mut egui::Ui) {
2493 if self.update_directory_content(ui) {
2494 return;
2495 }
2496
2497 self.ui_update_central_panel_content(ui);
2498 }
2499
2500 fn update_directory_content(&mut self, ui: &mut egui::Ui) -> bool {
2505 const SHOW_SPINNER_AFTER: f32 = 0.2;
2506
2507 match self.directory_content.update() {
2508 DirectoryContentState::Pending(timestamp) => {
2509 let now = std::time::SystemTime::now();
2510
2511 if now
2512 .duration_since(*timestamp)
2513 .unwrap_or_default()
2514 .as_secs_f32()
2515 > SHOW_SPINNER_AFTER
2516 {
2517 ui.centered_and_justified(egui::Ui::spinner);
2518 }
2519
2520 ui.ctx().request_repaint();
2522
2523 true
2524 }
2525 DirectoryContentState::Errored(err) => {
2526 ui.centered_and_justified(|ui| ui.colored_label(ui.visuals().error_fg_color, err));
2527 true
2528 }
2529 DirectoryContentState::Finished => {
2530 if self.mode == DialogMode::PickDirectory {
2531 if let Some(dir) = self.current_directory() {
2532 let mut dir_entry =
2533 DirectoryEntry::from_path(&self.config, dir, &*self.config.file_system);
2534 self.select_item(&mut dir_entry);
2535 }
2536 }
2537
2538 false
2539 }
2540 DirectoryContentState::Success => false,
2541 }
2542 }
2543
2544 fn ui_update_central_panel_content(&mut self, ui: &mut egui::Ui) {
2547 let mut data = std::mem::take(&mut self.directory_content);
2549
2550 let mut selected_count = data
2553 .filtered_iter(&self.search_value)
2554 .filter(|item| item.selected)
2555 .count();
2556
2557 let mut reset_multi_selection = false;
2560
2561 let mut batch_select_item_b: Option<DirectoryEntry> = None;
2564
2565 let mut should_return = false;
2567
2568 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
2569 let scroll_area = egui::containers::ScrollArea::vertical().auto_shrink([false, false]);
2570
2571 if self.search_value.is_empty()
2572 && !self.create_directory_dialog.is_open()
2573 && !self.scroll_to_selection
2574 {
2575 let row_height = ui
2580 .spacing()
2581 .button_padding
2582 .y
2583 .mul_add(2.0, ui.text_style_height(&egui::TextStyle::Body));
2584
2585 scroll_area.show_rows(ui, row_height, data.len(), |ui, range| {
2586 for item in data.iter_range_mut(range) {
2587 if self.ui_update_central_panel_entry(
2588 ui,
2589 item,
2590 &mut reset_multi_selection,
2591 &mut batch_select_item_b,
2592 &mut selected_count,
2593 ) {
2594 should_return = true;
2595 }
2596 }
2597 });
2598 } else {
2599 scroll_area.show(ui, |ui| {
2605 for item in data.filtered_iter_mut(&self.search_value.clone()) {
2606 if self.ui_update_central_panel_entry(
2607 ui,
2608 item,
2609 &mut reset_multi_selection,
2610 &mut batch_select_item_b,
2611 &mut selected_count,
2612 ) {
2613 should_return = true;
2614 }
2615 }
2616
2617 if let Some(entry) = self.ui_update_create_directory_dialog(ui) {
2618 data.push(entry);
2619 }
2620 });
2621 }
2622 });
2623
2624 if should_return {
2625 return;
2626 }
2627
2628 if reset_multi_selection {
2630 for item in data.filtered_iter_mut(&self.search_value) {
2631 if let Some(selected_item) = &self.selected_item {
2632 if selected_item.path_eq(item) {
2633 continue;
2634 }
2635 }
2636
2637 item.selected = false;
2638 }
2639 }
2640
2641 if let Some(item_b) = batch_select_item_b {
2643 if let Some(item_a) = &self.selected_item {
2644 self.batch_select_between(&mut data, item_a, &item_b);
2645 }
2646 }
2647
2648 self.directory_content = data;
2649 self.scroll_to_selection = false;
2650 }
2651
2652 fn ui_update_central_panel_entry(
2655 &mut self,
2656 ui: &mut egui::Ui,
2657 item: &mut DirectoryEntry,
2658 reset_multi_selection: &mut bool,
2659 batch_select_item_b: &mut Option<DirectoryEntry>,
2660 selected_count: &mut usize,
2661 ) -> bool {
2662 let file_name = item.file_name();
2663 let primary_selected = self.is_primary_selected(item);
2664 let pinned = self.is_pinned(item.as_path());
2665
2666 let icons = if pinned {
2667 format!("{} {} ", item.icon(), self.config.pinned_icon)
2668 } else {
2669 format!("{} ", item.icon())
2670 };
2671
2672 let icons_width = Self::calc_text_width(ui, &icons);
2673
2674 let available_width = ui.available_width() - icons_width - 15.0;
2676
2677 let truncate = self.config.truncate_filenames
2678 && available_width < Self::calc_text_width(ui, file_name);
2679
2680 let text = if truncate {
2681 Self::truncate_filename(ui, item, available_width)
2682 } else {
2683 file_name.to_owned()
2684 };
2685
2686 let mut re =
2687 ui.selectable_label(primary_selected || item.selected, format!("{icons}{text}"));
2688
2689 if truncate {
2690 re = re.on_hover_text(file_name);
2691 }
2692
2693 if item.is_dir() {
2694 self.ui_update_central_panel_path_context_menu(&re, item.as_path());
2695
2696 if re.context_menu_opened() {
2697 self.select_item(item);
2698 }
2699 }
2700
2701 if primary_selected && self.scroll_to_selection {
2702 re.scroll_to_me(Some(egui::Align::Center));
2703 self.scroll_to_selection = false;
2704 }
2705
2706 if re.clicked()
2708 && !ui.input(|i| i.modifiers.command)
2709 && !ui.input(|i| i.modifiers.shift_only())
2710 {
2711 self.select_item(item);
2712
2713 if self.mode == DialogMode::PickMultiple {
2715 *reset_multi_selection = true;
2716 }
2717 }
2718
2719 if self.mode == DialogMode::PickMultiple
2722 && re.clicked()
2723 && ui.input(|i| i.modifiers.command)
2724 {
2725 if primary_selected {
2726 item.selected = false;
2729 self.selected_item = None;
2730 *selected_count = selected_count.saturating_sub(1);
2731 } else if !item.selected && self.selection_limit_reached_with(*selected_count) {
2732 } else {
2734 let was_selected = item.selected;
2735 item.selected = !item.selected;
2736
2737 if item.selected {
2738 *selected_count += 1;
2739 self.select_item(item);
2741 } else if was_selected {
2742 *selected_count = selected_count.saturating_sub(1);
2743 }
2744 }
2745 }
2746
2747 if self.mode == DialogMode::PickMultiple
2750 && re.clicked()
2751 && ui.input(|i| i.modifiers.shift_only())
2752 {
2753 if self.selection_limit_reached_with(*selected_count) && !item.selected {
2754 } else if let Some(selected_item) = self.selected_item.clone() {
2756 *batch_select_item_b = Some(selected_item);
2759
2760 if !item.selected {
2762 *selected_count += 1;
2763 }
2764 item.selected = true;
2765 self.select_item(item);
2766 }
2767 }
2768
2769 if re.double_clicked() && !ui.input(|i| i.modifiers.command) {
2772 if item.is_dir() {
2773 if self.should_open_directory(item.as_path()) {
2776 self.load_directory(&item.to_path_buf());
2777 return true;
2778 }
2779 }
2781
2782 self.select_item(item);
2783
2784 self.submit();
2785 }
2786
2787 false
2788 }
2789
2790 fn ui_update_create_directory_dialog(&mut self, ui: &mut egui::Ui) -> Option<DirectoryEntry> {
2791 self.create_directory_dialog
2792 .update(ui, &self.config)
2793 .directory()
2794 .map(|path| self.process_new_folder(&path))
2795 }
2796
2797 fn batch_select_between(
2800 &self,
2801 directory_content: &mut DirectoryContent,
2802 item_a: &DirectoryEntry,
2803 item_b: &DirectoryEntry,
2804 ) {
2805 let pos_a = directory_content
2807 .filtered_iter(&self.search_value)
2808 .position(|p| p.path_eq(item_a));
2809 let pos_b = directory_content
2810 .filtered_iter(&self.search_value)
2811 .position(|p| p.path_eq(item_b));
2812
2813 if let Some(pos_a) = pos_a {
2816 if let Some(pos_b) = pos_b {
2817 if pos_a == pos_b {
2818 return;
2819 }
2820
2821 let mut min = pos_a;
2824 let mut max = pos_b;
2825
2826 if min > max {
2827 min = pos_b;
2828 max = pos_a;
2829 }
2830
2831 let mut current_selected = directory_content
2834 .filtered_iter(&self.search_value)
2835 .filter(|item| item.selected)
2836 .count();
2837
2838 for item in directory_content
2839 .filtered_iter_mut(&self.search_value)
2840 .enumerate()
2841 .filter(|(i, _)| i > &min && i < &max)
2842 .map(|(_, p)| p)
2843 {
2844 if self.selection_limit_reached_with(current_selected) {
2845 break;
2846 }
2847 if !item.selected {
2848 current_selected += 1;
2849 }
2850 item.selected = true;
2851 }
2852 }
2853 }
2854 }
2855
2856 fn ui_button_sized(
2858 &self,
2859 ui: &mut egui::Ui,
2860 enabled: bool,
2861 size: egui::Vec2,
2862 label: &str,
2863 err_tooltip: Option<&str>,
2864 ) -> bool {
2865 let mut clicked = false;
2866
2867 ui.add_enabled_ui(enabled, |ui| {
2868 let response = ui.add_sized(size, egui::Button::new(label));
2869 clicked = response.clicked();
2870
2871 if let Some(err) = err_tooltip {
2872 response.on_disabled_hover_ui(|ui| {
2873 ui.horizontal_wrapped(|ui| {
2874 ui.spacing_mut().item_spacing.x = 0.0;
2875
2876 ui.colored_label(
2877 ui.global_style().visuals.error_fg_color,
2878 format!("{} ", self.config.err_icon),
2879 );
2880
2881 ui.label(err);
2882 });
2883 });
2884 }
2885 });
2886
2887 clicked
2888 }
2889
2890 fn ui_update_central_panel_path_context_menu(&mut self, item: &egui::Response, path: &Path) {
2897 if !self.config.show_pinned_folders {
2899 return;
2900 }
2901
2902 item.context_menu(|ui| {
2903 let pinned = self.is_pinned(path);
2904
2905 if pinned {
2906 if ui.button(&self.config.labels.unpin_folder).clicked() {
2907 self.unpin_path(path);
2908 ui.close();
2909 }
2910 } else if ui.button(&self.config.labels.pin_folder).clicked() {
2911 self.pin_path(path.to_path_buf());
2912 ui.close();
2913 }
2914 });
2915 }
2916
2917 fn set_cursor_to_end(re: &egui::Response, data: &str) {
2924 if let Some(mut state) = egui::TextEdit::load_state(&re.ctx, re.id) {
2926 state
2927 .cursor
2928 .set_char_range(Some(CCursorRange::one(CCursor::new(data.len()))));
2929 state.store(&re.ctx, re.id);
2930 }
2931 }
2932
2933 fn calc_char_width(ui: &egui::Ui, char: char) -> f32 {
2935 ui.fonts_mut(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char))
2936 }
2937
2938 fn calc_text_width(ui: &egui::Ui, text: &str) -> f32 {
2941 let mut width = 0.0;
2942
2943 for char in text.chars() {
2944 width += Self::calc_char_width(ui, char);
2945 }
2946
2947 width
2948 }
2949
2950 fn truncate_filename(ui: &egui::Ui, item: &DirectoryEntry, max_length: f32) -> String {
2951 const TRUNCATE_STR: &str = "...";
2952
2953 let path = item.as_path();
2954
2955 let file_stem = if item.is_file() {
2956 path.file_stem().and_then(|f| f.to_str()).unwrap_or("")
2957 } else {
2958 item.file_name()
2959 };
2960
2961 let extension = if item.is_file() {
2962 path.extension().map_or(String::new(), |ext| {
2963 format!(".{}", ext.to_str().unwrap_or(""))
2964 })
2965 } else {
2966 String::new()
2967 };
2968
2969 let extension_width = Self::calc_text_width(ui, &extension);
2970 let reserved = extension_width + Self::calc_text_width(ui, TRUNCATE_STR);
2971
2972 if max_length <= reserved {
2973 return format!("{TRUNCATE_STR}{extension}");
2974 }
2975
2976 let mut width = reserved;
2977 let mut front = String::new();
2978 let mut back = String::new();
2979
2980 for (i, char) in file_stem.chars().enumerate() {
2981 let w = Self::calc_char_width(ui, char);
2982
2983 if width + w > max_length {
2984 break;
2985 }
2986
2987 front.push(char);
2988 width += w;
2989
2990 let back_index = file_stem.len() - i - 1;
2991
2992 if back_index <= i {
2993 break;
2994 }
2995
2996 if let Some(char) = file_stem.chars().nth(back_index) {
2997 let w = Self::calc_char_width(ui, char);
2998
2999 if width + w > max_length {
3000 break;
3001 }
3002
3003 back.push(char);
3004 width += w;
3005 }
3006 }
3007
3008 format!(
3009 "{front}{TRUNCATE_STR}{}{extension}",
3010 back.chars().rev().collect::<String>()
3011 )
3012 }
3013}
3014
3015impl FileDialog {
3017 fn update_keybindings(&mut self, ctx: &egui::Context) {
3019 if let Some(modal) = self.modals.last_mut() {
3022 modal.update_keybindings(&self.config, ctx);
3023 return;
3024 }
3025
3026 let keybindings = std::mem::take(&mut self.config.keybindings);
3027
3028 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.submit, false) {
3029 self.exec_keybinding_submit();
3030 }
3031
3032 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.cancel, false) {
3033 self.exec_keybinding_cancel();
3034 }
3035
3036 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.parent, true) {
3037 self.load_parent_directory();
3038 }
3039
3040 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.back, true) {
3041 self.load_previous_directory();
3042 }
3043
3044 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.forward, true) {
3045 self.load_next_directory();
3046 }
3047
3048 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.reload, true) {
3049 self.refresh();
3050 }
3051
3052 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.new_folder, true) {
3053 self.open_new_folder_dialog();
3054 }
3055
3056 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.edit_path, true) {
3057 self.open_path_edit();
3058 }
3059
3060 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.home_edit_path, true) {
3061 if let Some(dirs) = &self.user_directories {
3062 if let Some(home) = dirs.home_dir() {
3063 self.load_directory(home.to_path_buf().as_path());
3064 self.open_path_edit();
3065 }
3066 }
3067 }
3068
3069 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_up, false) {
3070 self.exec_keybinding_selection_up();
3071
3072 if let Some(id) = ctx.memory(egui::Memory::focused) {
3074 ctx.memory_mut(|w| w.surrender_focus(id));
3075 }
3076 }
3077
3078 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_down, false) {
3079 self.exec_keybinding_selection_down();
3080
3081 if let Some(id) = ctx.memory(egui::Memory::focused) {
3083 ctx.memory_mut(|w| w.surrender_focus(id));
3084 }
3085 }
3086
3087 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.select_all, true)
3088 && self.mode == DialogMode::PickMultiple
3089 {
3090 self.select_all_items();
3091 }
3092
3093 self.config.keybindings = keybindings;
3094 }
3095
3096 fn exec_keybinding_submit(&mut self) {
3098 if self.path_edit_visible {
3099 self.submit_path_edit();
3100 return;
3101 }
3102
3103 if self.create_directory_dialog.is_open() {
3104 if let Some(dir) = self.create_directory_dialog.submit().directory() {
3105 self.process_new_folder(&dir);
3106 }
3107 return;
3108 }
3109
3110 if self.any_focused_last_frame {
3111 return;
3112 }
3113
3114 if let Some(item) = &self.selected_item {
3116 let is_visible = self
3118 .get_dir_content_filtered_iter()
3119 .any(|p| p.path_eq(item));
3120
3121 if is_visible && item.is_dir() {
3122 self.load_directory(&item.to_path_buf());
3123 return;
3124 }
3125 }
3126
3127 self.submit();
3128 }
3129
3130 fn exec_keybinding_cancel(&mut self) {
3132 if self.create_directory_dialog.is_open() {
3150 self.create_directory_dialog.close();
3151 } else if self.path_edit_visible {
3152 self.close_path_edit();
3153 } else if !self.any_focused_last_frame {
3154 self.cancel();
3155 }
3156 }
3157
3158 fn exec_keybinding_selection_up(&mut self) {
3160 if self.directory_content.len() == 0 {
3161 return;
3162 }
3163
3164 self.directory_content.reset_multi_selection();
3165
3166 if let Some(item) = &self.selected_item {
3167 if self.select_next_visible_item_before(&item.clone()) {
3168 return;
3169 }
3170 }
3171
3172 self.select_last_visible_item();
3175 }
3176
3177 fn exec_keybinding_selection_down(&mut self) {
3179 if self.directory_content.len() == 0 {
3180 return;
3181 }
3182
3183 self.directory_content.reset_multi_selection();
3184
3185 if let Some(item) = &self.selected_item {
3186 if self.select_next_visible_item_after(&item.clone()) {
3187 return;
3188 }
3189 }
3190
3191 self.select_first_visible_item();
3194 }
3195}
3196
3197impl FileDialog {
3199 fn get_selected_file_filter(&self) -> Option<&FileFilter> {
3201 self.selected_file_filter
3202 .and_then(|id| self.config.file_filters.iter().find(|p| p.id == id))
3203 }
3204
3205 fn set_default_file_filter(&mut self) {
3207 if let Some(name) = &self.config.default_file_filter {
3208 for filter in &self.config.file_filters {
3209 if filter.name == name.as_str() {
3210 self.selected_file_filter = Some(filter.id);
3211 }
3212 }
3213 }
3214 }
3215
3216 fn select_file_filter(&mut self, filter: Option<FileFilter>) {
3218 self.selected_file_filter = filter.map(|f| f.id);
3219 self.selected_item = None;
3220 self.refresh();
3221 }
3222
3223 fn get_selected_save_extension(&self) -> Option<&SaveExtension> {
3225 self.selected_save_extension
3226 .and_then(|id| self.config.save_extensions.iter().find(|p| p.id == id))
3227 }
3228
3229 fn set_default_save_extension(&mut self) {
3231 let config = std::mem::take(&mut self.config);
3232
3233 if let Some(name) = &config.default_save_extension {
3234 for extension in &config.save_extensions {
3235 if extension.name == name.as_str() {
3236 self.selected_save_extension = Some(extension.id);
3237 self.set_file_name_extension(&extension.file_extension);
3238 }
3239 }
3240 }
3241
3242 self.config = config;
3243 }
3244
3245 fn select_save_extension(&mut self, extension: Option<SaveExtension>) {
3247 if let Some(ex) = extension {
3248 self.selected_save_extension = Some(ex.id);
3249 self.set_file_name_extension(&ex.file_extension);
3250 }
3251
3252 self.selected_item = None;
3253 self.refresh();
3254 }
3255
3256 fn set_file_name_extension(&mut self, extension: &str) {
3258 let dot_count = self.file_name_input.chars().filter(|c| *c == '.').count();
3262 let use_simple = dot_count == 1 && self.file_name_input.chars().nth(0) == Some('.');
3263
3264 let mut p = PathBuf::from(&self.file_name_input);
3265 if !use_simple && p.set_extension(extension) {
3266 self.file_name_input = p.to_string_lossy().into_owned();
3267 } else {
3268 self.file_name_input = format!(".{extension}");
3269 }
3270 }
3271
3272 fn get_dir_content_filtered_iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
3274 self.directory_content.filtered_iter(&self.search_value)
3275 }
3276
3277 fn open_new_folder_dialog(&mut self) {
3279 if let Some(x) = self.current_directory() {
3280 self.create_directory_dialog.open(x.to_path_buf());
3281 }
3282 }
3283
3284 fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry {
3286 let mut entry =
3287 DirectoryEntry::from_path(&self.config, created_dir, &*self.config.file_system);
3288
3289 self.directory_content.push(entry.clone());
3290
3291 self.select_item(&mut entry);
3292
3293 entry
3294 }
3295
3296 fn open_modal(&mut self, modal: Box<dyn FileDialogModal + Send + Sync>) {
3298 self.modals.push(modal);
3299 }
3300
3301 fn exec_modal_action(&mut self, action: ModalAction) {
3303 match action {
3304 ModalAction::None => {}
3305 ModalAction::SaveFile(path) => self.state = DialogState::Picked(path),
3306 }
3307 }
3308
3309 fn canonicalize_path(&self, path: &Path) -> PathBuf {
3312 if self.config.canonicalize_paths {
3313 dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
3314 } else {
3315 path.to_path_buf()
3316 }
3317 }
3318
3319 fn pin_path(&mut self, path: PathBuf) {
3321 let pinned = PinnedFolder::from_path(path);
3322 self.storage.pinned_folders.push(pinned);
3323 }
3324
3325 fn unpin_path(&mut self, path: &Path) {
3327 self.storage
3328 .pinned_folders
3329 .retain(|p| p.path.as_path() != path);
3330 }
3331
3332 fn is_pinned(&self, path: &Path) -> bool {
3334 self.storage
3335 .pinned_folders
3336 .iter()
3337 .any(|p| p.path.as_path() == path)
3338 }
3339
3340 fn begin_rename_pinned_folder(&mut self, pinned: PinnedFolder) {
3342 self.rename_pinned_folder = Some(pinned);
3343 self.rename_pinned_folder_request_focus = true;
3344 }
3345
3346 fn end_rename_pinned_folder(&mut self) {
3349 let renamed = std::mem::take(&mut self.rename_pinned_folder);
3350
3351 if let Some(renamed) = renamed {
3352 let old = self
3353 .storage
3354 .pinned_folders
3355 .iter_mut()
3356 .find(|p| p.path == renamed.path);
3357 if let Some(old) = old {
3358 old.label = renamed.label;
3359 }
3360 }
3361 }
3362
3363 fn is_pinned_folder_being_renamed(&self, pinned: &PinnedFolder) -> bool {
3365 self.rename_pinned_folder
3366 .as_ref()
3367 .is_some_and(|p| p.path == pinned.path)
3368 }
3369
3370 fn is_primary_selected(&self, item: &DirectoryEntry) -> bool {
3371 self.selected_item.as_ref().is_some_and(|x| x.path_eq(item))
3372 }
3373
3374 fn reset(&mut self) {
3377 let user_data = std::mem::take(&mut self.user_data);
3378 let storage = self.storage.clone();
3379 let config = self.config.clone();
3380 let selected = self.selected_item.clone();
3381
3382 *self = Self::with_config(config);
3383 if self.config.retain_selected_entry {
3384 self.selected_item = selected;
3385 }
3386 self.storage = storage;
3387 self.user_data = user_data;
3388 }
3389
3390 fn refresh(&mut self) {
3393 self.user_directories = self
3394 .config
3395 .file_system
3396 .user_dirs(self.config.canonicalize_paths);
3397 self.system_disks = self
3398 .config
3399 .file_system
3400 .get_disks(self.config.canonicalize_paths);
3401
3402 self.reload_directory();
3403 }
3404
3405 fn submit(&mut self) {
3407 if !self.is_selection_valid() {
3409 return;
3410 }
3411
3412 self.storage.last_picked_dir = self.current_directory().map(PathBuf::from);
3413
3414 match &self.mode {
3415 DialogMode::PickDirectory | DialogMode::PickFile => {
3416 if let Some(item) = self.selected_item.clone() {
3419 self.state = DialogState::Picked(item.to_path_buf());
3420 }
3421 }
3422 DialogMode::PickMultiple => {
3423 let result: Vec<PathBuf> = self
3424 .selected_entries()
3425 .map(crate::DirectoryEntry::to_path_buf)
3426 .collect();
3427
3428 self.state = DialogState::PickedMultiple(result);
3429 }
3430 DialogMode::SaveFile => {
3431 if let Some(path) = self.current_directory() {
3434 let full_path = path.join(&self.file_name_input);
3435 self.submit_save_file(full_path);
3436 }
3437 }
3438 }
3439 }
3440
3441 fn submit_save_file(&mut self, path: PathBuf) {
3444 if path.exists() {
3445 self.open_modal(Box::new(OverwriteFileModal::new(path)));
3446
3447 return;
3448 }
3449
3450 self.state = DialogState::Picked(path);
3451 }
3452
3453 fn cancel(&mut self) {
3455 self.state = DialogState::Cancelled;
3456 }
3457
3458 fn get_initial_directory(&self) -> PathBuf {
3464 let path = match self.config.opening_mode {
3465 OpeningMode::AlwaysInitialDir => &self.config.initial_directory,
3466 OpeningMode::LastVisitedDir => self
3467 .storage
3468 .last_visited_dir
3469 .as_deref()
3470 .unwrap_or(&self.config.initial_directory),
3471 OpeningMode::LastPickedDir => self
3472 .storage
3473 .last_picked_dir
3474 .as_deref()
3475 .unwrap_or(&self.config.initial_directory),
3476 };
3477
3478 let mut path = self.canonicalize_path(path);
3479
3480 if self.config.file_system.is_file(&path) {
3481 if let Some(parent) = path.parent() {
3482 path = parent.to_path_buf();
3483 }
3484 }
3485
3486 path
3487 }
3488
3489 fn current_directory(&self) -> Option<&Path> {
3491 if let Some(x) = self.directory_stack.iter().nth_back(self.directory_offset) {
3492 return Some(x.as_path());
3493 }
3494
3495 None
3496 }
3497
3498 fn is_selection_valid(&self) -> bool {
3501 match &self.mode {
3502 DialogMode::PickDirectory => self
3503 .selected_item
3504 .as_ref()
3505 .is_some_and(crate::DirectoryEntry::is_dir),
3506 DialogMode::PickFile => self
3507 .selected_item
3508 .as_ref()
3509 .is_some_and(DirectoryEntry::is_file),
3510 DialogMode::PickMultiple => self.get_dir_content_filtered_iter().any(|p| p.selected),
3511 DialogMode::SaveFile => self.file_name_input_error.is_none(),
3512 }
3513 }
3514
3515 fn validate_file_name_input(&self) -> Option<String> {
3519 if self.file_name_input.is_empty() {
3520 return Some(self.config.labels.err_empty_file_name.clone());
3521 }
3522
3523 if let Some(x) = self.current_directory() {
3524 let mut full_path = x.to_path_buf();
3525 full_path.push(self.file_name_input.as_str());
3526
3527 if self.config.file_system.is_dir(&full_path) {
3528 return Some(self.config.labels.err_directory_exists.clone());
3529 }
3530
3531 if !self.config.allow_file_overwrite && self.config.file_system.is_file(&full_path) {
3532 return Some(self.config.labels.err_file_exists.clone());
3533 }
3534 } else {
3535 return Some("Currently not in a directory".to_string());
3537 }
3538
3539 None
3540 }
3541
3542 fn select_item(&mut self, item: &mut DirectoryEntry) {
3545 if self.mode == DialogMode::PickMultiple {
3546 item.selected = true;
3547 }
3548 self.selected_item = Some(item.clone());
3549
3550 if self.mode == DialogMode::SaveFile && item.is_file() {
3551 self.file_name_input = item.file_name().to_string();
3552 self.file_name_input_error = self.validate_file_name_input();
3553 }
3554 }
3555
3556 fn select_next_visible_item_before(&mut self, item: &DirectoryEntry) -> bool {
3561 let mut return_val = false;
3562
3563 self.directory_content.reset_multi_selection();
3564
3565 let mut directory_content = std::mem::take(&mut self.directory_content);
3566 let search_value = std::mem::take(&mut self.search_value);
3567
3568 let index = directory_content
3569 .filtered_iter(&search_value)
3570 .position(|p| p.path_eq(item));
3571
3572 if let Some(index) = index {
3573 if index != 0 {
3574 if let Some(item) = directory_content
3575 .filtered_iter_mut(&search_value)
3576 .nth(index.saturating_sub(1))
3577 {
3578 self.select_item(item);
3579 self.scroll_to_selection = true;
3580 return_val = true;
3581 }
3582 }
3583 }
3584
3585 self.directory_content = directory_content;
3586 self.search_value = search_value;
3587
3588 return_val
3589 }
3590
3591 fn select_next_visible_item_after(&mut self, item: &DirectoryEntry) -> bool {
3596 let mut return_val = false;
3597
3598 self.directory_content.reset_multi_selection();
3599
3600 let mut directory_content = std::mem::take(&mut self.directory_content);
3601 let search_value = std::mem::take(&mut self.search_value);
3602
3603 let index = directory_content
3604 .filtered_iter(&search_value)
3605 .position(|p| p.path_eq(item));
3606
3607 if let Some(index) = index {
3608 if let Some(item) = directory_content
3609 .filtered_iter_mut(&search_value)
3610 .nth(index.saturating_add(1))
3611 {
3612 self.select_item(item);
3613 self.scroll_to_selection = true;
3614 return_val = true;
3615 }
3616 }
3617
3618 self.directory_content = directory_content;
3619 self.search_value = search_value;
3620
3621 return_val
3622 }
3623
3624 fn select_first_visible_item(&mut self) {
3626 self.directory_content.reset_multi_selection();
3627
3628 let mut directory_content = std::mem::take(&mut self.directory_content);
3629
3630 if let Some(item) = directory_content
3631 .filtered_iter_mut(&self.search_value.clone())
3632 .next()
3633 {
3634 self.select_item(item);
3635 self.scroll_to_selection = true;
3636 }
3637
3638 self.directory_content = directory_content;
3639 }
3640
3641 fn select_last_visible_item(&mut self) {
3643 self.directory_content.reset_multi_selection();
3644
3645 let mut directory_content = std::mem::take(&mut self.directory_content);
3646
3647 if let Some(item) = directory_content
3648 .filtered_iter_mut(&self.search_value.clone())
3649 .last()
3650 {
3651 self.select_item(item);
3652 self.scroll_to_selection = true;
3653 }
3654
3655 self.directory_content = directory_content;
3656 }
3657
3658 fn selection_limit_reached_with(&self, selected_count: usize) -> bool {
3660 self.config
3661 .max_selections
3662 .is_some_and(|max| selected_count >= max)
3663 }
3664
3665 fn select_all_items(&mut self) {
3667 let mut selected_count = self
3668 .directory_content
3669 .filtered_iter(&self.search_value)
3670 .filter(|p| p.selected)
3671 .count();
3672
3673 for item in self.directory_content.filtered_iter_mut(&self.search_value) {
3674 if item.selected {
3675 continue; }
3677 if self
3678 .config
3679 .max_selections
3680 .is_some_and(|max| selected_count >= max)
3681 {
3682 break;
3683 }
3684 item.selected = true;
3685 selected_count += 1;
3686 }
3687 }
3688
3689 fn open_path_edit(&mut self) {
3691 let path = self.current_directory().map_or_else(String::new, |path| {
3692 path.to_str().unwrap_or_default().to_string()
3693 });
3694
3695 self.path_edit_value = path;
3696 self.path_edit_activate = true;
3697 self.path_edit_visible = true;
3698 }
3699
3700 fn submit_path_edit(&mut self) {
3702 self.close_path_edit();
3703
3704 let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value));
3705
3706 if self.mode == DialogMode::PickFile && self.config.file_system.is_file(&path) {
3707 self.state = DialogState::Picked(path);
3708 return;
3709 }
3710
3711 if self.mode == DialogMode::SaveFile
3718 && (path.extension().is_some()
3719 || self.config.allow_path_edit_to_save_file_without_extension)
3720 && !self.config.file_system.is_dir(&path)
3721 && path.parent().is_some_and(std::path::Path::exists)
3722 {
3723 self.submit_save_file(path);
3724 return;
3725 }
3726
3727 self.load_directory(&path);
3728 }
3729
3730 const fn close_path_edit(&mut self) {
3733 self.path_edit_visible = false;
3734 }
3735
3736 fn load_next_directory(&mut self) {
3741 if self.directory_offset == 0 {
3742 return;
3744 }
3745
3746 self.directory_offset -= 1;
3747
3748 if let Some(path) = self.current_directory() {
3750 self.load_directory_content(path.to_path_buf().as_path());
3751 }
3752 }
3753
3754 fn load_previous_directory(&mut self) {
3758 if self.directory_offset + 1 >= self.directory_stack.len() {
3759 return;
3761 }
3762
3763 self.directory_offset += 1;
3764
3765 if let Some(path) = self.current_directory() {
3767 self.load_directory_content(path.to_path_buf().as_path());
3768 }
3769 }
3770
3771 fn load_parent_directory(&mut self) {
3775 if let Some(x) = self.current_directory() {
3776 if let Some(x) = x.to_path_buf().parent() {
3777 self.load_directory(x);
3778 }
3779 }
3780 }
3781
3782 fn reload_directory(&mut self) {
3789 if let Some(x) = self.current_directory() {
3790 self.load_directory_content(x.to_path_buf().as_path());
3791 }
3792 }
3793
3794 fn load_directory(&mut self, path: &Path) {
3800 if let Some(x) = self.current_directory() {
3803 if x == path {
3804 return;
3805 }
3806 }
3807
3808 if self.directory_offset != 0 && self.directory_stack.len() > self.directory_offset {
3809 self.directory_stack
3810 .drain(self.directory_stack.len() - self.directory_offset..);
3811 }
3812
3813 self.directory_stack.push(path.to_path_buf());
3814 self.directory_offset = 0;
3815
3816 self.load_directory_content(path);
3817
3818 self.search_value.clear();
3821 }
3822
3823 fn load_directory_content(&mut self, path: &Path) {
3825 self.storage.last_visited_dir = Some(path.to_path_buf());
3826
3827 let selected_file_filter = match self.mode {
3828 DialogMode::PickFile | DialogMode::PickMultiple => self.get_selected_file_filter(),
3829 _ => None,
3830 };
3831
3832 let selected_save_extension = if self.mode == DialogMode::SaveFile {
3833 self.get_selected_save_extension()
3834 .map(|e| e.file_extension.as_str())
3835 } else {
3836 None
3837 };
3838
3839 let filter = DirectoryFilter {
3840 show_files: self.show_files,
3841 show_hidden: self.storage.show_hidden,
3842 show_system_files: self.storage.show_system_files,
3843 file_filter: selected_file_filter.cloned(),
3844 filter_extension: selected_save_extension.map(str::to_string),
3845 };
3846
3847 self.directory_content = DirectoryContent::from_path(
3848 &self.config,
3849 path,
3850 self.config.file_system.clone(),
3851 filter,
3852 );
3853
3854 self.create_directory_dialog.close();
3855 self.scroll_to_selection = true;
3856
3857 if self.mode == DialogMode::SaveFile {
3858 self.file_name_input_error = self.validate_file_name_input();
3859 }
3860 }
3861
3862 fn should_open_directory(&self, path: &std::path::Path) -> bool {
3866 self.config
3867 .open_directory_filter
3868 .as_ref()
3869 .is_none_or(|f| f.matches(path))
3870 }
3871}
3872
3873#[cfg(test)]
3875const fn test_prop<T: Send + Sync>() {}
3876
3877#[test]
3878const fn test() {
3879 test_prop::<FileDialog>();
3880}
3881
3882#[cfg(test)]
3883mod open_directory_filter_tests {
3884 use std::path::Path;
3885
3886 use super::*;
3887
3888 #[test]
3889 fn filter_is_none_by_default() {
3890 let dialog = FileDialog::new();
3891 assert!(dialog.config.open_directory_filter.is_none());
3892 }
3893
3894 #[test]
3895 fn set_open_directory_filter_stores_filter() {
3896 let mut dialog = FileDialog::new();
3897 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3898 assert!(dialog.config.open_directory_filter.is_some());
3899 }
3900
3901 #[test]
3902 fn clear_open_directory_filter_removes_filter() {
3903 let mut dialog = FileDialog::new();
3904 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3905 assert!(dialog.config.open_directory_filter.is_some());
3906 dialog.clear_open_directory_filter();
3907 assert!(dialog.config.open_directory_filter.is_none());
3908 }
3909
3910 #[test]
3913 fn no_filter_always_navigates() {
3914 let dialog = FileDialog::new();
3915 assert!(dialog.should_open_directory(Path::new("/any/dir")));
3916 }
3917
3918 #[test]
3920 fn filter_returning_false_prevents_navigation() {
3921 let mut dialog = FileDialog::new();
3922 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3923 assert!(!dialog.should_open_directory(Path::new("/any/dir")));
3924 }
3925
3926 #[test]
3928 fn filter_returning_true_allows_navigation() {
3929 let mut dialog = FileDialog::new();
3930 dialog.set_open_directory_filter(Filter::new(|_: &Path| true));
3931 assert!(dialog.should_open_directory(Path::new("/any/dir")));
3932 }
3933
3934 #[test]
3936 fn cleared_filter_restores_default_navigation() {
3937 let mut dialog = FileDialog::new();
3938 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3939 assert!(!dialog.should_open_directory(Path::new("/any/dir")));
3940 dialog.clear_open_directory_filter();
3941 assert!(dialog.should_open_directory(Path::new("/any/dir")));
3942 }
3943
3944 #[test]
3948 fn filter_based_on_sentinel_file() -> Result<(), Box<dyn std::error::Error>> {
3949 use tempdir::TempDir;
3950 let tmp = TempDir::new("egui_fd_test")?;
3951 let project_dir = tmp.path().join("project");
3952 std::fs::create_dir_all(&project_dir)?;
3953 let sentinel = project_dir.join("project.json");
3954 std::fs::write(&sentinel, b"{}")?;
3955
3956 let regular_dir = tmp.path().join("regular");
3957 std::fs::create_dir_all(®ular_dir)?;
3958
3959 let mut dialog = FileDialog::new();
3960 dialog.set_open_directory_filter(Filter::new(|path: &Path| {
3962 !path.join("project.json").exists()
3963 }));
3964
3965 assert!(!dialog.should_open_directory(&project_dir));
3967 assert!(dialog.should_open_directory(®ular_dir));
3969 Ok(())
3971 }
3972}