1use std::any::Any;
2use std::fmt::Debug;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use egui::text::{CCursor, CCursorRange};
7
8use crate::config::{
9 FileDialogConfig, FileDialogKeyBindings, FileDialogLabels, FileFilter, Filter, OpeningMode,
10 PinnedFolder, QuickAccess, SaveExtension,
11};
12use crate::create_directory_dialog::CreateDirectoryDialog;
13use crate::data::{
14 DirectoryContent, DirectoryContentState, DirectoryEntry, DirectoryFilter, Disk, Disks,
15 UserDirectories,
16};
17use crate::modals::{FileDialogModal, ModalAction, ModalState, OverwriteFileModal};
18use crate::{FileSystem, NativeFileSystem};
19
20#[derive(Debug, PartialEq, Eq, Clone, Copy)]
22pub enum DialogMode {
23 PickFile,
25
26 PickDirectory,
28
29 PickMultiple,
31
32 SaveFile,
34}
35
36#[derive(Debug, PartialEq, Eq, Clone)]
38pub enum DialogState {
39 Open,
41
42 Closed,
44
45 Picked(PathBuf),
47
48 PickedMultiple(Vec<PathBuf>),
50
51 Cancelled,
53}
54
55#[derive(Debug, Clone)]
57#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
58pub struct FileDialogStorage {
59 pub pinned_folders: Vec<PinnedFolder>,
61 pub show_hidden: bool,
63 pub show_system_files: bool,
65 pub last_visited_dir: Option<PathBuf>,
67 pub last_picked_dir: Option<PathBuf>,
69}
70
71impl Default for FileDialogStorage {
72 fn default() -> Self {
74 Self {
75 pinned_folders: Vec::new(),
76 show_hidden: false,
77 show_system_files: false,
78 last_visited_dir: None,
79 last_picked_dir: None,
80 }
81 }
82}
83
84#[derive(Debug)]
110pub struct FileDialog {
111 config: FileDialogConfig,
113 storage: FileDialogStorage,
115
116 modals: Vec<Box<dyn FileDialogModal + Send + Sync>>,
119
120 mode: DialogMode,
122 state: DialogState,
124 show_files: bool,
127 user_data: Option<Box<dyn Any + Send + Sync>>,
130 window_id: egui::Id,
132
133 user_directories: Option<UserDirectories>,
136 system_disks: Disks,
139
140 directory_stack: Vec<PathBuf>,
144 directory_offset: usize,
149 directory_content: DirectoryContent,
151
152 create_directory_dialog: CreateDirectoryDialog,
154
155 path_edit_visible: bool,
157 path_edit_value: String,
159 path_edit_activate: bool,
162 path_edit_request_focus: bool,
164
165 selected_item: Option<DirectoryEntry>,
168 file_name_input: String,
170 file_name_input_error: Option<String>,
173 file_name_input_request_focus: bool,
175 selected_file_filter: Option<egui::Id>,
177 selected_save_extension: Option<egui::Id>,
179
180 scroll_to_selection: bool,
182 search_value: String,
184 init_search: bool,
186
187 any_focused_last_frame: bool,
191
192 rename_pinned_folder: Option<PinnedFolder>,
195 rename_pinned_folder_request_focus: bool,
198}
199
200impl Default for FileDialog {
201 fn default() -> Self {
203 Self::new()
204 }
205}
206
207impl Debug for dyn FileDialogModal + Send + Sync {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 write!(f, "<FileDialogModal>")
210 }
211}
212
213type FileDialogUiCallback<'a> = dyn FnMut(&mut egui::Ui, &mut FileDialog) + 'a;
218
219impl FileDialog {
220 #[must_use]
225 pub fn new() -> Self {
226 let file_system = Arc::new(NativeFileSystem);
227
228 Self {
229 config: FileDialogConfig::default_from_filesystem(file_system.clone()),
230 storage: FileDialogStorage::default(),
231
232 modals: Vec::new(),
233
234 mode: DialogMode::PickDirectory,
235 state: DialogState::Closed,
236 show_files: true,
237 user_data: None,
238
239 window_id: egui::Id::new("file_dialog"),
240
241 user_directories: None,
242 system_disks: Disks::new_empty(),
243
244 directory_stack: Vec::new(),
245 directory_offset: 0,
246 directory_content: DirectoryContent::default(),
247
248 create_directory_dialog: CreateDirectoryDialog::from_filesystem(file_system),
249
250 path_edit_visible: false,
251 path_edit_value: String::new(),
252 path_edit_activate: false,
253 path_edit_request_focus: false,
254
255 selected_item: None,
256 file_name_input: String::new(),
257 file_name_input_error: None,
258 file_name_input_request_focus: true,
259 selected_file_filter: None,
260 selected_save_extension: None,
261
262 scroll_to_selection: false,
263 search_value: String::new(),
264 init_search: false,
265
266 any_focused_last_frame: false,
267
268 rename_pinned_folder: None,
269 rename_pinned_folder_request_focus: false,
270 }
271 }
272
273 pub fn with_config(config: FileDialogConfig) -> Self {
275 let mut obj = Self::new();
276 *obj.config_mut() = config;
277 obj.create_directory_dialog =
278 CreateDirectoryDialog::from_filesystem(obj.config.file_system.clone());
279 obj
280 }
281
282 #[must_use]
284 pub fn with_file_system(file_system: Arc<dyn FileSystem + Send + Sync>) -> Self {
285 let mut obj = Self::new();
286 obj.config.initial_directory = file_system.current_dir().unwrap_or_default();
287 obj.config.file_system = file_system;
288 obj.create_directory_dialog =
289 CreateDirectoryDialog::from_filesystem(obj.config.file_system.clone());
290 obj
291 }
292
293 #[deprecated(
339 since = "0.10.0",
340 note = "Use `pick_file` / `pick_directory` / `pick_multiple` in combination with \
341 `set_user_data` instead"
342 )]
343 pub fn open(&mut self, mode: DialogMode, mut show_files: bool) {
344 self.reset();
345 self.refresh();
346
347 if mode == DialogMode::PickFile {
348 show_files = true;
349 }
350
351 if mode == DialogMode::SaveFile {
352 self.file_name_input_request_focus = true;
353 self.file_name_input
354 .clone_from(&self.config.default_file_name);
355 }
356
357 self.selected_file_filter = None;
358 self.selected_save_extension = None;
359
360 self.set_default_file_filter();
361 self.set_default_save_extension();
362
363 self.mode = mode;
364 self.state = DialogState::Open;
365 self.show_files = show_files;
366
367 self.window_id = self
368 .config
369 .id
370 .unwrap_or_else(|| egui::Id::new(self.get_window_title()));
371
372 self.load_directory(&self.get_initial_directory());
373 }
374
375 pub fn pick_directory(&mut self) {
383 #[allow(deprecated)]
385 self.open(DialogMode::PickDirectory, false);
386 }
387
388 pub fn pick_file(&mut self) {
394 #[allow(deprecated)]
396 self.open(DialogMode::PickFile, true);
397 }
398
399 pub fn pick_multiple(&mut self) {
406 #[allow(deprecated)]
408 self.open(DialogMode::PickMultiple, true);
409 }
410
411 pub fn save_file(&mut self) {
417 #[allow(deprecated)]
419 self.open(DialogMode::SaveFile, true);
420 }
421
422 pub fn update(&mut self, ctx: &egui::Context) -> &Self {
426 if self.state != DialogState::Open {
427 return self;
428 }
429
430 self.update_keybindings(ctx);
431 self.update_ui(ctx, None);
432
433 self
434 }
435
436 pub fn set_right_panel_width(&mut self, width: f32) {
438 self.config.right_panel_width = Some(width);
439 }
440
441 pub fn clear_right_panel_width(&mut self) {
443 self.config.right_panel_width = None;
444 }
445
446 pub fn update_with_right_panel_ui(
458 &mut self,
459 ctx: &egui::Context,
460 f: &mut FileDialogUiCallback,
461 ) -> &Self {
462 if self.state != DialogState::Open {
463 return self;
464 }
465
466 self.update_keybindings(ctx);
467 self.update_ui(ctx, Some(f));
468
469 self
470 }
471
472 pub fn config_mut(&mut self) -> &mut FileDialogConfig {
477 &mut self.config
478 }
479
480 pub fn set_open_directory_filter(&mut self, filter: Filter<Path>) {
484 self.config.open_directory_filter = Some(filter);
485 }
486
487 pub fn clear_open_directory_filter(&mut self) {
489 self.config.open_directory_filter = None;
490 }
491
492 pub fn storage(mut self, storage: FileDialogStorage) -> Self {
496 self.storage = storage;
497 self
498 }
499
500 pub fn storage_mut(&mut self) -> &mut FileDialogStorage {
502 &mut self.storage
503 }
504
505 pub fn keybindings(mut self, keybindings: FileDialogKeyBindings) -> Self {
507 self.config.keybindings = keybindings;
508 self
509 }
510
511 pub fn labels(mut self, labels: FileDialogLabels) -> Self {
517 self.config.labels = labels;
518 self
519 }
520
521 pub fn labels_mut(&mut self) -> &mut FileDialogLabels {
523 &mut self.config.labels
524 }
525
526 pub const fn opening_mode(mut self, opening_mode: OpeningMode) -> Self {
528 self.config.opening_mode = opening_mode;
529 self
530 }
531
532 pub const fn as_modal(mut self, as_modal: bool) -> Self {
537 self.config.as_modal = as_modal;
538 self
539 }
540
541 pub const fn modal_overlay_color(mut self, modal_overlay_color: egui::Color32) -> Self {
543 self.config.modal_overlay_color = modal_overlay_color;
544 self
545 }
546
547 pub fn initial_directory(mut self, directory: PathBuf) -> Self {
556 self.config.initial_directory = directory;
557 self
558 }
559
560 pub fn default_file_name(mut self, name: &str) -> Self {
562 name.clone_into(&mut self.config.default_file_name);
563 self
564 }
565
566 pub const fn allow_file_overwrite(mut self, allow_file_overwrite: bool) -> Self {
572 self.config.allow_file_overwrite = allow_file_overwrite;
573 self
574 }
575
576 pub const fn allow_path_edit_to_save_file_without_extension(mut self, allow: bool) -> Self {
585 self.config.allow_path_edit_to_save_file_without_extension = allow;
586 self
587 }
588
589 pub fn directory_separator(mut self, separator: &str) -> Self {
592 self.config.directory_separator = separator.to_string();
593 self
594 }
595
596 pub const fn canonicalize_paths(mut self, canonicalize: bool) -> Self {
613 self.config.canonicalize_paths = canonicalize;
614 self
615 }
616
617 pub const fn load_via_thread(mut self, load_via_thread: bool) -> Self {
621 self.config.load_via_thread = load_via_thread;
622 self
623 }
624
625 pub const fn truncate_filenames(mut self, truncate_filenames: bool) -> Self {
631 self.config.truncate_filenames = truncate_filenames;
632 self
633 }
634
635 pub fn max_selections(mut self, max: usize) -> Self {
637 self.config.max_selections = Some(max);
638 self
639 }
640
641 pub fn err_icon(mut self, icon: &str) -> Self {
643 self.config.err_icon = icon.to_string();
644 self
645 }
646
647 pub fn default_file_icon(mut self, icon: &str) -> Self {
649 self.config.default_file_icon = icon.to_string();
650 self
651 }
652
653 pub fn default_folder_icon(mut self, icon: &str) -> Self {
655 self.config.default_folder_icon = icon.to_string();
656 self
657 }
658
659 pub fn device_icon(mut self, icon: &str) -> Self {
661 self.config.device_icon = icon.to_string();
662 self
663 }
664
665 pub fn removable_device_icon(mut self, icon: &str) -> Self {
667 self.config.removable_device_icon = icon.to_string();
668 self
669 }
670
671 pub fn parent_directory_icon(mut self, icon: &str) -> Self {
673 self.config.parent_directory_icon = icon.to_string();
674 self
675 }
676
677 pub fn back_icon(mut self, icon: &str) -> Self {
679 self.config.back_icon = icon.to_string();
680 self
681 }
682
683 pub fn forward_icon(mut self, icon: &str) -> Self {
685 self.config.forward_icon = icon.to_string();
686 self
687 }
688
689 pub fn new_folder_icon(mut self, icon: &str) -> Self {
691 self.config.new_folder_icon = icon.to_string();
692 self
693 }
694
695 pub fn menu_icon(mut self, icon: &str) -> Self {
697 self.config.menu_icon = icon.to_string();
698 self
699 }
700
701 pub fn search_icon(mut self, icon: &str) -> Self {
703 self.config.search_icon = icon.to_string();
704 self
705 }
706
707 pub fn path_edit_icon(mut self, icon: &str) -> Self {
709 self.config.path_edit_icon = icon.to_string();
710 self
711 }
712
713 pub fn add_file_filter(mut self, name: &str, filter: Filter<Path>) -> Self {
739 self.config = self.config.add_file_filter(name, filter);
740 self
741 }
742
743 pub fn add_file_filter_extensions(mut self, name: &str, extensions: Vec<&'static str>) -> Self {
759 self.config = self.config.add_file_filter_extensions(name, extensions);
760 self
761 }
762
763 pub fn default_file_filter(mut self, name: &str) -> Self {
767 self.config.default_file_filter = Some(name.to_string());
768 self
769 }
770
771 pub fn add_save_extension(mut self, name: &str, file_extension: &str) -> Self {
793 self.config = self.config.add_save_extension(name, file_extension);
794 self
795 }
796
797 pub fn default_save_extension(mut self, name: &str) -> Self {
801 self.config.default_save_extension = Some(name.to_string());
802 self
803 }
804
805 pub fn set_file_icon(mut self, icon: &str, filter: Filter<std::path::Path>) -> Self {
826 self.config = self.config.set_file_icon(icon, filter);
827 self
828 }
829
830 pub fn add_quick_access(
846 mut self,
847 heading: &str,
848 builder: impl FnOnce(&mut QuickAccess),
849 ) -> Self {
850 self.config = self.config.add_quick_access(heading, builder);
851 self
852 }
853
854 pub fn title(mut self, title: &str) -> Self {
859 self.config.title = Some(title.to_string());
860 self
861 }
862
863 pub fn id(mut self, id: impl Into<egui::Id>) -> Self {
865 self.config.id = Some(id.into());
866 self
867 }
868
869 pub fn default_pos(mut self, default_pos: impl Into<egui::Pos2>) -> Self {
871 self.config.default_pos = Some(default_pos.into());
872 self
873 }
874
875 pub fn fixed_pos(mut self, pos: impl Into<egui::Pos2>) -> Self {
877 self.config.fixed_pos = Some(pos.into());
878 self
879 }
880
881 pub fn default_size(mut self, size: impl Into<egui::Vec2>) -> Self {
883 self.config.default_size = size.into();
884 self
885 }
886
887 pub fn max_size(mut self, max_size: impl Into<egui::Vec2>) -> Self {
889 self.config.max_size = Some(max_size.into());
890 self
891 }
892
893 pub fn min_size(mut self, min_size: impl Into<egui::Vec2>) -> Self {
897 self.config.min_size = min_size.into();
898 self
899 }
900
901 pub fn anchor(mut self, align: egui::Align2, offset: impl Into<egui::Vec2>) -> Self {
903 self.config.anchor = Some((align, offset.into()));
904 self
905 }
906
907 pub const fn resizable(mut self, resizable: bool) -> Self {
909 self.config.resizable = resizable;
910 self
911 }
912
913 pub const fn movable(mut self, movable: bool) -> Self {
917 self.config.movable = movable;
918 self
919 }
920
921 pub const fn title_bar(mut self, title_bar: bool) -> Self {
923 self.config.title_bar = title_bar;
924 self
925 }
926
927 pub const fn show_top_panel(mut self, show_top_panel: bool) -> Self {
930 self.config.show_top_panel = show_top_panel;
931 self
932 }
933
934 pub const fn show_parent_button(mut self, show_parent_button: bool) -> Self {
938 self.config.show_parent_button = show_parent_button;
939 self
940 }
941
942 pub const fn show_back_button(mut self, show_back_button: bool) -> Self {
946 self.config.show_back_button = show_back_button;
947 self
948 }
949
950 pub const fn show_forward_button(mut self, show_forward_button: bool) -> Self {
954 self.config.show_forward_button = show_forward_button;
955 self
956 }
957
958 pub const fn show_new_folder_button(mut self, show_new_folder_button: bool) -> Self {
962 self.config.show_new_folder_button = show_new_folder_button;
963 self
964 }
965
966 pub const fn show_current_path(mut self, show_current_path: bool) -> Self {
970 self.config.show_current_path = show_current_path;
971 self
972 }
973
974 pub const fn show_path_edit_button(mut self, show_path_edit_button: bool) -> Self {
978 self.config.show_path_edit_button = show_path_edit_button;
979 self
980 }
981
982 pub const fn show_menu_button(mut self, show_menu_button: bool) -> Self {
987 self.config.show_menu_button = show_menu_button;
988 self
989 }
990
991 pub const fn show_reload_button(mut self, show_reload_button: bool) -> Self {
996 self.config.show_reload_button = show_reload_button;
997 self
998 }
999
1000 pub const fn show_working_directory_button(
1007 mut self,
1008 show_working_directory_button: bool,
1009 ) -> Self {
1010 self.config.show_working_directory_button = show_working_directory_button;
1011 self
1012 }
1013
1014 pub const fn show_select_all_button(mut self, show_select_all_button: bool) -> Self {
1020 self.config.show_select_all_button = show_select_all_button;
1021 self
1022 }
1023
1024 pub const fn show_hidden_option(mut self, show_hidden_option: bool) -> Self {
1030 self.config.show_hidden_option = show_hidden_option;
1031 self
1032 }
1033
1034 pub const fn show_system_files_option(mut self, show_system_files_option: bool) -> Self {
1040 self.config.show_system_files_option = show_system_files_option;
1041 self
1042 }
1043
1044 pub const fn show_search(mut self, show_search: bool) -> Self {
1048 self.config.show_search = show_search;
1049 self
1050 }
1051
1052 pub const fn show_all_files_filter(mut self, show_all_files_filter: bool) -> Self {
1061 self.config.show_all_files_filter = show_all_files_filter;
1062 self
1063 }
1064
1065 pub const fn show_left_panel(mut self, show_left_panel: bool) -> Self {
1068 self.config.show_left_panel = show_left_panel;
1069 self
1070 }
1071
1072 pub const fn show_pinned_folders(mut self, show_pinned_folders: bool) -> Self {
1075 self.config.show_pinned_folders = show_pinned_folders;
1076 self
1077 }
1078
1079 pub const fn show_places(mut self, show_places: bool) -> Self {
1084 self.config.show_places = show_places;
1085 self
1086 }
1087
1088 pub const fn show_devices(mut self, show_devices: bool) -> Self {
1093 self.config.show_devices = show_devices;
1094 self
1095 }
1096
1097 pub const fn show_removable_devices(mut self, show_removable_devices: bool) -> Self {
1102 self.config.show_removable_devices = show_removable_devices;
1103 self
1104 }
1105
1106 pub fn picked(&self) -> Option<&Path> {
1114 match &self.state {
1115 DialogState::Picked(path) => Some(path),
1116 _ => None,
1117 }
1118 }
1119
1120 pub fn take_picked(&mut self) -> Option<PathBuf> {
1127 match &mut self.state {
1128 DialogState::Picked(path) => {
1129 let path = std::mem::take(path);
1130 self.state = DialogState::Closed;
1131 Some(path)
1132 }
1133 _ => None,
1134 }
1135 }
1136
1137 pub fn picked_multiple(&self) -> Option<Vec<&Path>> {
1142 match &self.state {
1143 DialogState::PickedMultiple(items) => {
1144 Some(items.iter().map(std::path::PathBuf::as_path).collect())
1145 }
1146 _ => None,
1147 }
1148 }
1149
1150 pub fn take_picked_multiple(&mut self) -> Option<Vec<PathBuf>> {
1157 match &mut self.state {
1158 DialogState::PickedMultiple(items) => {
1159 let items = std::mem::take(items);
1160 self.state = DialogState::Closed;
1161 Some(items)
1162 }
1163 _ => None,
1164 }
1165 }
1166
1167 pub const fn selected_entry(&self) -> Option<&DirectoryEntry> {
1175 self.selected_item.as_ref()
1176 }
1177
1178 pub fn selected_entries(&self) -> impl Iterator<Item = &DirectoryEntry> {
1184 self.get_dir_content_filtered_iter().filter(|p| p.selected)
1185 }
1186
1187 pub fn user_data<U: Any>(&self) -> Option<&U> {
1191 #[allow(clippy::coerce_container_to_any)]
1192 self.user_data.as_ref().and_then(|u| u.downcast_ref())
1193 }
1194
1195 pub fn user_data_mut<U: Any>(&mut self) -> Option<&mut U> {
1199 #[allow(clippy::coerce_container_to_any)]
1200 self.user_data.as_mut().and_then(|u| u.downcast_mut())
1201 }
1202
1203 pub fn set_user_data<U: Any + Send + Sync>(&mut self, user_data: U) {
1227 self.user_data = Some(Box::new(user_data));
1228 }
1229
1230 pub const fn mode(&self) -> DialogMode {
1232 self.mode
1233 }
1234
1235 pub const fn state(&self) -> &DialogState {
1237 &self.state
1238 }
1239
1240 pub const fn get_window_id(&self) -> egui::Id {
1242 self.window_id
1243 }
1244}
1245
1246impl FileDialog {
1248 fn update_ui(
1252 &mut self,
1253 ctx: &egui::Context,
1254 right_panel_fn: Option<&mut FileDialogUiCallback>,
1255 ) {
1256 let mut is_open = true;
1257
1258 if self.config.as_modal {
1259 let re = self.ui_update_modal_background(ctx);
1260 ctx.move_to_top(re.response.layer_id);
1261 }
1262
1263 let re = self.create_window(&mut is_open).show(ctx, |ui| {
1264 if !self.modals.is_empty() {
1265 self.ui_update_modals(ui);
1266 return;
1267 }
1268
1269 if self.config.show_top_panel {
1270 egui::Panel::top(self.window_id.with("top_panel"))
1271 .resizable(false)
1272 .show_inside(ui, |ui| {
1273 self.ui_update_top_panel(ui);
1274 });
1275 }
1276
1277 if self.config.show_left_panel {
1278 egui::Panel::left(self.window_id.with("left_panel"))
1279 .resizable(true)
1280 .default_size(150.0)
1281 .size_range(90.0..=250.0)
1282 .show_inside(ui, |ui| {
1283 self.ui_update_left_panel(ui);
1284 });
1285 }
1286
1287 if let Some(f) = right_panel_fn {
1289 let mut right_panel = egui::Panel::right(self.window_id.with("right_panel"))
1290 .resizable(true);
1293 if let Some(width) = self.config.right_panel_width {
1294 right_panel = right_panel.default_size(width);
1295 }
1296 right_panel.show_inside(ui, |ui| {
1297 f(ui, self);
1298 });
1299 }
1300
1301 egui::Panel::bottom(self.window_id.with("bottom_panel"))
1302 .resizable(false)
1303 .show_inside(ui, |ui| {
1304 self.ui_update_bottom_panel(ui);
1305 });
1306
1307 egui::CentralPanel::default().show_inside(ui, |ui| {
1308 self.ui_update_central_panel(ui);
1309 });
1310 });
1311
1312 if self.config.as_modal {
1313 if let Some(inner_response) = re {
1314 ctx.move_to_top(inner_response.response.layer_id);
1315 }
1316 }
1317
1318 self.any_focused_last_frame = ctx.memory(egui::Memory::focused).is_some();
1319
1320 if !is_open {
1322 self.cancel();
1323 }
1324
1325 let mut repaint = false;
1326
1327 ctx.input(|i| {
1329 if let Some(dropped_file) = i.raw.dropped_files.last() {
1331 if let Some(path) = &dropped_file.path {
1332 if self.config.file_system.is_dir(path) {
1333 self.load_directory(path.as_path());
1335 repaint = true;
1336 } else if let Some(parent) = path.parent() {
1337 self.load_directory(parent);
1339 self.select_item(&mut DirectoryEntry::from_path(
1340 &self.config,
1341 path,
1342 &*self.config.file_system,
1343 ));
1344 self.scroll_to_selection = true;
1345 repaint = true;
1346 }
1347 }
1348 }
1349 });
1350
1351 if repaint {
1353 ctx.request_repaint();
1354 }
1355 }
1356
1357 fn ui_update_modal_background(&self, ctx: &egui::Context) -> egui::InnerResponse<()> {
1359 egui::Area::new(self.window_id.with("modal_overlay"))
1360 .interactable(true)
1361 .fixed_pos(egui::Pos2::ZERO)
1362 .show(ctx, |ui| {
1363 let content_rect = ctx.input(egui::InputState::content_rect);
1364
1365 ui.allocate_response(content_rect.size(), egui::Sense::click());
1366
1367 ui.painter().rect_filled(
1368 content_rect,
1369 egui::CornerRadius::ZERO,
1370 self.config.modal_overlay_color,
1371 );
1372 })
1373 }
1374
1375 fn ui_update_modals(&mut self, ui: &mut egui::Ui) {
1376 egui::Panel::bottom(self.window_id.with("modal_bottom_panel"))
1381 .resizable(false)
1382 .show_separator_line(false)
1383 .show_inside(ui, |_| {});
1384
1385 egui::CentralPanel::default().show_inside(ui, |ui| {
1388 if let Some(modal) = self.modals.last_mut() {
1389 #[allow(clippy::single_match)]
1390 match modal.update(&self.config, ui) {
1391 ModalState::Close(action) => {
1392 self.exec_modal_action(action);
1393 self.modals.pop();
1394 }
1395 ModalState::Pending => {}
1396 }
1397 }
1398 });
1399 }
1400
1401 fn create_window<'a>(&self, is_open: &'a mut bool) -> egui::Window<'a> {
1403 let mut window = egui::Window::new(self.get_window_title())
1404 .id(self.window_id)
1405 .open(is_open)
1406 .default_size(self.config.default_size)
1407 .min_size(self.config.min_size)
1408 .resizable(self.config.resizable)
1409 .movable(self.config.movable)
1410 .title_bar(self.config.title_bar)
1411 .collapsible(false);
1412
1413 if let Some(pos) = self.config.default_pos {
1414 window = window.default_pos(pos);
1415 }
1416
1417 if let Some(pos) = self.config.fixed_pos {
1418 window = window.fixed_pos(pos);
1419 }
1420
1421 if let Some((anchor, offset)) = self.config.anchor {
1422 window = window.anchor(anchor, offset);
1423 }
1424
1425 if let Some(size) = self.config.max_size {
1426 window = window.max_size(size);
1427 }
1428
1429 window
1430 }
1431
1432 const fn get_window_title(&self) -> &String {
1435 match &self.config.title {
1436 Some(title) => title,
1437 None => match &self.mode {
1438 DialogMode::PickDirectory => &self.config.labels.title_select_directory,
1439 DialogMode::PickFile => &self.config.labels.title_select_file,
1440 DialogMode::PickMultiple => &self.config.labels.title_select_multiple,
1441 DialogMode::SaveFile => &self.config.labels.title_save_file,
1442 },
1443 }
1444 }
1445
1446 fn ui_update_top_panel(&mut self, ui: &mut egui::Ui) {
1449 const BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(25.0, 25.0);
1450
1451 ui.horizontal(|ui| {
1452 self.ui_update_nav_buttons(ui, BUTTON_SIZE);
1453
1454 let mut path_display_width = ui.available_width();
1455
1456 if self.config.show_reload_button {
1458 path_display_width -= ui
1459 .style()
1460 .spacing
1461 .item_spacing
1462 .x
1463 .mul_add(2.5, BUTTON_SIZE.x);
1464 }
1465
1466 if self.config.show_search {
1467 path_display_width -= 140.0;
1468 }
1469
1470 if self.config.show_current_path {
1471 self.ui_update_current_path(ui, path_display_width);
1472 }
1473
1474 if self.config.show_menu_button
1476 && (self.config.show_reload_button
1477 || self.config.show_working_directory_button
1478 || self.config.show_hidden_option
1479 || self.config.show_system_files_option)
1480 {
1481 ui.allocate_ui_with_layout(
1482 BUTTON_SIZE,
1483 egui::Layout::centered_and_justified(egui::Direction::LeftToRight),
1484 |ui| {
1485 let menu_icon = std::mem::take(&mut self.config.menu_icon);
1486 ui.menu_button(&menu_icon, |ui| {
1487 self.ui_update_hamburger_menu(ui);
1488 });
1489 self.config.menu_icon = menu_icon;
1490 },
1491 );
1492 }
1493
1494 if self.config.show_search {
1495 self.ui_update_search(ui);
1496 }
1497 });
1498
1499 ui.add_space(ui.global_style().spacing.item_spacing.y);
1500 }
1501
1502 fn ui_update_nav_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
1504 if self.config.show_parent_button {
1505 if let Some(x) = self.current_directory() {
1506 if self.ui_button_sized(
1507 ui,
1508 x.parent().is_some(),
1509 button_size,
1510 self.config.parent_directory_icon.as_str(),
1511 None,
1512 ) {
1513 self.load_parent_directory();
1514 }
1515 } else {
1516 let _ = self.ui_button_sized(
1517 ui,
1518 false,
1519 button_size,
1520 self.config.parent_directory_icon.as_str(),
1521 None,
1522 );
1523 }
1524 }
1525
1526 if self.config.show_back_button
1527 && self.ui_button_sized(
1528 ui,
1529 self.directory_offset + 1 < self.directory_stack.len(),
1530 button_size,
1531 self.config.back_icon.as_str(),
1532 None,
1533 )
1534 {
1535 self.load_previous_directory();
1536 }
1537
1538 if self.config.show_forward_button
1539 && self.ui_button_sized(
1540 ui,
1541 self.directory_offset != 0,
1542 button_size,
1543 self.config.forward_icon.as_str(),
1544 None,
1545 )
1546 {
1547 self.load_next_directory();
1548 }
1549
1550 if self.config.show_new_folder_button
1551 && self.ui_button_sized(
1552 ui,
1553 !self.create_directory_dialog.is_open(),
1554 button_size,
1555 self.config.new_folder_icon.as_str(),
1556 None,
1557 )
1558 {
1559 self.open_new_folder_dialog();
1560 }
1561 }
1562
1563 fn ui_update_current_path(&mut self, ui: &mut egui::Ui, width: f32) {
1567 egui::Frame::default()
1568 .stroke(egui::Stroke::new(
1569 1.0,
1570 ui.global_style().visuals.window_stroke.color,
1571 ))
1572 .inner_margin(egui::Margin::from(4))
1573 .corner_radius(egui::CornerRadius::from(4))
1574 .show(ui, |ui| {
1575 const EDIT_BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(22.0, 20.0);
1576
1577 if self.path_edit_visible {
1578 self.ui_update_path_edit(ui, width, EDIT_BUTTON_SIZE);
1579 } else {
1580 self.ui_update_path_display(ui, width, EDIT_BUTTON_SIZE);
1581 }
1582 });
1583 }
1584
1585 fn ui_update_path_display(
1587 &mut self,
1588 ui: &mut egui::Ui,
1589 width: f32,
1590 edit_button_size: egui::Vec2,
1591 ) {
1592 ui.style_mut().always_scroll_the_only_direction = true;
1593 ui.style_mut().spacing.scroll.bar_width = 8.0;
1594
1595 let max_width = if self.config.show_path_edit_button {
1596 ui.style()
1597 .spacing
1598 .item_spacing
1599 .x
1600 .mul_add(-2.0, width - edit_button_size.x)
1601 } else {
1602 width
1603 };
1604
1605 egui::ScrollArea::horizontal()
1606 .auto_shrink([false, false])
1607 .stick_to_right(true)
1608 .max_width(max_width)
1609 .show(ui, |ui| {
1610 ui.horizontal(|ui| {
1611 ui.style_mut().spacing.item_spacing.x /= 2.5;
1612 ui.style_mut().spacing.button_padding = egui::Vec2::new(5.0, 3.0);
1613
1614 let mut path = PathBuf::new();
1615
1616 if let Some(data) = self.current_directory().map(Path::to_path_buf) {
1617 for (i, segment) in data.iter().enumerate() {
1618 path.push(segment);
1619
1620 let mut segment_str = segment.to_str().unwrap_or_default().to_string();
1621
1622 if self.is_pinned(&path) {
1623 segment_str =
1624 format!("{} {}", &self.config.pinned_icon, segment_str);
1625 }
1626
1627 if i != 0 {
1628 ui.label(self.config.directory_separator.as_str());
1629 }
1630
1631 let re = ui.button(segment_str);
1632
1633 if re.clicked() {
1634 self.load_directory(path.as_path());
1635 return;
1636 }
1637
1638 self.ui_update_central_panel_path_context_menu(&re, &path.clone());
1639 }
1640 }
1641 });
1642 });
1643
1644 if !self.config.show_path_edit_button {
1645 return;
1646 }
1647
1648 if ui
1649 .add_sized(
1650 edit_button_size,
1651 egui::Button::new(self.config.path_edit_icon.as_str())
1652 .fill(egui::Color32::TRANSPARENT),
1653 )
1654 .clicked()
1655 {
1656 self.open_path_edit();
1657 }
1658 }
1659
1660 fn ui_update_path_edit(&mut self, ui: &mut egui::Ui, width: f32, edit_button_size: egui::Vec2) {
1662 let desired_width: f32 = ui
1663 .style()
1664 .spacing
1665 .item_spacing
1666 .x
1667 .mul_add(-3.0, width - edit_button_size.x);
1668
1669 let response = egui::TextEdit::singleline(&mut self.path_edit_value)
1670 .desired_width(desired_width)
1671 .show(ui)
1672 .response;
1673
1674 if self.path_edit_activate {
1675 response.request_focus();
1676 Self::set_cursor_to_end(&response, &self.path_edit_value);
1677 self.path_edit_activate = false;
1678 }
1679
1680 if self.path_edit_request_focus {
1681 response.request_focus();
1682 self.path_edit_request_focus = false;
1683 }
1684
1685 let btn_response = ui.add_sized(edit_button_size, egui::Button::new("✔"));
1686
1687 if btn_response.clicked() {
1688 self.submit_path_edit();
1689 }
1690
1691 if !response.has_focus() && !btn_response.contains_pointer() {
1692 self.path_edit_visible = false;
1693 }
1694 }
1695
1696 fn ui_update_hamburger_menu(&mut self, ui: &mut egui::Ui) {
1698 const SEPARATOR_SPACING: f32 = 2.0;
1699
1700 let working_dir = self.config.file_system.current_dir();
1701
1702 let show_reload = self.config.show_reload_button;
1703 let show_working_dir = self.config.show_working_directory_button && working_dir.is_ok();
1704 let show_select_all =
1705 self.config.show_select_all_button && self.mode == DialogMode::PickMultiple;
1706
1707 let show_hidden = self.config.show_hidden_option;
1708 let show_system_files = self.config.show_system_files_option;
1709
1710 if show_reload && ui.button(&self.config.labels.reload).clicked() {
1711 self.refresh();
1712 ui.close();
1713 }
1714
1715 if show_working_dir && ui.button(&self.config.labels.working_directory).clicked() {
1716 self.load_directory(&working_dir.unwrap_or_default());
1717 ui.close();
1718 }
1719
1720 if show_select_all && ui.button(&self.config.labels.select_all).clicked() {
1721 self.select_all_items();
1722 ui.close();
1723 }
1724
1725 let any_above = show_reload || show_working_dir || show_select_all;
1726 let any_below = show_hidden || show_system_files;
1727
1728 if any_above && any_below {
1729 ui.add_space(SEPARATOR_SPACING);
1730 ui.separator();
1731 ui.add_space(SEPARATOR_SPACING);
1732 }
1733
1734 if show_hidden
1735 && ui
1736 .checkbox(
1737 &mut self.storage.show_hidden,
1738 &self.config.labels.show_hidden,
1739 )
1740 .clicked()
1741 {
1742 self.refresh();
1743 ui.close();
1744 }
1745
1746 if show_system_files
1747 && ui
1748 .checkbox(
1749 &mut self.storage.show_system_files,
1750 &self.config.labels.show_system_files,
1751 )
1752 .clicked()
1753 {
1754 self.refresh();
1755 ui.close();
1756 }
1757 }
1758
1759 fn ui_update_search(&mut self, ui: &mut egui::Ui) {
1761 egui::Frame::default()
1762 .stroke(egui::Stroke::new(
1763 1.0,
1764 ui.global_style().visuals.window_stroke.color,
1765 ))
1766 .inner_margin(egui::Margin::symmetric(4, 4))
1767 .corner_radius(egui::CornerRadius::from(4))
1768 .show(ui, |ui| {
1769 ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1770 ui.add_space(ui.global_style().spacing.item_spacing.y);
1771
1772 ui.label(egui::RichText::from(self.config.search_icon.as_str()).size(15.0));
1773
1774 let re = ui.add_sized(
1775 egui::Vec2::new(ui.available_width(), 0.0),
1776 egui::TextEdit::singleline(&mut self.search_value),
1777 );
1778
1779 self.edit_search_on_text_input(ui);
1780
1781 if re.changed() || self.init_search {
1782 self.selected_item = None;
1783 self.select_first_visible_item();
1784 }
1785
1786 if self.init_search {
1787 re.request_focus();
1788 Self::set_cursor_to_end(&re, &self.search_value);
1789 self.directory_content.reset_multi_selection();
1790
1791 self.init_search = false;
1792 }
1793 });
1794 });
1795 }
1796
1797 fn edit_search_on_text_input(&mut self, ui: &egui::Ui) {
1804 if ui.memory(|mem| mem.focused().is_some()) {
1805 return;
1806 }
1807
1808 ui.input(|inp| {
1809 if inp.modifiers.any() && !inp.modifiers.shift_only() {
1811 return;
1812 }
1813
1814 for text in inp.events.iter().filter_map(|ev| match ev {
1817 egui::Event::Text(t) => Some(t),
1818 _ => None,
1819 }) {
1820 self.search_value.push_str(text);
1821 self.init_search = true;
1822 }
1823 });
1824 }
1825
1826 fn ui_update_left_panel(&mut self, ui: &mut egui::Ui) {
1829 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
1830 const SPACING_MULTIPLIER: f32 = 4.0;
1832
1833 egui::containers::ScrollArea::vertical()
1834 .auto_shrink([false, false])
1835 .show(ui, |ui| {
1836 let mut spacing = ui.global_style().spacing.item_spacing.y * 2.0;
1838
1839 if self.config.show_pinned_folders && self.ui_update_pinned_folders(ui, spacing)
1841 {
1842 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1843 }
1844
1845 let quick_accesses = std::mem::take(&mut self.config.quick_accesses);
1847
1848 for quick_access in &quick_accesses {
1849 ui.add_space(spacing);
1850 self.ui_update_quick_access(ui, quick_access);
1851 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1852 }
1853
1854 self.config.quick_accesses = quick_accesses;
1855
1856 if self.config.show_places && self.ui_update_user_directories(ui, spacing) {
1858 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1859 }
1860
1861 let disks = std::mem::take(&mut self.system_disks);
1862
1863 if self.config.show_devices && self.ui_update_devices(ui, spacing, &disks) {
1864 spacing = ui.global_style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1865 }
1866
1867 if self.config.show_removable_devices
1868 && self.ui_update_removable_devices(ui, spacing, &disks)
1869 {
1870 }
1873
1874 self.system_disks = disks;
1875 });
1876 });
1877 }
1878
1879 fn ui_update_left_panel_entry(
1883 &mut self,
1884 ui: &mut egui::Ui,
1885 display_name: &str,
1886 path: &Path,
1887 ) -> egui::Response {
1888 let response = ui.selectable_label(self.current_directory() == Some(path), display_name);
1889
1890 if response.clicked() {
1891 self.load_directory(path);
1892 }
1893
1894 response
1895 }
1896
1897 fn ui_update_quick_access(&mut self, ui: &mut egui::Ui, quick_access: &QuickAccess) {
1899 ui.label(&quick_access.heading);
1900
1901 for entry in &quick_access.paths {
1902 self.ui_update_left_panel_entry(ui, &entry.display_name, &entry.path);
1903 }
1904 }
1905
1906 fn ui_update_pinned_folders(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1911 let mut visible = false;
1912
1913 for (i, pinned) in self.storage.pinned_folders.clone().iter().enumerate() {
1914 if i == 0 {
1915 ui.add_space(spacing);
1916 ui.label(self.config.labels.heading_pinned.as_str());
1917
1918 visible = true;
1919 }
1920
1921 if self.is_pinned_folder_being_renamed(pinned) {
1922 self.ui_update_pinned_folder_rename(ui);
1923 continue;
1924 }
1925
1926 let response = self.ui_update_left_panel_entry(
1927 ui,
1928 &format!("{} {}", self.config.pinned_icon, &pinned.label),
1929 pinned.path.as_path(),
1930 );
1931
1932 self.ui_update_pinned_folder_context_menu(&response, pinned);
1933 }
1934
1935 visible
1936 }
1937
1938 fn ui_update_pinned_folder_rename(&mut self, ui: &mut egui::Ui) {
1939 if let Some(r) = &mut self.rename_pinned_folder {
1940 let id = self.window_id.with("pinned_folder_rename").with(&r.path);
1941 let mut output = egui::TextEdit::singleline(&mut r.label)
1942 .id(id)
1943 .cursor_at_end(true)
1944 .show(ui);
1945
1946 if self.rename_pinned_folder_request_focus {
1947 output.state.cursor.set_char_range(Some(CCursorRange::two(
1948 CCursor::new(0),
1949 CCursor::new(r.label.chars().count()),
1950 )));
1951 output.state.store(ui.ctx(), output.response.id);
1952
1953 output.response.request_focus();
1954
1955 self.rename_pinned_folder_request_focus = false;
1956 }
1957
1958 if output.response.lost_focus() {
1959 self.end_rename_pinned_folder();
1960 }
1961 }
1962 }
1963
1964 fn ui_update_pinned_folder_context_menu(
1965 &mut self,
1966 item: &egui::Response,
1967 pinned: &PinnedFolder,
1968 ) {
1969 item.context_menu(|ui| {
1970 if ui.button(&self.config.labels.unpin_folder).clicked() {
1971 self.unpin_path(&pinned.path);
1972 ui.close();
1973 }
1974
1975 if ui
1976 .button(&self.config.labels.rename_pinned_folder)
1977 .clicked()
1978 {
1979 self.begin_rename_pinned_folder(pinned.clone());
1980 ui.close();
1981 }
1982 });
1983 }
1984
1985 fn ui_update_user_directories(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1990 let user_directories = std::mem::take(&mut self.user_directories);
1994 let labels = std::mem::take(&mut self.config.labels);
1995
1996 let visible = if let Some(dirs) = &user_directories {
1997 ui.add_space(spacing);
1998 ui.label(labels.heading_places.as_str());
1999
2000 if let Some(path) = dirs.home_dir() {
2001 self.ui_update_left_panel_entry(ui, &labels.home_dir, path);
2002 }
2003 if let Some(path) = dirs.desktop_dir() {
2004 self.ui_update_left_panel_entry(ui, &labels.desktop_dir, path);
2005 }
2006 if let Some(path) = dirs.document_dir() {
2007 self.ui_update_left_panel_entry(ui, &labels.documents_dir, path);
2008 }
2009 if let Some(path) = dirs.download_dir() {
2010 self.ui_update_left_panel_entry(ui, &labels.downloads_dir, path);
2011 }
2012 if let Some(path) = dirs.audio_dir() {
2013 self.ui_update_left_panel_entry(ui, &labels.audio_dir, path);
2014 }
2015 if let Some(path) = dirs.picture_dir() {
2016 self.ui_update_left_panel_entry(ui, &labels.pictures_dir, path);
2017 }
2018 if let Some(path) = dirs.video_dir() {
2019 self.ui_update_left_panel_entry(ui, &labels.videos_dir, path);
2020 }
2021
2022 true
2023 } else {
2024 false
2025 };
2026
2027 self.user_directories = user_directories;
2028 self.config.labels = labels;
2029
2030 visible
2031 }
2032
2033 fn ui_update_devices(&mut self, ui: &mut egui::Ui, spacing: f32, disks: &Disks) -> bool {
2038 let mut visible = false;
2039
2040 for (i, disk) in disks.iter().filter(|x| !x.is_removable()).enumerate() {
2041 if i == 0 {
2042 ui.add_space(spacing);
2043 ui.label(self.config.labels.heading_devices.as_str());
2044
2045 visible = true;
2046 }
2047
2048 self.ui_update_device_entry(ui, disk);
2049 }
2050
2051 visible
2052 }
2053
2054 fn ui_update_removable_devices(
2059 &mut self,
2060 ui: &mut egui::Ui,
2061 spacing: f32,
2062 disks: &Disks,
2063 ) -> bool {
2064 let mut visible = false;
2065
2066 for (i, disk) in disks.iter().filter(|x| x.is_removable()).enumerate() {
2067 if i == 0 {
2068 ui.add_space(spacing);
2069 ui.label(self.config.labels.heading_removable_devices.as_str());
2070
2071 visible = true;
2072 }
2073
2074 self.ui_update_device_entry(ui, disk);
2075 }
2076
2077 visible
2078 }
2079
2080 fn ui_update_device_entry(&mut self, ui: &mut egui::Ui, device: &Disk) {
2082 let label = if device.is_removable() {
2083 format!(
2084 "{} {}",
2085 self.config.removable_device_icon,
2086 device.display_name()
2087 )
2088 } else {
2089 format!("{} {}", self.config.device_icon, device.display_name())
2090 };
2091
2092 self.ui_update_left_panel_entry(ui, &label, device.mount_point());
2093 }
2094
2095 fn ui_update_bottom_panel(&mut self, ui: &mut egui::Ui) {
2097 const BUTTON_HEIGHT: f32 = 20.0;
2098 ui.add_space(5.0);
2099
2100 let label_submit_width = match self.mode {
2102 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2103 Self::calc_text_width(ui, &self.config.labels.open_button)
2104 }
2105 DialogMode::SaveFile => Self::calc_text_width(ui, &self.config.labels.save_button),
2106 };
2107
2108 let mut btn_width = Self::calc_text_width(ui, &self.config.labels.cancel_button);
2109 if label_submit_width > btn_width {
2110 btn_width = label_submit_width;
2111 }
2112
2113 btn_width += ui.spacing().button_padding.x * 4.0;
2114
2115 let button_size: egui::Vec2 = egui::Vec2::new(btn_width, BUTTON_HEIGHT);
2117
2118 self.ui_update_selection_preview(ui, button_size);
2119
2120 if self.mode == DialogMode::SaveFile && self.config.save_extensions.is_empty() {
2121 ui.add_space(ui.style().spacing.item_spacing.y);
2122 }
2123
2124 self.ui_update_action_buttons(ui, button_size);
2125 }
2126
2127 fn ui_update_selection_preview(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2129 const SELECTION_PREVIEW_MIN_WIDTH: f32 = 50.0;
2130 let item_spacing = ui.style().spacing.item_spacing;
2131
2132 let render_filter_selection = (!self.config.file_filters.is_empty()
2133 && (self.mode == DialogMode::PickFile || self.mode == DialogMode::PickMultiple))
2134 || (!self.config.save_extensions.is_empty() && self.mode == DialogMode::SaveFile);
2135
2136 let filter_selection_width = button_size.x.mul_add(2.0, item_spacing.x);
2137 let mut filter_selection_separate_line = false;
2138
2139 ui.horizontal(|ui| {
2140 match &self.mode {
2141 DialogMode::PickDirectory => ui.label(&self.config.labels.selected_directory),
2142 DialogMode::PickFile => ui.label(&self.config.labels.selected_file),
2143 DialogMode::PickMultiple => ui.label(&self.config.labels.selected_items),
2144 DialogMode::SaveFile => ui.label(&self.config.labels.file_name),
2145 };
2146
2147 let mut scroll_bar_width: f32 =
2152 ui.available_width() - filter_selection_width - item_spacing.x;
2153
2154 if scroll_bar_width < SELECTION_PREVIEW_MIN_WIDTH || !render_filter_selection {
2155 filter_selection_separate_line = true;
2156 scroll_bar_width = ui.available_width();
2157 }
2158
2159 match &self.mode {
2160 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2161 use egui::containers::scroll_area::ScrollBarVisibility;
2162
2163 let text = self.get_selection_preview_text();
2164
2165 egui::containers::ScrollArea::horizontal()
2166 .auto_shrink([false, false])
2167 .max_width(scroll_bar_width)
2168 .stick_to_right(true)
2169 .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
2170 .show(ui, |ui| {
2171 ui.colored_label(ui.style().visuals.selection.bg_fill, text);
2172 });
2173 }
2174 DialogMode::SaveFile => {
2175 let mut output = egui::TextEdit::singleline(&mut self.file_name_input)
2176 .cursor_at_end(false)
2177 .margin(egui::Margin::symmetric(4, 3))
2178 .desired_width(scroll_bar_width - item_spacing.x)
2179 .show(ui);
2180
2181 if self.file_name_input_request_focus {
2182 self.highlight_file_name_input(&mut output);
2183 output.state.store(ui.ctx(), output.response.id);
2184
2185 output.response.request_focus();
2186 self.file_name_input_request_focus = false;
2187 }
2188
2189 if output.response.changed() {
2190 self.file_name_input_error = self.validate_file_name_input();
2191 }
2192
2193 if output.response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
2194 {
2195 self.submit();
2196 }
2197 }
2198 }
2199
2200 if !filter_selection_separate_line && render_filter_selection {
2201 if self.mode == DialogMode::SaveFile {
2202 self.ui_update_save_extension_selection(ui, filter_selection_width);
2203 } else {
2204 self.ui_update_file_filter_selection(ui, filter_selection_width);
2205 }
2206 }
2207 });
2208
2209 if filter_selection_separate_line && render_filter_selection {
2210 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2211 if self.mode == DialogMode::SaveFile {
2212 self.ui_update_save_extension_selection(ui, filter_selection_width);
2213 } else {
2214 self.ui_update_file_filter_selection(ui, filter_selection_width);
2215 }
2216 });
2217 }
2218 }
2219
2220 fn highlight_file_name_input(&self, output: &mut egui::text_edit::TextEditOutput) {
2224 if let Some(pos) = self.file_name_input.rfind('.') {
2225 let range = if pos == 0 {
2226 CCursorRange::two(CCursor::new(0), CCursor::new(0))
2227 } else {
2228 CCursorRange::two(CCursor::new(0), CCursor::new(pos))
2229 };
2230
2231 output.state.cursor.set_char_range(Some(range));
2232 }
2233 }
2234
2235 fn get_selection_preview_text(&self) -> String {
2236 if self.is_selection_valid() {
2237 match &self.mode {
2238 DialogMode::PickDirectory | DialogMode::PickFile => self
2239 .selected_item
2240 .as_ref()
2241 .map_or_else(String::new, |item| item.file_name().to_string()),
2242 DialogMode::PickMultiple => {
2243 let mut result = String::new();
2244
2245 for (i, item) in self
2246 .get_dir_content_filtered_iter()
2247 .filter(|p| p.selected)
2248 .enumerate()
2249 {
2250 if i == 0 {
2251 result += item.file_name();
2252 continue;
2253 }
2254
2255 result += format!(", {}", item.file_name()).as_str();
2256 }
2257
2258 result
2259 }
2260 DialogMode::SaveFile => String::new(),
2261 }
2262 } else {
2263 String::new()
2264 }
2265 }
2266
2267 fn ui_update_file_filter_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2268 let selected_filter = self.get_selected_file_filter();
2269 let selected_text = match selected_filter {
2270 Some(f) => &f.name,
2271 None => &self.config.labels.file_filter_all_files,
2272 };
2273
2274 let mut select_filter: Option<Option<FileFilter>> = None;
2277
2278 egui::containers::ComboBox::from_id_salt(self.window_id.with("file_filter_selection"))
2279 .width(width)
2280 .selected_text(selected_text)
2281 .wrap_mode(egui::TextWrapMode::Truncate)
2282 .show_ui(ui, |ui| {
2283 for filter in &self.config.file_filters {
2284 let selected = selected_filter.is_some_and(|f| f.id == filter.id);
2285
2286 if ui.selectable_label(selected, &filter.name).clicked() {
2287 select_filter = Some(Some(filter.clone()));
2288 }
2289 }
2290
2291 if self.config.show_all_files_filter
2292 && ui
2293 .selectable_label(
2294 selected_filter.is_none(),
2295 &self.config.labels.file_filter_all_files,
2296 )
2297 .clicked()
2298 {
2299 select_filter = Some(None);
2300 }
2301 });
2302
2303 if let Some(i) = select_filter {
2304 self.select_file_filter(i);
2305 }
2306 }
2307
2308 fn ui_update_save_extension_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2309 let selected_extension = self.get_selected_save_extension();
2310 let selected_text = match selected_extension {
2311 Some(e) => &e.to_string(),
2312 None => &self.config.labels.save_extension_any,
2313 };
2314
2315 let mut select_extension: Option<Option<SaveExtension>> = None;
2318
2319 egui::containers::ComboBox::from_id_salt(self.window_id.with("save_extension_selection"))
2320 .width(width)
2321 .selected_text(selected_text)
2322 .wrap_mode(egui::TextWrapMode::Truncate)
2323 .show_ui(ui, |ui| {
2324 for extension in &self.config.save_extensions {
2325 let selected = selected_extension.is_some_and(|s| s.id == extension.id);
2326
2327 if ui
2328 .selectable_label(selected, extension.to_string())
2329 .clicked()
2330 {
2331 select_extension = Some(Some(extension.clone()));
2332 }
2333 }
2334 });
2335
2336 if let Some(i) = select_extension {
2337 self.file_name_input_request_focus = true;
2338 self.select_save_extension(i);
2339 }
2340 }
2341
2342 fn ui_update_action_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2344 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2345 let label = match &self.mode {
2346 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2347 self.config.labels.open_button.as_str()
2348 }
2349 DialogMode::SaveFile => self.config.labels.save_button.as_str(),
2350 };
2351
2352 if self.ui_button_sized(
2353 ui,
2354 self.is_selection_valid(),
2355 button_size,
2356 label,
2357 self.file_name_input_error.as_deref(),
2358 ) {
2359 self.submit();
2360 }
2361
2362 if ui
2363 .add_sized(
2364 button_size,
2365 egui::Button::new(self.config.labels.cancel_button.as_str()),
2366 )
2367 .clicked()
2368 {
2369 self.cancel();
2370 }
2371 });
2372 }
2373
2374 fn ui_update_central_panel(&mut self, ui: &mut egui::Ui) {
2377 if self.update_directory_content(ui) {
2378 return;
2379 }
2380
2381 self.ui_update_central_panel_content(ui);
2382 }
2383
2384 fn update_directory_content(&mut self, ui: &mut egui::Ui) -> bool {
2389 const SHOW_SPINNER_AFTER: f32 = 0.2;
2390
2391 match self.directory_content.update() {
2392 DirectoryContentState::Pending(timestamp) => {
2393 let now = std::time::SystemTime::now();
2394
2395 if now
2396 .duration_since(*timestamp)
2397 .unwrap_or_default()
2398 .as_secs_f32()
2399 > SHOW_SPINNER_AFTER
2400 {
2401 ui.centered_and_justified(egui::Ui::spinner);
2402 }
2403
2404 ui.ctx().request_repaint();
2406
2407 true
2408 }
2409 DirectoryContentState::Errored(err) => {
2410 ui.centered_and_justified(|ui| ui.colored_label(ui.visuals().error_fg_color, err));
2411 true
2412 }
2413 DirectoryContentState::Finished => {
2414 if self.mode == DialogMode::PickDirectory {
2415 if let Some(dir) = self.current_directory() {
2416 let mut dir_entry =
2417 DirectoryEntry::from_path(&self.config, dir, &*self.config.file_system);
2418 self.select_item(&mut dir_entry);
2419 }
2420 }
2421
2422 false
2423 }
2424 DirectoryContentState::Success => false,
2425 }
2426 }
2427
2428 fn ui_update_central_panel_content(&mut self, ui: &mut egui::Ui) {
2431 let mut data = std::mem::take(&mut self.directory_content);
2433
2434 let mut selected_count = data
2437 .filtered_iter(&self.search_value)
2438 .filter(|item| item.selected)
2439 .count();
2440
2441 let mut reset_multi_selection = false;
2444
2445 let mut batch_select_item_b: Option<DirectoryEntry> = None;
2448
2449 let mut should_return = false;
2451
2452 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
2453 let scroll_area = egui::containers::ScrollArea::vertical().auto_shrink([false, false]);
2454
2455 if self.search_value.is_empty()
2456 && !self.create_directory_dialog.is_open()
2457 && !self.scroll_to_selection
2458 {
2459 let row_height = ui
2464 .spacing()
2465 .button_padding
2466 .y
2467 .mul_add(2.0, ui.text_style_height(&egui::TextStyle::Body));
2468
2469 scroll_area.show_rows(ui, row_height, data.len(), |ui, range| {
2470 for item in data.iter_range_mut(range) {
2471 if self.ui_update_central_panel_entry(
2472 ui,
2473 item,
2474 &mut reset_multi_selection,
2475 &mut batch_select_item_b,
2476 &mut selected_count,
2477 ) {
2478 should_return = true;
2479 }
2480 }
2481 });
2482 } else {
2483 scroll_area.show(ui, |ui| {
2489 for item in data.filtered_iter_mut(&self.search_value.clone()) {
2490 if self.ui_update_central_panel_entry(
2491 ui,
2492 item,
2493 &mut reset_multi_selection,
2494 &mut batch_select_item_b,
2495 &mut selected_count,
2496 ) {
2497 should_return = true;
2498 }
2499 }
2500
2501 if let Some(entry) = self.ui_update_create_directory_dialog(ui) {
2502 data.push(entry);
2503 }
2504 });
2505 }
2506 });
2507
2508 if should_return {
2509 return;
2510 }
2511
2512 if reset_multi_selection {
2514 for item in data.filtered_iter_mut(&self.search_value) {
2515 if let Some(selected_item) = &self.selected_item {
2516 if selected_item.path_eq(item) {
2517 continue;
2518 }
2519 }
2520
2521 item.selected = false;
2522 }
2523 }
2524
2525 if let Some(item_b) = batch_select_item_b {
2527 if let Some(item_a) = &self.selected_item {
2528 self.batch_select_between(&mut data, item_a, &item_b);
2529 }
2530 }
2531
2532 self.directory_content = data;
2533 self.scroll_to_selection = false;
2534 }
2535
2536 fn ui_update_central_panel_entry(
2539 &mut self,
2540 ui: &mut egui::Ui,
2541 item: &mut DirectoryEntry,
2542 reset_multi_selection: &mut bool,
2543 batch_select_item_b: &mut Option<DirectoryEntry>,
2544 selected_count: &mut usize,
2545 ) -> bool {
2546 let file_name = item.file_name();
2547 let primary_selected = self.is_primary_selected(item);
2548 let pinned = self.is_pinned(item.as_path());
2549
2550 let icons = if pinned {
2551 format!("{} {} ", item.icon(), self.config.pinned_icon)
2552 } else {
2553 format!("{} ", item.icon())
2554 };
2555
2556 let icons_width = Self::calc_text_width(ui, &icons);
2557
2558 let available_width = ui.available_width() - icons_width - 15.0;
2560
2561 let truncate = self.config.truncate_filenames
2562 && available_width < Self::calc_text_width(ui, file_name);
2563
2564 let text = if truncate {
2565 Self::truncate_filename(ui, item, available_width)
2566 } else {
2567 file_name.to_owned()
2568 };
2569
2570 let mut re =
2571 ui.selectable_label(primary_selected || item.selected, format!("{icons}{text}"));
2572
2573 if truncate {
2574 re = re.on_hover_text(file_name);
2575 }
2576
2577 if item.is_dir() {
2578 self.ui_update_central_panel_path_context_menu(&re, item.as_path());
2579
2580 if re.context_menu_opened() {
2581 self.select_item(item);
2582 }
2583 }
2584
2585 if primary_selected && self.scroll_to_selection {
2586 re.scroll_to_me(Some(egui::Align::Center));
2587 self.scroll_to_selection = false;
2588 }
2589
2590 if re.clicked()
2592 && !ui.input(|i| i.modifiers.command)
2593 && !ui.input(|i| i.modifiers.shift_only())
2594 {
2595 self.select_item(item);
2596
2597 if self.mode == DialogMode::PickMultiple {
2599 *reset_multi_selection = true;
2600 }
2601 }
2602
2603 if self.mode == DialogMode::PickMultiple
2606 && re.clicked()
2607 && ui.input(|i| i.modifiers.command)
2608 {
2609 if primary_selected {
2610 item.selected = false;
2613 self.selected_item = None;
2614 *selected_count = selected_count.saturating_sub(1);
2615 } else if !item.selected && self.selection_limit_reached_with(*selected_count) {
2616 } else {
2618 let was_selected = item.selected;
2619 item.selected = !item.selected;
2620
2621 if item.selected {
2622 *selected_count += 1;
2623 self.select_item(item);
2625 } else if was_selected {
2626 *selected_count = selected_count.saturating_sub(1);
2627 }
2628 }
2629 }
2630
2631 if self.mode == DialogMode::PickMultiple
2634 && re.clicked()
2635 && ui.input(|i| i.modifiers.shift_only())
2636 {
2637 if self.selection_limit_reached_with(*selected_count) && !item.selected {
2638 } else if let Some(selected_item) = self.selected_item.clone() {
2640 *batch_select_item_b = Some(selected_item);
2643
2644 if !item.selected {
2646 *selected_count += 1;
2647 }
2648 item.selected = true;
2649 self.select_item(item);
2650 }
2651 }
2652
2653 if re.double_clicked() && !ui.input(|i| i.modifiers.command) {
2656 if item.is_dir() {
2657 if self.should_open_directory(item.as_path()) {
2660 self.load_directory(&item.to_path_buf());
2661 return true;
2662 }
2663 }
2665
2666 self.select_item(item);
2667
2668 self.submit();
2669 }
2670
2671 false
2672 }
2673
2674 fn ui_update_create_directory_dialog(&mut self, ui: &mut egui::Ui) -> Option<DirectoryEntry> {
2675 self.create_directory_dialog
2676 .update(ui, &self.config)
2677 .directory()
2678 .map(|path| self.process_new_folder(&path))
2679 }
2680
2681 fn batch_select_between(
2684 &self,
2685 directory_content: &mut DirectoryContent,
2686 item_a: &DirectoryEntry,
2687 item_b: &DirectoryEntry,
2688 ) {
2689 let pos_a = directory_content
2691 .filtered_iter(&self.search_value)
2692 .position(|p| p.path_eq(item_a));
2693 let pos_b = directory_content
2694 .filtered_iter(&self.search_value)
2695 .position(|p| p.path_eq(item_b));
2696
2697 if let Some(pos_a) = pos_a {
2700 if let Some(pos_b) = pos_b {
2701 if pos_a == pos_b {
2702 return;
2703 }
2704
2705 let mut min = pos_a;
2708 let mut max = pos_b;
2709
2710 if min > max {
2711 min = pos_b;
2712 max = pos_a;
2713 }
2714
2715 let mut current_selected = directory_content
2718 .filtered_iter(&self.search_value)
2719 .filter(|item| item.selected)
2720 .count();
2721
2722 for item in directory_content
2723 .filtered_iter_mut(&self.search_value)
2724 .enumerate()
2725 .filter(|(i, _)| i > &min && i < &max)
2726 .map(|(_, p)| p)
2727 {
2728 if self.selection_limit_reached_with(current_selected) {
2729 break;
2730 }
2731 if !item.selected {
2732 current_selected += 1;
2733 }
2734 item.selected = true;
2735 }
2736 }
2737 }
2738 }
2739
2740 fn ui_button_sized(
2742 &self,
2743 ui: &mut egui::Ui,
2744 enabled: bool,
2745 size: egui::Vec2,
2746 label: &str,
2747 err_tooltip: Option<&str>,
2748 ) -> bool {
2749 let mut clicked = false;
2750
2751 ui.add_enabled_ui(enabled, |ui| {
2752 let response = ui.add_sized(size, egui::Button::new(label));
2753 clicked = response.clicked();
2754
2755 if let Some(err) = err_tooltip {
2756 response.on_disabled_hover_ui(|ui| {
2757 ui.horizontal_wrapped(|ui| {
2758 ui.spacing_mut().item_spacing.x = 0.0;
2759
2760 ui.colored_label(
2761 ui.global_style().visuals.error_fg_color,
2762 format!("{} ", self.config.err_icon),
2763 );
2764
2765 ui.label(err);
2766 });
2767 });
2768 }
2769 });
2770
2771 clicked
2772 }
2773
2774 fn ui_update_central_panel_path_context_menu(&mut self, item: &egui::Response, path: &Path) {
2781 if !self.config.show_pinned_folders {
2783 return;
2784 }
2785
2786 item.context_menu(|ui| {
2787 let pinned = self.is_pinned(path);
2788
2789 if pinned {
2790 if ui.button(&self.config.labels.unpin_folder).clicked() {
2791 self.unpin_path(path);
2792 ui.close();
2793 }
2794 } else if ui.button(&self.config.labels.pin_folder).clicked() {
2795 self.pin_path(path.to_path_buf());
2796 ui.close();
2797 }
2798 });
2799 }
2800
2801 fn set_cursor_to_end(re: &egui::Response, data: &str) {
2808 if let Some(mut state) = egui::TextEdit::load_state(&re.ctx, re.id) {
2810 state
2811 .cursor
2812 .set_char_range(Some(CCursorRange::one(CCursor::new(data.len()))));
2813 state.store(&re.ctx, re.id);
2814 }
2815 }
2816
2817 fn calc_char_width(ui: &egui::Ui, char: char) -> f32 {
2819 ui.fonts_mut(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char))
2820 }
2821
2822 fn calc_text_width(ui: &egui::Ui, text: &str) -> f32 {
2825 let mut width = 0.0;
2826
2827 for char in text.chars() {
2828 width += Self::calc_char_width(ui, char);
2829 }
2830
2831 width
2832 }
2833
2834 fn truncate_filename(ui: &egui::Ui, item: &DirectoryEntry, max_length: f32) -> String {
2835 const TRUNCATE_STR: &str = "...";
2836
2837 let path = item.as_path();
2838
2839 let file_stem = if item.is_file() {
2840 path.file_stem().and_then(|f| f.to_str()).unwrap_or("")
2841 } else {
2842 item.file_name()
2843 };
2844
2845 let extension = if item.is_file() {
2846 path.extension().map_or(String::new(), |ext| {
2847 format!(".{}", ext.to_str().unwrap_or(""))
2848 })
2849 } else {
2850 String::new()
2851 };
2852
2853 let extension_width = Self::calc_text_width(ui, &extension);
2854 let reserved = extension_width + Self::calc_text_width(ui, TRUNCATE_STR);
2855
2856 if max_length <= reserved {
2857 return format!("{TRUNCATE_STR}{extension}");
2858 }
2859
2860 let mut width = reserved;
2861 let mut front = String::new();
2862 let mut back = String::new();
2863
2864 for (i, char) in file_stem.chars().enumerate() {
2865 let w = Self::calc_char_width(ui, char);
2866
2867 if width + w > max_length {
2868 break;
2869 }
2870
2871 front.push(char);
2872 width += w;
2873
2874 let back_index = file_stem.len() - i - 1;
2875
2876 if back_index <= i {
2877 break;
2878 }
2879
2880 if let Some(char) = file_stem.chars().nth(back_index) {
2881 let w = Self::calc_char_width(ui, char);
2882
2883 if width + w > max_length {
2884 break;
2885 }
2886
2887 back.push(char);
2888 width += w;
2889 }
2890 }
2891
2892 format!(
2893 "{front}{TRUNCATE_STR}{}{extension}",
2894 back.chars().rev().collect::<String>()
2895 )
2896 }
2897}
2898
2899impl FileDialog {
2901 fn update_keybindings(&mut self, ctx: &egui::Context) {
2903 if let Some(modal) = self.modals.last_mut() {
2906 modal.update_keybindings(&self.config, ctx);
2907 return;
2908 }
2909
2910 let keybindings = std::mem::take(&mut self.config.keybindings);
2911
2912 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.submit, false) {
2913 self.exec_keybinding_submit();
2914 }
2915
2916 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.cancel, false) {
2917 self.exec_keybinding_cancel();
2918 }
2919
2920 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.parent, true) {
2921 self.load_parent_directory();
2922 }
2923
2924 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.back, true) {
2925 self.load_previous_directory();
2926 }
2927
2928 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.forward, true) {
2929 self.load_next_directory();
2930 }
2931
2932 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.reload, true) {
2933 self.refresh();
2934 }
2935
2936 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.new_folder, true) {
2937 self.open_new_folder_dialog();
2938 }
2939
2940 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.edit_path, true) {
2941 self.open_path_edit();
2942 }
2943
2944 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.home_edit_path, true) {
2945 if let Some(dirs) = &self.user_directories {
2946 if let Some(home) = dirs.home_dir() {
2947 self.load_directory(home.to_path_buf().as_path());
2948 self.open_path_edit();
2949 }
2950 }
2951 }
2952
2953 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_up, false) {
2954 self.exec_keybinding_selection_up();
2955
2956 if let Some(id) = ctx.memory(egui::Memory::focused) {
2958 ctx.memory_mut(|w| w.surrender_focus(id));
2959 }
2960 }
2961
2962 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_down, false) {
2963 self.exec_keybinding_selection_down();
2964
2965 if let Some(id) = ctx.memory(egui::Memory::focused) {
2967 ctx.memory_mut(|w| w.surrender_focus(id));
2968 }
2969 }
2970
2971 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.select_all, true)
2972 && self.mode == DialogMode::PickMultiple
2973 {
2974 self.select_all_items();
2975 }
2976
2977 self.config.keybindings = keybindings;
2978 }
2979
2980 fn exec_keybinding_submit(&mut self) {
2982 if self.path_edit_visible {
2983 self.submit_path_edit();
2984 return;
2985 }
2986
2987 if self.create_directory_dialog.is_open() {
2988 if let Some(dir) = self.create_directory_dialog.submit().directory() {
2989 self.process_new_folder(&dir);
2990 }
2991 return;
2992 }
2993
2994 if self.any_focused_last_frame {
2995 return;
2996 }
2997
2998 if let Some(item) = &self.selected_item {
3000 let is_visible = self
3002 .get_dir_content_filtered_iter()
3003 .any(|p| p.path_eq(item));
3004
3005 if is_visible && item.is_dir() {
3006 self.load_directory(&item.to_path_buf());
3007 return;
3008 }
3009 }
3010
3011 self.submit();
3012 }
3013
3014 fn exec_keybinding_cancel(&mut self) {
3016 if self.create_directory_dialog.is_open() {
3034 self.create_directory_dialog.close();
3035 } else if self.path_edit_visible {
3036 self.close_path_edit();
3037 } else if !self.any_focused_last_frame {
3038 self.cancel();
3039 }
3040 }
3041
3042 fn exec_keybinding_selection_up(&mut self) {
3044 if self.directory_content.len() == 0 {
3045 return;
3046 }
3047
3048 self.directory_content.reset_multi_selection();
3049
3050 if let Some(item) = &self.selected_item {
3051 if self.select_next_visible_item_before(&item.clone()) {
3052 return;
3053 }
3054 }
3055
3056 self.select_last_visible_item();
3059 }
3060
3061 fn exec_keybinding_selection_down(&mut self) {
3063 if self.directory_content.len() == 0 {
3064 return;
3065 }
3066
3067 self.directory_content.reset_multi_selection();
3068
3069 if let Some(item) = &self.selected_item {
3070 if self.select_next_visible_item_after(&item.clone()) {
3071 return;
3072 }
3073 }
3074
3075 self.select_first_visible_item();
3078 }
3079}
3080
3081impl FileDialog {
3083 fn get_selected_file_filter(&self) -> Option<&FileFilter> {
3085 self.selected_file_filter
3086 .and_then(|id| self.config.file_filters.iter().find(|p| p.id == id))
3087 }
3088
3089 fn set_default_file_filter(&mut self) {
3091 if let Some(name) = &self.config.default_file_filter {
3092 for filter in &self.config.file_filters {
3093 if filter.name == name.as_str() {
3094 self.selected_file_filter = Some(filter.id);
3095 }
3096 }
3097 }
3098 }
3099
3100 fn select_file_filter(&mut self, filter: Option<FileFilter>) {
3102 self.selected_file_filter = filter.map(|f| f.id);
3103 self.selected_item = None;
3104 self.refresh();
3105 }
3106
3107 fn get_selected_save_extension(&self) -> Option<&SaveExtension> {
3109 self.selected_save_extension
3110 .and_then(|id| self.config.save_extensions.iter().find(|p| p.id == id))
3111 }
3112
3113 fn set_default_save_extension(&mut self) {
3115 let config = std::mem::take(&mut self.config);
3116
3117 if let Some(name) = &config.default_save_extension {
3118 for extension in &config.save_extensions {
3119 if extension.name == name.as_str() {
3120 self.selected_save_extension = Some(extension.id);
3121 self.set_file_name_extension(&extension.file_extension);
3122 }
3123 }
3124 }
3125
3126 self.config = config;
3127 }
3128
3129 fn select_save_extension(&mut self, extension: Option<SaveExtension>) {
3131 if let Some(ex) = extension {
3132 self.selected_save_extension = Some(ex.id);
3133 self.set_file_name_extension(&ex.file_extension);
3134 }
3135
3136 self.selected_item = None;
3137 self.refresh();
3138 }
3139
3140 fn set_file_name_extension(&mut self, extension: &str) {
3142 let dot_count = self.file_name_input.chars().filter(|c| *c == '.').count();
3146 let use_simple = dot_count == 1 && self.file_name_input.chars().nth(0) == Some('.');
3147
3148 let mut p = PathBuf::from(&self.file_name_input);
3149 if !use_simple && p.set_extension(extension) {
3150 self.file_name_input = p.to_string_lossy().into_owned();
3151 } else {
3152 self.file_name_input = format!(".{extension}");
3153 }
3154 }
3155
3156 fn get_dir_content_filtered_iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
3158 self.directory_content.filtered_iter(&self.search_value)
3159 }
3160
3161 fn open_new_folder_dialog(&mut self) {
3163 if let Some(x) = self.current_directory() {
3164 self.create_directory_dialog.open(x.to_path_buf());
3165 }
3166 }
3167
3168 fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry {
3170 let mut entry =
3171 DirectoryEntry::from_path(&self.config, created_dir, &*self.config.file_system);
3172
3173 self.directory_content.push(entry.clone());
3174
3175 self.select_item(&mut entry);
3176
3177 entry
3178 }
3179
3180 fn open_modal(&mut self, modal: Box<dyn FileDialogModal + Send + Sync>) {
3182 self.modals.push(modal);
3183 }
3184
3185 fn exec_modal_action(&mut self, action: ModalAction) {
3187 match action {
3188 ModalAction::None => {}
3189 ModalAction::SaveFile(path) => self.state = DialogState::Picked(path),
3190 }
3191 }
3192
3193 fn canonicalize_path(&self, path: &Path) -> PathBuf {
3196 if self.config.canonicalize_paths {
3197 dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
3198 } else {
3199 path.to_path_buf()
3200 }
3201 }
3202
3203 fn pin_path(&mut self, path: PathBuf) {
3205 let pinned = PinnedFolder::from_path(path);
3206 self.storage.pinned_folders.push(pinned);
3207 }
3208
3209 fn unpin_path(&mut self, path: &Path) {
3211 self.storage
3212 .pinned_folders
3213 .retain(|p| p.path.as_path() != path);
3214 }
3215
3216 fn is_pinned(&self, path: &Path) -> bool {
3218 self.storage
3219 .pinned_folders
3220 .iter()
3221 .any(|p| p.path.as_path() == path)
3222 }
3223
3224 fn begin_rename_pinned_folder(&mut self, pinned: PinnedFolder) {
3226 self.rename_pinned_folder = Some(pinned);
3227 self.rename_pinned_folder_request_focus = true;
3228 }
3229
3230 fn end_rename_pinned_folder(&mut self) {
3233 let renamed = std::mem::take(&mut self.rename_pinned_folder);
3234
3235 if let Some(renamed) = renamed {
3236 let old = self
3237 .storage
3238 .pinned_folders
3239 .iter_mut()
3240 .find(|p| p.path == renamed.path);
3241 if let Some(old) = old {
3242 old.label = renamed.label;
3243 }
3244 }
3245 }
3246
3247 fn is_pinned_folder_being_renamed(&self, pinned: &PinnedFolder) -> bool {
3249 self.rename_pinned_folder
3250 .as_ref()
3251 .is_some_and(|p| p.path == pinned.path)
3252 }
3253
3254 fn is_primary_selected(&self, item: &DirectoryEntry) -> bool {
3255 self.selected_item.as_ref().is_some_and(|x| x.path_eq(item))
3256 }
3257
3258 fn reset(&mut self) {
3261 let user_data = std::mem::take(&mut self.user_data);
3262 let storage = self.storage.clone();
3263 let config = self.config.clone();
3264
3265 *self = Self::with_config(config);
3266 self.storage = storage;
3267 self.user_data = user_data;
3268 }
3269
3270 fn refresh(&mut self) {
3273 self.user_directories = self
3274 .config
3275 .file_system
3276 .user_dirs(self.config.canonicalize_paths);
3277 self.system_disks = self
3278 .config
3279 .file_system
3280 .get_disks(self.config.canonicalize_paths);
3281
3282 self.reload_directory();
3283 }
3284
3285 fn submit(&mut self) {
3287 if !self.is_selection_valid() {
3289 return;
3290 }
3291
3292 self.storage.last_picked_dir = self.current_directory().map(PathBuf::from);
3293
3294 match &self.mode {
3295 DialogMode::PickDirectory | DialogMode::PickFile => {
3296 if let Some(item) = self.selected_item.clone() {
3299 self.state = DialogState::Picked(item.to_path_buf());
3300 }
3301 }
3302 DialogMode::PickMultiple => {
3303 let result: Vec<PathBuf> = self
3304 .selected_entries()
3305 .map(crate::DirectoryEntry::to_path_buf)
3306 .collect();
3307
3308 self.state = DialogState::PickedMultiple(result);
3309 }
3310 DialogMode::SaveFile => {
3311 if let Some(path) = self.current_directory() {
3314 let full_path = path.join(&self.file_name_input);
3315 self.submit_save_file(full_path);
3316 }
3317 }
3318 }
3319 }
3320
3321 fn submit_save_file(&mut self, path: PathBuf) {
3324 if path.exists() {
3325 self.open_modal(Box::new(OverwriteFileModal::new(path)));
3326
3327 return;
3328 }
3329
3330 self.state = DialogState::Picked(path);
3331 }
3332
3333 fn cancel(&mut self) {
3335 self.state = DialogState::Cancelled;
3336 }
3337
3338 fn get_initial_directory(&self) -> PathBuf {
3344 let path = match self.config.opening_mode {
3345 OpeningMode::AlwaysInitialDir => &self.config.initial_directory,
3346 OpeningMode::LastVisitedDir => self
3347 .storage
3348 .last_visited_dir
3349 .as_deref()
3350 .unwrap_or(&self.config.initial_directory),
3351 OpeningMode::LastPickedDir => self
3352 .storage
3353 .last_picked_dir
3354 .as_deref()
3355 .unwrap_or(&self.config.initial_directory),
3356 };
3357
3358 let mut path = self.canonicalize_path(path);
3359
3360 if self.config.file_system.is_file(&path) {
3361 if let Some(parent) = path.parent() {
3362 path = parent.to_path_buf();
3363 }
3364 }
3365
3366 path
3367 }
3368
3369 fn current_directory(&self) -> Option<&Path> {
3371 if let Some(x) = self.directory_stack.iter().nth_back(self.directory_offset) {
3372 return Some(x.as_path());
3373 }
3374
3375 None
3376 }
3377
3378 fn is_selection_valid(&self) -> bool {
3381 match &self.mode {
3382 DialogMode::PickDirectory => self
3383 .selected_item
3384 .as_ref()
3385 .is_some_and(crate::DirectoryEntry::is_dir),
3386 DialogMode::PickFile => self
3387 .selected_item
3388 .as_ref()
3389 .is_some_and(DirectoryEntry::is_file),
3390 DialogMode::PickMultiple => self.get_dir_content_filtered_iter().any(|p| p.selected),
3391 DialogMode::SaveFile => self.file_name_input_error.is_none(),
3392 }
3393 }
3394
3395 fn validate_file_name_input(&self) -> Option<String> {
3399 if self.file_name_input.is_empty() {
3400 return Some(self.config.labels.err_empty_file_name.clone());
3401 }
3402
3403 if let Some(x) = self.current_directory() {
3404 let mut full_path = x.to_path_buf();
3405 full_path.push(self.file_name_input.as_str());
3406
3407 if self.config.file_system.is_dir(&full_path) {
3408 return Some(self.config.labels.err_directory_exists.clone());
3409 }
3410
3411 if !self.config.allow_file_overwrite && self.config.file_system.is_file(&full_path) {
3412 return Some(self.config.labels.err_file_exists.clone());
3413 }
3414 } else {
3415 return Some("Currently not in a directory".to_string());
3417 }
3418
3419 None
3420 }
3421
3422 fn select_item(&mut self, item: &mut DirectoryEntry) {
3425 if self.mode == DialogMode::PickMultiple {
3426 item.selected = true;
3427 }
3428 self.selected_item = Some(item.clone());
3429
3430 if self.mode == DialogMode::SaveFile && item.is_file() {
3431 self.file_name_input = item.file_name().to_string();
3432 self.file_name_input_error = self.validate_file_name_input();
3433 }
3434 }
3435
3436 fn select_next_visible_item_before(&mut self, item: &DirectoryEntry) -> bool {
3441 let mut return_val = false;
3442
3443 self.directory_content.reset_multi_selection();
3444
3445 let mut directory_content = std::mem::take(&mut self.directory_content);
3446 let search_value = std::mem::take(&mut self.search_value);
3447
3448 let index = directory_content
3449 .filtered_iter(&search_value)
3450 .position(|p| p.path_eq(item));
3451
3452 if let Some(index) = index {
3453 if index != 0 {
3454 if let Some(item) = directory_content
3455 .filtered_iter_mut(&search_value)
3456 .nth(index.saturating_sub(1))
3457 {
3458 self.select_item(item);
3459 self.scroll_to_selection = true;
3460 return_val = true;
3461 }
3462 }
3463 }
3464
3465 self.directory_content = directory_content;
3466 self.search_value = search_value;
3467
3468 return_val
3469 }
3470
3471 fn select_next_visible_item_after(&mut self, item: &DirectoryEntry) -> bool {
3476 let mut return_val = false;
3477
3478 self.directory_content.reset_multi_selection();
3479
3480 let mut directory_content = std::mem::take(&mut self.directory_content);
3481 let search_value = std::mem::take(&mut self.search_value);
3482
3483 let index = directory_content
3484 .filtered_iter(&search_value)
3485 .position(|p| p.path_eq(item));
3486
3487 if let Some(index) = index {
3488 if let Some(item) = directory_content
3489 .filtered_iter_mut(&search_value)
3490 .nth(index.saturating_add(1))
3491 {
3492 self.select_item(item);
3493 self.scroll_to_selection = true;
3494 return_val = true;
3495 }
3496 }
3497
3498 self.directory_content = directory_content;
3499 self.search_value = search_value;
3500
3501 return_val
3502 }
3503
3504 fn select_first_visible_item(&mut self) {
3506 self.directory_content.reset_multi_selection();
3507
3508 let mut directory_content = std::mem::take(&mut self.directory_content);
3509
3510 if let Some(item) = directory_content
3511 .filtered_iter_mut(&self.search_value.clone())
3512 .next()
3513 {
3514 self.select_item(item);
3515 self.scroll_to_selection = true;
3516 }
3517
3518 self.directory_content = directory_content;
3519 }
3520
3521 fn select_last_visible_item(&mut self) {
3523 self.directory_content.reset_multi_selection();
3524
3525 let mut directory_content = std::mem::take(&mut self.directory_content);
3526
3527 if let Some(item) = directory_content
3528 .filtered_iter_mut(&self.search_value.clone())
3529 .last()
3530 {
3531 self.select_item(item);
3532 self.scroll_to_selection = true;
3533 }
3534
3535 self.directory_content = directory_content;
3536 }
3537
3538 fn selection_limit_reached_with(&self, selected_count: usize) -> bool {
3540 self.config
3541 .max_selections
3542 .is_some_and(|max| selected_count >= max)
3543 }
3544
3545 fn select_all_items(&mut self) {
3547 let mut selected_count = self
3548 .directory_content
3549 .filtered_iter(&self.search_value)
3550 .filter(|p| p.selected)
3551 .count();
3552
3553 for item in self.directory_content.filtered_iter_mut(&self.search_value) {
3554 if item.selected {
3555 continue; }
3557 if self
3558 .config
3559 .max_selections
3560 .is_some_and(|max| selected_count >= max)
3561 {
3562 break;
3563 }
3564 item.selected = true;
3565 selected_count += 1;
3566 }
3567 }
3568
3569 fn open_path_edit(&mut self) {
3571 let path = self.current_directory().map_or_else(String::new, |path| {
3572 path.to_str().unwrap_or_default().to_string()
3573 });
3574
3575 self.path_edit_value = path;
3576 self.path_edit_activate = true;
3577 self.path_edit_visible = true;
3578 }
3579
3580 fn submit_path_edit(&mut self) {
3582 self.close_path_edit();
3583
3584 let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value));
3585
3586 if self.mode == DialogMode::PickFile && self.config.file_system.is_file(&path) {
3587 self.state = DialogState::Picked(path);
3588 return;
3589 }
3590
3591 if self.mode == DialogMode::SaveFile
3598 && (path.extension().is_some()
3599 || self.config.allow_path_edit_to_save_file_without_extension)
3600 && !self.config.file_system.is_dir(&path)
3601 && path.parent().is_some_and(std::path::Path::exists)
3602 {
3603 self.submit_save_file(path);
3604 return;
3605 }
3606
3607 self.load_directory(&path);
3608 }
3609
3610 const fn close_path_edit(&mut self) {
3613 self.path_edit_visible = false;
3614 }
3615
3616 fn load_next_directory(&mut self) {
3621 if self.directory_offset == 0 {
3622 return;
3624 }
3625
3626 self.directory_offset -= 1;
3627
3628 if let Some(path) = self.current_directory() {
3630 self.load_directory_content(path.to_path_buf().as_path());
3631 }
3632 }
3633
3634 fn load_previous_directory(&mut self) {
3638 if self.directory_offset + 1 >= self.directory_stack.len() {
3639 return;
3641 }
3642
3643 self.directory_offset += 1;
3644
3645 if let Some(path) = self.current_directory() {
3647 self.load_directory_content(path.to_path_buf().as_path());
3648 }
3649 }
3650
3651 fn load_parent_directory(&mut self) {
3655 if let Some(x) = self.current_directory() {
3656 if let Some(x) = x.to_path_buf().parent() {
3657 self.load_directory(x);
3658 }
3659 }
3660 }
3661
3662 fn reload_directory(&mut self) {
3669 if let Some(x) = self.current_directory() {
3670 self.load_directory_content(x.to_path_buf().as_path());
3671 }
3672 }
3673
3674 fn load_directory(&mut self, path: &Path) {
3680 if let Some(x) = self.current_directory() {
3683 if x == path {
3684 return;
3685 }
3686 }
3687
3688 if self.directory_offset != 0 && self.directory_stack.len() > self.directory_offset {
3689 self.directory_stack
3690 .drain(self.directory_stack.len() - self.directory_offset..);
3691 }
3692
3693 self.directory_stack.push(path.to_path_buf());
3694 self.directory_offset = 0;
3695
3696 self.load_directory_content(path);
3697
3698 self.search_value.clear();
3701 }
3702
3703 fn load_directory_content(&mut self, path: &Path) {
3705 self.storage.last_visited_dir = Some(path.to_path_buf());
3706
3707 let selected_file_filter = match self.mode {
3708 DialogMode::PickFile | DialogMode::PickMultiple => self.get_selected_file_filter(),
3709 _ => None,
3710 };
3711
3712 let selected_save_extension = if self.mode == DialogMode::SaveFile {
3713 self.get_selected_save_extension()
3714 .map(|e| e.file_extension.as_str())
3715 } else {
3716 None
3717 };
3718
3719 let filter = DirectoryFilter {
3720 show_files: self.show_files,
3721 show_hidden: self.storage.show_hidden,
3722 show_system_files: self.storage.show_system_files,
3723 file_filter: selected_file_filter.cloned(),
3724 filter_extension: selected_save_extension.map(str::to_string),
3725 };
3726
3727 self.directory_content = DirectoryContent::from_path(
3728 &self.config,
3729 path,
3730 self.config.file_system.clone(),
3731 filter,
3732 );
3733
3734 self.create_directory_dialog.close();
3735 self.scroll_to_selection = true;
3736
3737 if self.mode == DialogMode::SaveFile {
3738 self.file_name_input_error = self.validate_file_name_input();
3739 }
3740 }
3741
3742 fn should_open_directory(&self, path: &std::path::Path) -> bool {
3746 self.config
3747 .open_directory_filter
3748 .as_ref()
3749 .is_none_or(|f| f.matches(path))
3750 }
3751}
3752
3753#[cfg(test)]
3755const fn test_prop<T: Send + Sync>() {}
3756
3757#[test]
3758const fn test() {
3759 test_prop::<FileDialog>();
3760}
3761
3762#[cfg(test)]
3763mod open_directory_filter_tests {
3764 use std::path::Path;
3765
3766 use super::*;
3767
3768 #[test]
3769 fn filter_is_none_by_default() {
3770 let dialog = FileDialog::new();
3771 assert!(dialog.config.open_directory_filter.is_none());
3772 }
3773
3774 #[test]
3775 fn set_open_directory_filter_stores_filter() {
3776 let mut dialog = FileDialog::new();
3777 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3778 assert!(dialog.config.open_directory_filter.is_some());
3779 }
3780
3781 #[test]
3782 fn clear_open_directory_filter_removes_filter() {
3783 let mut dialog = FileDialog::new();
3784 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3785 assert!(dialog.config.open_directory_filter.is_some());
3786 dialog.clear_open_directory_filter();
3787 assert!(dialog.config.open_directory_filter.is_none());
3788 }
3789
3790 #[test]
3793 fn no_filter_always_navigates() {
3794 let dialog = FileDialog::new();
3795 assert!(dialog.should_open_directory(Path::new("/any/dir")));
3796 }
3797
3798 #[test]
3800 fn filter_returning_false_prevents_navigation() {
3801 let mut dialog = FileDialog::new();
3802 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3803 assert!(!dialog.should_open_directory(Path::new("/any/dir")));
3804 }
3805
3806 #[test]
3808 fn filter_returning_true_allows_navigation() {
3809 let mut dialog = FileDialog::new();
3810 dialog.set_open_directory_filter(Filter::new(|_: &Path| true));
3811 assert!(dialog.should_open_directory(Path::new("/any/dir")));
3812 }
3813
3814 #[test]
3816 fn cleared_filter_restores_default_navigation() {
3817 let mut dialog = FileDialog::new();
3818 dialog.set_open_directory_filter(Filter::new(|_: &Path| false));
3819 assert!(!dialog.should_open_directory(Path::new("/any/dir")));
3820 dialog.clear_open_directory_filter();
3821 assert!(dialog.should_open_directory(Path::new("/any/dir")));
3822 }
3823
3824 #[test]
3828 fn filter_based_on_sentinel_file() -> Result<(), Box<dyn std::error::Error>> {
3829 use tempdir::TempDir;
3830 let tmp = TempDir::new("egui_fd_test")?;
3831 let project_dir = tmp.path().join("project");
3832 std::fs::create_dir_all(&project_dir)?;
3833 let sentinel = project_dir.join("project.json");
3834 std::fs::write(&sentinel, b"{}")?;
3835
3836 let regular_dir = tmp.path().join("regular");
3837 std::fs::create_dir_all(®ular_dir)?;
3838
3839 let mut dialog = FileDialog::new();
3840 dialog.set_open_directory_filter(Filter::new(|path: &Path| {
3842 !path.join("project.json").exists()
3843 }));
3844
3845 assert!(!dialog.should_open_directory(&project_dir));
3847 assert!(dialog.should_open_directory(®ular_dir));
3849 Ok(())
3851 }
3852}