1use crate::config::{
2 FileDialogConfig, FileDialogKeyBindings, FileDialogLabels, FileFilter, Filter, OpeningMode,
3 PinnedFolder, QuickAccess, SaveExtension,
4};
5use crate::create_directory_dialog::CreateDirectoryDialog;
6use crate::data::{
7 DirectoryContent, DirectoryContentState, DirectoryEntry, DirectoryFilter, Disk, Disks,
8 UserDirectories,
9};
10use crate::modals::{FileDialogModal, ModalAction, ModalState, OverwriteFileModal};
11use crate::{FileSystem, NativeFileSystem};
12use egui::text::{CCursor, CCursorRange};
13use std::fmt::Debug;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16
17#[derive(Debug, PartialEq, Eq, Clone, Copy)]
19pub enum DialogMode {
20 PickFile,
22
23 PickDirectory,
25
26 PickMultiple,
28
29 SaveFile,
31}
32
33#[derive(Debug, PartialEq, Eq, Clone)]
35pub enum DialogState {
36 Open,
38
39 Closed,
41
42 Picked(PathBuf),
44
45 PickedMultiple(Vec<PathBuf>),
47
48 Cancelled,
50}
51
52#[derive(Debug, Clone)]
54#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
55pub struct FileDialogStorage {
56 pub pinned_folders: Vec<PinnedFolder>,
58 pub show_hidden: bool,
60 pub show_system_files: bool,
62 pub last_visited_dir: Option<PathBuf>,
64 pub last_picked_dir: Option<PathBuf>,
66}
67
68impl Default for FileDialogStorage {
69 fn default() -> Self {
71 Self {
72 pinned_folders: Vec::new(),
73 show_hidden: false,
74 show_system_files: false,
75 last_visited_dir: None,
76 last_picked_dir: None,
77 }
78 }
79}
80
81#[derive(Debug)]
107pub struct FileDialog {
108 config: FileDialogConfig,
110 storage: FileDialogStorage,
112
113 modals: Vec<Box<dyn FileDialogModal + Send + Sync>>,
116
117 mode: DialogMode,
119 state: DialogState,
121 show_files: bool,
124 operation_id: Option<String>,
130
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
201#[cfg(test)]
203const fn test_prop<T: Send + Sync>() {}
204
205#[test]
206const fn test() {
207 test_prop::<FileDialog>();
208}
209
210impl Default for FileDialog {
211 fn default() -> Self {
213 Self::new()
214 }
215}
216
217impl Debug for dyn FileDialogModal + Send + Sync {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 write!(f, "<FileDialogModal>")
220 }
221}
222
223type FileDialogUiCallback<'a> = dyn FnMut(&mut egui::Ui, &mut FileDialog) + 'a;
228
229impl FileDialog {
230 #[must_use]
235 pub fn new() -> Self {
236 let file_system = Arc::new(NativeFileSystem);
237
238 Self {
239 config: FileDialogConfig::default_from_filesystem(file_system.clone()),
240 storage: FileDialogStorage::default(),
241
242 modals: Vec::new(),
243
244 mode: DialogMode::PickDirectory,
245 state: DialogState::Closed,
246 show_files: true,
247 operation_id: None,
248
249 window_id: egui::Id::new("file_dialog"),
250
251 user_directories: None,
252 system_disks: Disks::new_empty(),
253
254 directory_stack: Vec::new(),
255 directory_offset: 0,
256 directory_content: DirectoryContent::default(),
257
258 create_directory_dialog: CreateDirectoryDialog::from_filesystem(file_system),
259
260 path_edit_visible: false,
261 path_edit_value: String::new(),
262 path_edit_activate: false,
263 path_edit_request_focus: false,
264
265 selected_item: None,
266 file_name_input: String::new(),
267 file_name_input_error: None,
268 file_name_input_request_focus: true,
269 selected_file_filter: None,
270 selected_save_extension: None,
271
272 scroll_to_selection: false,
273 search_value: String::new(),
274 init_search: false,
275
276 any_focused_last_frame: false,
277
278 rename_pinned_folder: None,
279 rename_pinned_folder_request_focus: false,
280 }
281 }
282
283 pub fn with_config(config: FileDialogConfig) -> Self {
285 let mut obj = Self::new();
286 *obj.config_mut() = config;
287 obj
288 }
289
290 #[must_use]
292 pub fn with_file_system(file_system: Arc<dyn FileSystem + Send + Sync>) -> Self {
293 let mut obj = Self::new();
294 obj.config.initial_directory = file_system.current_dir().unwrap_or_default();
295 obj.config.file_system = file_system;
296 obj
297 }
298
299 #[deprecated(
361 since = "0.10.0",
362 note = "Use `pick_file` / `pick_directory` / `pick_multiple` in combination with \
363 `set_operation_id` instead"
364 )]
365 pub fn open(&mut self, mode: DialogMode, mut show_files: bool, operation_id: Option<&str>) {
366 self.reset();
367 self.refresh();
368
369 if mode == DialogMode::PickFile {
370 show_files = true;
371 }
372
373 if mode == DialogMode::SaveFile {
374 self.file_name_input_request_focus = true;
375 self.file_name_input
376 .clone_from(&self.config.default_file_name);
377 }
378
379 self.selected_file_filter = None;
380 self.selected_save_extension = None;
381
382 self.set_default_file_filter();
383 self.set_default_save_extension();
384
385 self.mode = mode;
386 self.state = DialogState::Open;
387 self.show_files = show_files;
388 self.operation_id = operation_id.map(String::from);
389
390 self.window_id = self
391 .config
392 .id
393 .map_or_else(|| egui::Id::new(self.get_window_title()), |id| id);
394
395 self.load_directory(&self.get_initial_directory());
396 }
397
398 pub fn pick_directory(&mut self) {
406 #[allow(deprecated)]
408 self.open(DialogMode::PickDirectory, false, None);
409 }
410
411 pub fn pick_file(&mut self) {
417 #[allow(deprecated)]
419 self.open(DialogMode::PickFile, true, None);
420 }
421
422 pub fn pick_multiple(&mut self) {
429 #[allow(deprecated)]
431 self.open(DialogMode::PickMultiple, true, None);
432 }
433
434 pub fn save_file(&mut self) {
440 #[allow(deprecated)]
442 self.open(DialogMode::SaveFile, true, None);
443 }
444
445 pub fn update(&mut self, ctx: &egui::Context) -> &Self {
449 if self.state != DialogState::Open {
450 return self;
451 }
452
453 self.update_keybindings(ctx);
454 self.update_ui(ctx, None);
455
456 self
457 }
458
459 pub fn set_right_panel_width(&mut self, width: f32) {
461 self.config.right_panel_width = Some(width);
462 }
463
464 pub fn clear_right_panel_width(&mut self) {
466 self.config.right_panel_width = None;
467 }
468
469 pub fn update_with_right_panel_ui(
481 &mut self,
482 ctx: &egui::Context,
483 f: &mut FileDialogUiCallback,
484 ) -> &Self {
485 if self.state != DialogState::Open {
486 return self;
487 }
488
489 self.update_keybindings(ctx);
490 self.update_ui(ctx, Some(f));
491
492 self
493 }
494
495 pub fn config_mut(&mut self) -> &mut FileDialogConfig {
500 &mut self.config
501 }
502
503 pub fn storage(mut self, storage: FileDialogStorage) -> Self {
507 self.storage = storage;
508 self
509 }
510
511 pub fn storage_mut(&mut self) -> &mut FileDialogStorage {
513 &mut self.storage
514 }
515
516 pub fn keybindings(mut self, keybindings: FileDialogKeyBindings) -> Self {
518 self.config.keybindings = keybindings;
519 self
520 }
521
522 pub fn labels(mut self, labels: FileDialogLabels) -> Self {
528 self.config.labels = labels;
529 self
530 }
531
532 pub fn labels_mut(&mut self) -> &mut FileDialogLabels {
534 &mut self.config.labels
535 }
536
537 pub const fn opening_mode(mut self, opening_mode: OpeningMode) -> Self {
539 self.config.opening_mode = opening_mode;
540 self
541 }
542
543 pub const fn as_modal(mut self, as_modal: bool) -> Self {
548 self.config.as_modal = as_modal;
549 self
550 }
551
552 pub const fn modal_overlay_color(mut self, modal_overlay_color: egui::Color32) -> Self {
554 self.config.modal_overlay_color = modal_overlay_color;
555 self
556 }
557
558 pub fn initial_directory(mut self, directory: PathBuf) -> Self {
567 self.config.initial_directory = directory;
568 self
569 }
570
571 pub fn default_file_name(mut self, name: &str) -> Self {
573 name.clone_into(&mut self.config.default_file_name);
574 self
575 }
576
577 pub const fn allow_file_overwrite(mut self, allow_file_overwrite: bool) -> Self {
583 self.config.allow_file_overwrite = allow_file_overwrite;
584 self
585 }
586
587 pub const fn allow_path_edit_to_save_file_without_extension(mut self, allow: bool) -> Self {
596 self.config.allow_path_edit_to_save_file_without_extension = allow;
597 self
598 }
599
600 pub fn directory_separator(mut self, separator: &str) -> Self {
603 self.config.directory_separator = separator.to_string();
604 self
605 }
606
607 pub const fn canonicalize_paths(mut self, canonicalize: bool) -> Self {
624 self.config.canonicalize_paths = canonicalize;
625 self
626 }
627
628 pub const fn load_via_thread(mut self, load_via_thread: bool) -> Self {
632 self.config.load_via_thread = load_via_thread;
633 self
634 }
635
636 pub const fn truncate_filenames(mut self, truncate_filenames: bool) -> Self {
642 self.config.truncate_filenames = truncate_filenames;
643 self
644 }
645
646 pub fn err_icon(mut self, icon: &str) -> Self {
648 self.config.err_icon = icon.to_string();
649 self
650 }
651
652 pub fn default_file_icon(mut self, icon: &str) -> Self {
654 self.config.default_file_icon = icon.to_string();
655 self
656 }
657
658 pub fn default_folder_icon(mut self, icon: &str) -> Self {
660 self.config.default_folder_icon = icon.to_string();
661 self
662 }
663
664 pub fn device_icon(mut self, icon: &str) -> Self {
666 self.config.device_icon = icon.to_string();
667 self
668 }
669
670 pub fn removable_device_icon(mut self, icon: &str) -> Self {
672 self.config.removable_device_icon = icon.to_string();
673 self
674 }
675
676 pub fn add_file_filter(mut self, name: &str, filter: Filter<Path>) -> Self {
702 self.config = self.config.add_file_filter(name, filter);
703 self
704 }
705
706 pub fn add_file_filter_extensions(mut self, name: &str, extensions: Vec<&'static str>) -> Self {
722 self.config = self.config.add_file_filter_extensions(name, extensions);
723 self
724 }
725
726 pub fn default_file_filter(mut self, name: &str) -> Self {
730 self.config.default_file_filter = Some(name.to_string());
731 self
732 }
733
734 pub fn add_save_extension(mut self, name: &str, file_extension: &str) -> Self {
756 self.config = self.config.add_save_extension(name, file_extension);
757 self
758 }
759
760 pub fn default_save_extension(mut self, name: &str) -> Self {
764 self.config.default_save_extension = Some(name.to_string());
765 self
766 }
767
768 pub fn set_file_icon(mut self, icon: &str, filter: Filter<std::path::Path>) -> Self {
789 self.config = self.config.set_file_icon(icon, filter);
790 self
791 }
792
793 pub fn add_quick_access(
809 mut self,
810 heading: &str,
811 builder: impl FnOnce(&mut QuickAccess),
812 ) -> Self {
813 self.config = self.config.add_quick_access(heading, builder);
814 self
815 }
816
817 pub fn title(mut self, title: &str) -> Self {
822 self.config.title = Some(title.to_string());
823 self
824 }
825
826 pub fn id(mut self, id: impl Into<egui::Id>) -> Self {
828 self.config.id = Some(id.into());
829 self
830 }
831
832 pub fn default_pos(mut self, default_pos: impl Into<egui::Pos2>) -> Self {
834 self.config.default_pos = Some(default_pos.into());
835 self
836 }
837
838 pub fn fixed_pos(mut self, pos: impl Into<egui::Pos2>) -> Self {
840 self.config.fixed_pos = Some(pos.into());
841 self
842 }
843
844 pub fn default_size(mut self, size: impl Into<egui::Vec2>) -> Self {
846 self.config.default_size = size.into();
847 self
848 }
849
850 pub fn max_size(mut self, max_size: impl Into<egui::Vec2>) -> Self {
852 self.config.max_size = Some(max_size.into());
853 self
854 }
855
856 pub fn min_size(mut self, min_size: impl Into<egui::Vec2>) -> Self {
860 self.config.min_size = min_size.into();
861 self
862 }
863
864 pub fn anchor(mut self, align: egui::Align2, offset: impl Into<egui::Vec2>) -> Self {
866 self.config.anchor = Some((align, offset.into()));
867 self
868 }
869
870 pub const fn resizable(mut self, resizable: bool) -> Self {
872 self.config.resizable = resizable;
873 self
874 }
875
876 pub const fn movable(mut self, movable: bool) -> Self {
880 self.config.movable = movable;
881 self
882 }
883
884 pub const fn title_bar(mut self, title_bar: bool) -> Self {
886 self.config.title_bar = title_bar;
887 self
888 }
889
890 pub const fn show_top_panel(mut self, show_top_panel: bool) -> Self {
893 self.config.show_top_panel = show_top_panel;
894 self
895 }
896
897 pub const fn show_parent_button(mut self, show_parent_button: bool) -> Self {
901 self.config.show_parent_button = show_parent_button;
902 self
903 }
904
905 pub const fn show_back_button(mut self, show_back_button: bool) -> Self {
909 self.config.show_back_button = show_back_button;
910 self
911 }
912
913 pub const fn show_forward_button(mut self, show_forward_button: bool) -> Self {
917 self.config.show_forward_button = show_forward_button;
918 self
919 }
920
921 pub const fn show_new_folder_button(mut self, show_new_folder_button: bool) -> Self {
925 self.config.show_new_folder_button = show_new_folder_button;
926 self
927 }
928
929 pub const fn show_current_path(mut self, show_current_path: bool) -> Self {
933 self.config.show_current_path = show_current_path;
934 self
935 }
936
937 pub const fn show_path_edit_button(mut self, show_path_edit_button: bool) -> Self {
941 self.config.show_path_edit_button = show_path_edit_button;
942 self
943 }
944
945 pub const fn show_menu_button(mut self, show_menu_button: bool) -> Self {
950 self.config.show_menu_button = show_menu_button;
951 self
952 }
953
954 pub const fn show_reload_button(mut self, show_reload_button: bool) -> Self {
959 self.config.show_reload_button = show_reload_button;
960 self
961 }
962
963 pub const fn show_working_directory_button(
970 mut self,
971 show_working_directory_button: bool,
972 ) -> Self {
973 self.config.show_working_directory_button = show_working_directory_button;
974 self
975 }
976
977 pub const fn show_hidden_option(mut self, show_hidden_option: bool) -> Self {
983 self.config.show_hidden_option = show_hidden_option;
984 self
985 }
986
987 pub const fn show_system_files_option(mut self, show_system_files_option: bool) -> Self {
993 self.config.show_system_files_option = show_system_files_option;
994 self
995 }
996
997 pub const fn show_search(mut self, show_search: bool) -> Self {
1001 self.config.show_search = show_search;
1002 self
1003 }
1004
1005 pub const fn show_left_panel(mut self, show_left_panel: bool) -> Self {
1008 self.config.show_left_panel = show_left_panel;
1009 self
1010 }
1011
1012 pub const fn show_pinned_folders(mut self, show_pinned_folders: bool) -> Self {
1015 self.config.show_pinned_folders = show_pinned_folders;
1016 self
1017 }
1018
1019 pub const fn show_places(mut self, show_places: bool) -> Self {
1024 self.config.show_places = show_places;
1025 self
1026 }
1027
1028 pub const fn show_devices(mut self, show_devices: bool) -> Self {
1033 self.config.show_devices = show_devices;
1034 self
1035 }
1036
1037 pub const fn show_removable_devices(mut self, show_removable_devices: bool) -> Self {
1042 self.config.show_removable_devices = show_removable_devices;
1043 self
1044 }
1045
1046 pub fn picked(&self) -> Option<&Path> {
1054 match &self.state {
1055 DialogState::Picked(path) => Some(path),
1056 _ => None,
1057 }
1058 }
1059
1060 pub fn take_picked(&mut self) -> Option<PathBuf> {
1067 match &mut self.state {
1068 DialogState::Picked(path) => {
1069 let path = std::mem::take(path);
1070 self.state = DialogState::Closed;
1071 Some(path)
1072 }
1073 _ => None,
1074 }
1075 }
1076
1077 pub fn picked_multiple(&self) -> Option<Vec<&Path>> {
1082 match &self.state {
1083 DialogState::PickedMultiple(items) => {
1084 Some(items.iter().map(std::path::PathBuf::as_path).collect())
1085 }
1086 _ => None,
1087 }
1088 }
1089
1090 pub fn take_picked_multiple(&mut self) -> Option<Vec<PathBuf>> {
1097 match &mut self.state {
1098 DialogState::PickedMultiple(items) => {
1099 let items = std::mem::take(items);
1100 self.state = DialogState::Closed;
1101 Some(items)
1102 }
1103 _ => None,
1104 }
1105 }
1106
1107 pub const fn selected_entry(&self) -> Option<&DirectoryEntry> {
1115 self.selected_item.as_ref()
1116 }
1117
1118 pub fn selected_entries(&self) -> impl Iterator<Item = &DirectoryEntry> {
1124 self.get_dir_content_filtered_iter().filter(|p| p.selected)
1125 }
1126
1127 pub fn operation_id(&self) -> Option<&str> {
1131 self.operation_id.as_deref()
1132 }
1133
1134 pub fn set_operation_id(&mut self, operation_id: &str) {
1138 self.operation_id = Some(operation_id.to_owned());
1139 }
1140
1141 pub const fn mode(&self) -> DialogMode {
1143 self.mode
1144 }
1145
1146 pub fn state(&self) -> DialogState {
1148 self.state.clone()
1149 }
1150
1151 pub const fn get_window_id(&self) -> egui::Id {
1153 self.window_id
1154 }
1155}
1156
1157impl FileDialog {
1159 fn update_ui(
1163 &mut self,
1164 ctx: &egui::Context,
1165 right_panel_fn: Option<&mut FileDialogUiCallback>,
1166 ) {
1167 let mut is_open = true;
1168
1169 if self.config.as_modal {
1170 let re = self.ui_update_modal_background(ctx);
1171 ctx.move_to_top(re.response.layer_id);
1172 }
1173
1174 let re = self.create_window(&mut is_open).show(ctx, |ui| {
1175 if !self.modals.is_empty() {
1176 self.ui_update_modals(ui);
1177 return;
1178 }
1179
1180 if self.config.show_top_panel {
1181 egui::TopBottomPanel::top(self.window_id.with("top_panel"))
1182 .resizable(false)
1183 .show_inside(ui, |ui| {
1184 self.ui_update_top_panel(ui);
1185 });
1186 }
1187
1188 if self.config.show_left_panel {
1189 egui::SidePanel::left(self.window_id.with("left_panel"))
1190 .resizable(true)
1191 .default_width(150.0)
1192 .width_range(90.0..=250.0)
1193 .show_inside(ui, |ui| {
1194 self.ui_update_left_panel(ui);
1195 });
1196 }
1197
1198 if let Some(f) = right_panel_fn {
1200 let mut right_panel = egui::SidePanel::right(self.window_id.with("right_panel"))
1201 .resizable(true);
1204 if let Some(width) = self.config.right_panel_width {
1205 right_panel = right_panel.default_width(width);
1206 }
1207 right_panel.show_inside(ui, |ui| {
1208 f(ui, self);
1209 });
1210 }
1211
1212 egui::TopBottomPanel::bottom(self.window_id.with("bottom_panel"))
1213 .resizable(false)
1214 .show_inside(ui, |ui| {
1215 self.ui_update_bottom_panel(ui);
1216 });
1217
1218 egui::CentralPanel::default().show_inside(ui, |ui| {
1219 self.ui_update_central_panel(ui);
1220 });
1221 });
1222
1223 if self.config.as_modal {
1224 if let Some(inner_response) = re {
1225 ctx.move_to_top(inner_response.response.layer_id);
1226 }
1227 }
1228
1229 self.any_focused_last_frame = ctx.memory(egui::Memory::focused).is_some();
1230
1231 if !is_open {
1233 self.cancel();
1234 }
1235
1236 let mut repaint = false;
1237
1238 ctx.input(|i| {
1240 if let Some(dropped_file) = i.raw.dropped_files.last() {
1242 if let Some(path) = &dropped_file.path {
1243 if self.config.file_system.is_dir(path) {
1244 self.load_directory(path.as_path());
1246 repaint = true;
1247 } else if let Some(parent) = path.parent() {
1248 self.load_directory(parent);
1250 self.select_item(&mut DirectoryEntry::from_path(
1251 &self.config,
1252 path,
1253 &*self.config.file_system,
1254 ));
1255 self.scroll_to_selection = true;
1256 repaint = true;
1257 }
1258 }
1259 }
1260 });
1261
1262 if repaint {
1264 ctx.request_repaint();
1265 }
1266 }
1267
1268 fn ui_update_modal_background(&self, ctx: &egui::Context) -> egui::InnerResponse<()> {
1270 egui::Area::new(self.window_id.with("modal_overlay"))
1271 .interactable(true)
1272 .fixed_pos(egui::Pos2::ZERO)
1273 .show(ctx, |ui| {
1274 let screen_rect = ctx.input(|i| i.screen_rect);
1275
1276 ui.allocate_response(screen_rect.size(), egui::Sense::click());
1277
1278 ui.painter().rect_filled(
1279 screen_rect,
1280 egui::CornerRadius::ZERO,
1281 self.config.modal_overlay_color,
1282 );
1283 })
1284 }
1285
1286 fn ui_update_modals(&mut self, ui: &mut egui::Ui) {
1287 egui::TopBottomPanel::bottom(self.window_id.with("modal_bottom_panel"))
1292 .resizable(false)
1293 .show_separator_line(false)
1294 .show_inside(ui, |_| {});
1295
1296 egui::CentralPanel::default().show_inside(ui, |ui| {
1299 if let Some(modal) = self.modals.last_mut() {
1300 #[allow(clippy::single_match)]
1301 match modal.update(&self.config, ui) {
1302 ModalState::Close(action) => {
1303 self.exec_modal_action(action);
1304 self.modals.pop();
1305 }
1306 ModalState::Pending => {}
1307 }
1308 }
1309 });
1310 }
1311
1312 fn create_window<'a>(&self, is_open: &'a mut bool) -> egui::Window<'a> {
1314 let mut window = egui::Window::new(self.get_window_title())
1315 .id(self.window_id)
1316 .open(is_open)
1317 .default_size(self.config.default_size)
1318 .min_size(self.config.min_size)
1319 .resizable(self.config.resizable)
1320 .movable(self.config.movable)
1321 .title_bar(self.config.title_bar)
1322 .collapsible(false);
1323
1324 if let Some(pos) = self.config.default_pos {
1325 window = window.default_pos(pos);
1326 }
1327
1328 if let Some(pos) = self.config.fixed_pos {
1329 window = window.fixed_pos(pos);
1330 }
1331
1332 if let Some((anchor, offset)) = self.config.anchor {
1333 window = window.anchor(anchor, offset);
1334 }
1335
1336 if let Some(size) = self.config.max_size {
1337 window = window.max_size(size);
1338 }
1339
1340 window
1341 }
1342
1343 const fn get_window_title(&self) -> &String {
1346 match &self.config.title {
1347 Some(title) => title,
1348 None => match &self.mode {
1349 DialogMode::PickDirectory => &self.config.labels.title_select_directory,
1350 DialogMode::PickFile => &self.config.labels.title_select_file,
1351 DialogMode::PickMultiple => &self.config.labels.title_select_multiple,
1352 DialogMode::SaveFile => &self.config.labels.title_save_file,
1353 },
1354 }
1355 }
1356
1357 fn ui_update_top_panel(&mut self, ui: &mut egui::Ui) {
1360 const BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(25.0, 25.0);
1361
1362 ui.horizontal(|ui| {
1363 self.ui_update_nav_buttons(ui, BUTTON_SIZE);
1364
1365 let mut path_display_width = ui.available_width();
1366
1367 if self.config.show_reload_button {
1369 path_display_width -= ui
1370 .style()
1371 .spacing
1372 .item_spacing
1373 .x
1374 .mul_add(2.5, BUTTON_SIZE.x);
1375 }
1376
1377 if self.config.show_search {
1378 path_display_width -= 140.0;
1379 }
1380
1381 if self.config.show_current_path {
1382 self.ui_update_current_path(ui, path_display_width);
1383 }
1384
1385 if self.config.show_menu_button
1387 && (self.config.show_reload_button
1388 || self.config.show_working_directory_button
1389 || self.config.show_hidden_option
1390 || self.config.show_system_files_option)
1391 {
1392 ui.allocate_ui_with_layout(
1393 BUTTON_SIZE,
1394 egui::Layout::centered_and_justified(egui::Direction::LeftToRight),
1395 |ui| {
1396 ui.menu_button("☰", |ui| {
1397 self.ui_update_hamburger_menu(ui);
1398 });
1399 },
1400 );
1401 }
1402
1403 if self.config.show_search {
1404 self.ui_update_search(ui);
1405 }
1406 });
1407
1408 ui.add_space(ui.ctx().style().spacing.item_spacing.y);
1409 }
1410
1411 fn ui_update_nav_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
1413 if self.config.show_parent_button {
1414 if let Some(x) = self.current_directory() {
1415 if self.ui_button_sized(ui, x.parent().is_some(), button_size, "⏶", None) {
1416 self.load_parent_directory();
1417 }
1418 } else {
1419 let _ = self.ui_button_sized(ui, false, button_size, "⏶", None);
1420 }
1421 }
1422
1423 if self.config.show_back_button
1424 && self.ui_button_sized(
1425 ui,
1426 self.directory_offset + 1 < self.directory_stack.len(),
1427 button_size,
1428 "⏴",
1429 None,
1430 )
1431 {
1432 self.load_previous_directory();
1433 }
1434
1435 if self.config.show_forward_button
1436 && self.ui_button_sized(ui, self.directory_offset != 0, button_size, "⏵", None)
1437 {
1438 self.load_next_directory();
1439 }
1440
1441 if self.config.show_new_folder_button
1442 && self.ui_button_sized(
1443 ui,
1444 !self.create_directory_dialog.is_open(),
1445 button_size,
1446 "+",
1447 None,
1448 )
1449 {
1450 self.open_new_folder_dialog();
1451 }
1452 }
1453
1454 fn ui_update_current_path(&mut self, ui: &mut egui::Ui, width: f32) {
1458 egui::Frame::default()
1459 .stroke(egui::Stroke::new(
1460 1.0,
1461 ui.ctx().style().visuals.window_stroke.color,
1462 ))
1463 .inner_margin(egui::Margin::from(4))
1464 .corner_radius(egui::CornerRadius::from(4))
1465 .show(ui, |ui| {
1466 const EDIT_BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(22.0, 20.0);
1467
1468 if self.path_edit_visible {
1469 self.ui_update_path_edit(ui, width, EDIT_BUTTON_SIZE);
1470 } else {
1471 self.ui_update_path_display(ui, width, EDIT_BUTTON_SIZE);
1472 }
1473 });
1474 }
1475
1476 fn ui_update_path_display(
1478 &mut self,
1479 ui: &mut egui::Ui,
1480 width: f32,
1481 edit_button_size: egui::Vec2,
1482 ) {
1483 ui.style_mut().always_scroll_the_only_direction = true;
1484 ui.style_mut().spacing.scroll.bar_width = 8.0;
1485
1486 let max_width = if self.config.show_path_edit_button {
1487 ui.style()
1488 .spacing
1489 .item_spacing
1490 .x
1491 .mul_add(-2.0, width - edit_button_size.x)
1492 } else {
1493 width
1494 };
1495
1496 egui::ScrollArea::horizontal()
1497 .auto_shrink([false, false])
1498 .stick_to_right(true)
1499 .max_width(max_width)
1500 .show(ui, |ui| {
1501 ui.horizontal(|ui| {
1502 ui.style_mut().spacing.item_spacing.x /= 2.5;
1503 ui.style_mut().spacing.button_padding = egui::Vec2::new(5.0, 3.0);
1504
1505 let mut path = PathBuf::new();
1506
1507 if let Some(data) = self.current_directory().map(Path::to_path_buf) {
1508 for (i, segment) in data.iter().enumerate() {
1509 path.push(segment);
1510
1511 let mut segment_str = segment.to_str().unwrap_or_default().to_string();
1512
1513 if self.is_pinned(&path) {
1514 segment_str =
1515 format!("{} {}", &self.config.pinned_icon, segment_str);
1516 }
1517
1518 if i != 0 {
1519 ui.label(self.config.directory_separator.as_str());
1520 }
1521
1522 let re = ui.button(segment_str);
1523
1524 if re.clicked() {
1525 self.load_directory(path.as_path());
1526 return;
1527 }
1528
1529 self.ui_update_central_panel_path_context_menu(&re, &path.clone());
1530 }
1531 }
1532 });
1533 });
1534
1535 if !self.config.show_path_edit_button {
1536 return;
1537 }
1538
1539 if ui
1540 .add_sized(
1541 edit_button_size,
1542 egui::Button::new("🖊").fill(egui::Color32::TRANSPARENT),
1543 )
1544 .clicked()
1545 {
1546 self.open_path_edit();
1547 }
1548 }
1549
1550 fn ui_update_path_edit(&mut self, ui: &mut egui::Ui, width: f32, edit_button_size: egui::Vec2) {
1552 let desired_width: f32 = ui
1553 .style()
1554 .spacing
1555 .item_spacing
1556 .x
1557 .mul_add(-3.0, width - edit_button_size.x);
1558
1559 let response = egui::TextEdit::singleline(&mut self.path_edit_value)
1560 .desired_width(desired_width)
1561 .show(ui)
1562 .response;
1563
1564 if self.path_edit_activate {
1565 response.request_focus();
1566 Self::set_cursor_to_end(&response, &self.path_edit_value);
1567 self.path_edit_activate = false;
1568 }
1569
1570 if self.path_edit_request_focus {
1571 response.request_focus();
1572 self.path_edit_request_focus = false;
1573 }
1574
1575 let btn_response = ui.add_sized(edit_button_size, egui::Button::new("✔"));
1576
1577 if btn_response.clicked() {
1578 self.submit_path_edit();
1579 }
1580
1581 if !response.has_focus() && !btn_response.contains_pointer() {
1582 self.path_edit_visible = false;
1583 }
1584 }
1585
1586 fn ui_update_hamburger_menu(&mut self, ui: &mut egui::Ui) {
1588 const SEPARATOR_SPACING: f32 = 2.0;
1589
1590 if self.config.show_reload_button && ui.button(&self.config.labels.reload).clicked() {
1591 self.refresh();
1592 ui.close_menu();
1593 }
1594
1595 let working_dir = self.config.file_system.current_dir();
1596
1597 if self.config.show_working_directory_button
1598 && working_dir.is_ok()
1599 && ui.button(&self.config.labels.working_directory).clicked()
1600 {
1601 self.load_directory(&working_dir.unwrap_or_default());
1602 ui.close_menu();
1603 }
1604
1605 if (self.config.show_reload_button || self.config.show_working_directory_button)
1606 && (self.config.show_hidden_option || self.config.show_system_files_option)
1607 {
1608 ui.add_space(SEPARATOR_SPACING);
1609 ui.separator();
1610 ui.add_space(SEPARATOR_SPACING);
1611 }
1612
1613 if self.config.show_hidden_option
1614 && ui
1615 .checkbox(
1616 &mut self.storage.show_hidden,
1617 &self.config.labels.show_hidden,
1618 )
1619 .clicked()
1620 {
1621 self.refresh();
1622 ui.close_menu();
1623 }
1624
1625 if self.config.show_system_files_option
1626 && ui
1627 .checkbox(
1628 &mut self.storage.show_system_files,
1629 &self.config.labels.show_system_files,
1630 )
1631 .clicked()
1632 {
1633 self.refresh();
1634 ui.close_menu();
1635 }
1636 }
1637
1638 fn ui_update_search(&mut self, ui: &mut egui::Ui) {
1640 egui::Frame::default()
1641 .stroke(egui::Stroke::new(
1642 1.0,
1643 ui.ctx().style().visuals.window_stroke.color,
1644 ))
1645 .inner_margin(egui::Margin::symmetric(4, 4))
1646 .corner_radius(egui::CornerRadius::from(4))
1647 .show(ui, |ui| {
1648 ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1649 ui.add_space(ui.ctx().style().spacing.item_spacing.y);
1650
1651 ui.label(egui::RichText::from("🔍").size(15.0));
1652
1653 let re = ui.add_sized(
1654 egui::Vec2::new(ui.available_width(), 0.0),
1655 egui::TextEdit::singleline(&mut self.search_value),
1656 );
1657
1658 self.edit_search_on_text_input(ui);
1659
1660 if re.changed() || self.init_search {
1661 self.selected_item = None;
1662 self.select_first_visible_item();
1663 }
1664
1665 if self.init_search {
1666 re.request_focus();
1667 Self::set_cursor_to_end(&re, &self.search_value);
1668 self.directory_content.reset_multi_selection();
1669
1670 self.init_search = false;
1671 }
1672 });
1673 });
1674 }
1675
1676 fn edit_search_on_text_input(&mut self, ui: &egui::Ui) {
1683 if ui.memory(|mem| mem.focused().is_some()) {
1684 return;
1685 }
1686
1687 ui.input(|inp| {
1688 if inp.modifiers.any() && !inp.modifiers.shift_only() {
1690 return;
1691 }
1692
1693 for text in inp.events.iter().filter_map(|ev| match ev {
1696 egui::Event::Text(t) => Some(t),
1697 _ => None,
1698 }) {
1699 self.search_value.push_str(text);
1700 self.init_search = true;
1701 }
1702 });
1703 }
1704
1705 fn ui_update_left_panel(&mut self, ui: &mut egui::Ui) {
1708 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
1709 const SPACING_MULTIPLIER: f32 = 4.0;
1711
1712 egui::containers::ScrollArea::vertical()
1713 .auto_shrink([false, false])
1714 .show(ui, |ui| {
1715 let mut spacing = ui.ctx().style().spacing.item_spacing.y * 2.0;
1717
1718 if self.config.show_pinned_folders && self.ui_update_pinned_folders(ui, spacing)
1720 {
1721 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1722 }
1723
1724 let quick_accesses = std::mem::take(&mut self.config.quick_accesses);
1726
1727 for quick_access in &quick_accesses {
1728 ui.add_space(spacing);
1729 self.ui_update_quick_access(ui, quick_access);
1730 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1731 }
1732
1733 self.config.quick_accesses = quick_accesses;
1734
1735 if self.config.show_places && self.ui_update_user_directories(ui, spacing) {
1737 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1738 }
1739
1740 let disks = std::mem::take(&mut self.system_disks);
1741
1742 if self.config.show_devices && self.ui_update_devices(ui, spacing, &disks) {
1743 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1744 }
1745
1746 if self.config.show_removable_devices
1747 && self.ui_update_removable_devices(ui, spacing, &disks)
1748 {
1749 }
1752
1753 self.system_disks = disks;
1754 });
1755 });
1756 }
1757
1758 fn ui_update_left_panel_entry(
1762 &mut self,
1763 ui: &mut egui::Ui,
1764 display_name: &str,
1765 path: &Path,
1766 ) -> egui::Response {
1767 let response = ui.selectable_label(self.current_directory() == Some(path), display_name);
1768
1769 if response.clicked() {
1770 self.load_directory(path);
1771 }
1772
1773 response
1774 }
1775
1776 fn ui_update_quick_access(&mut self, ui: &mut egui::Ui, quick_access: &QuickAccess) {
1778 ui.label(&quick_access.heading);
1779
1780 for entry in &quick_access.paths {
1781 self.ui_update_left_panel_entry(ui, &entry.display_name, &entry.path);
1782 }
1783 }
1784
1785 fn ui_update_pinned_folders(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1790 let mut visible = false;
1791
1792 for (i, pinned) in self.storage.pinned_folders.clone().iter().enumerate() {
1793 if i == 0 {
1794 ui.add_space(spacing);
1795 ui.label(self.config.labels.heading_pinned.as_str());
1796
1797 visible = true;
1798 }
1799
1800 if self.is_pinned_folder_being_renamed(pinned) {
1801 self.ui_update_pinned_folder_rename(ui);
1802 continue;
1803 }
1804
1805 let response = self.ui_update_left_panel_entry(
1806 ui,
1807 &format!("{} {}", self.config.pinned_icon, &pinned.label),
1808 pinned.path.as_path(),
1809 );
1810
1811 self.ui_update_pinned_folder_context_menu(&response, pinned);
1812 }
1813
1814 visible
1815 }
1816
1817 fn ui_update_pinned_folder_rename(&mut self, ui: &mut egui::Ui) {
1818 if let Some(r) = &mut self.rename_pinned_folder {
1819 let id = self.window_id.with("pinned_folder_rename").with(&r.path);
1820 let mut output = egui::TextEdit::singleline(&mut r.label)
1821 .id(id)
1822 .cursor_at_end(true)
1823 .show(ui);
1824
1825 if self.rename_pinned_folder_request_focus {
1826 output.state.cursor.set_char_range(Some(CCursorRange::two(
1827 CCursor::new(0),
1828 CCursor::new(r.label.chars().count()),
1829 )));
1830 output.state.store(ui.ctx(), output.response.id);
1831
1832 output.response.request_focus();
1833
1834 self.rename_pinned_folder_request_focus = false;
1835 }
1836
1837 if output.response.lost_focus() {
1838 self.end_rename_pinned_folder();
1839 }
1840 }
1841 }
1842
1843 fn ui_update_pinned_folder_context_menu(
1844 &mut self,
1845 item: &egui::Response,
1846 pinned: &PinnedFolder,
1847 ) {
1848 item.context_menu(|ui| {
1849 if ui.button(&self.config.labels.unpin_folder).clicked() {
1850 self.unpin_path(&pinned.path);
1851 ui.close_menu();
1852 }
1853
1854 if ui
1855 .button(&self.config.labels.rename_pinned_folder)
1856 .clicked()
1857 {
1858 self.begin_rename_pinned_folder(pinned.clone());
1859 ui.close_menu();
1860 }
1861 });
1862 }
1863
1864 fn ui_update_user_directories(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1869 let user_directories = std::mem::take(&mut self.user_directories);
1873 let labels = std::mem::take(&mut self.config.labels);
1874
1875 let mut visible = false;
1876
1877 if let Some(dirs) = &user_directories {
1878 ui.add_space(spacing);
1879 ui.label(labels.heading_places.as_str());
1880
1881 if let Some(path) = dirs.home_dir() {
1882 self.ui_update_left_panel_entry(ui, &labels.home_dir, path);
1883 }
1884 if let Some(path) = dirs.desktop_dir() {
1885 self.ui_update_left_panel_entry(ui, &labels.desktop_dir, path);
1886 }
1887 if let Some(path) = dirs.document_dir() {
1888 self.ui_update_left_panel_entry(ui, &labels.documents_dir, path);
1889 }
1890 if let Some(path) = dirs.download_dir() {
1891 self.ui_update_left_panel_entry(ui, &labels.downloads_dir, path);
1892 }
1893 if let Some(path) = dirs.audio_dir() {
1894 self.ui_update_left_panel_entry(ui, &labels.audio_dir, path);
1895 }
1896 if let Some(path) = dirs.picture_dir() {
1897 self.ui_update_left_panel_entry(ui, &labels.pictures_dir, path);
1898 }
1899 if let Some(path) = dirs.video_dir() {
1900 self.ui_update_left_panel_entry(ui, &labels.videos_dir, path);
1901 }
1902
1903 visible = true;
1904 }
1905
1906 self.user_directories = user_directories;
1907 self.config.labels = labels;
1908
1909 visible
1910 }
1911
1912 fn ui_update_devices(&mut self, ui: &mut egui::Ui, spacing: f32, disks: &Disks) -> bool {
1917 let mut visible = false;
1918
1919 for (i, disk) in disks.iter().filter(|x| !x.is_removable()).enumerate() {
1920 if i == 0 {
1921 ui.add_space(spacing);
1922 ui.label(self.config.labels.heading_devices.as_str());
1923
1924 visible = true;
1925 }
1926
1927 self.ui_update_device_entry(ui, disk);
1928 }
1929
1930 visible
1931 }
1932
1933 fn ui_update_removable_devices(
1938 &mut self,
1939 ui: &mut egui::Ui,
1940 spacing: f32,
1941 disks: &Disks,
1942 ) -> bool {
1943 let mut visible = false;
1944
1945 for (i, disk) in disks.iter().filter(|x| x.is_removable()).enumerate() {
1946 if i == 0 {
1947 ui.add_space(spacing);
1948 ui.label(self.config.labels.heading_removable_devices.as_str());
1949
1950 visible = true;
1951 }
1952
1953 self.ui_update_device_entry(ui, disk);
1954 }
1955
1956 visible
1957 }
1958
1959 fn ui_update_device_entry(&mut self, ui: &mut egui::Ui, device: &Disk) {
1961 let label = if device.is_removable() {
1962 format!(
1963 "{} {}",
1964 self.config.removable_device_icon,
1965 device.display_name()
1966 )
1967 } else {
1968 format!("{} {}", self.config.device_icon, device.display_name())
1969 };
1970
1971 self.ui_update_left_panel_entry(ui, &label, device.mount_point());
1972 }
1973
1974 fn ui_update_bottom_panel(&mut self, ui: &mut egui::Ui) {
1976 const BUTTON_HEIGHT: f32 = 20.0;
1977 ui.add_space(5.0);
1978
1979 let label_submit_width = match self.mode {
1981 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
1982 Self::calc_text_width(ui, &self.config.labels.open_button)
1983 }
1984 DialogMode::SaveFile => Self::calc_text_width(ui, &self.config.labels.save_button),
1985 };
1986
1987 let mut btn_width = Self::calc_text_width(ui, &self.config.labels.cancel_button);
1988 if label_submit_width > btn_width {
1989 btn_width = label_submit_width;
1990 }
1991
1992 btn_width += ui.spacing().button_padding.x * 4.0;
1993
1994 let button_size: egui::Vec2 = egui::Vec2::new(btn_width, BUTTON_HEIGHT);
1996
1997 self.ui_update_selection_preview(ui, button_size);
1998
1999 if self.mode == DialogMode::SaveFile && self.config.save_extensions.is_empty() {
2000 ui.add_space(ui.style().spacing.item_spacing.y);
2001 }
2002
2003 self.ui_update_action_buttons(ui, button_size);
2004 }
2005
2006 fn ui_update_selection_preview(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2008 const SELECTION_PREVIEW_MIN_WIDTH: f32 = 50.0;
2009 let item_spacing = ui.style().spacing.item_spacing;
2010
2011 let render_filter_selection = (!self.config.file_filters.is_empty()
2012 && (self.mode == DialogMode::PickFile || self.mode == DialogMode::PickMultiple))
2013 || (!self.config.save_extensions.is_empty() && self.mode == DialogMode::SaveFile);
2014
2015 let filter_selection_width = button_size.x.mul_add(2.0, item_spacing.x);
2016 let mut filter_selection_separate_line = false;
2017
2018 ui.horizontal(|ui| {
2019 match &self.mode {
2020 DialogMode::PickDirectory => ui.label(&self.config.labels.selected_directory),
2021 DialogMode::PickFile => ui.label(&self.config.labels.selected_file),
2022 DialogMode::PickMultiple => ui.label(&self.config.labels.selected_items),
2023 DialogMode::SaveFile => ui.label(&self.config.labels.file_name),
2024 };
2025
2026 let mut scroll_bar_width: f32 =
2031 ui.available_width() - filter_selection_width - item_spacing.x;
2032
2033 if scroll_bar_width < SELECTION_PREVIEW_MIN_WIDTH || !render_filter_selection {
2034 filter_selection_separate_line = true;
2035 scroll_bar_width = ui.available_width();
2036 }
2037
2038 match &self.mode {
2039 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2040 use egui::containers::scroll_area::ScrollBarVisibility;
2041
2042 let text = self.get_selection_preview_text();
2043
2044 egui::containers::ScrollArea::horizontal()
2045 .auto_shrink([false, false])
2046 .max_width(scroll_bar_width)
2047 .stick_to_right(true)
2048 .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
2049 .show(ui, |ui| {
2050 ui.colored_label(ui.style().visuals.selection.bg_fill, text);
2051 });
2052 }
2053 DialogMode::SaveFile => {
2054 let mut output = egui::TextEdit::singleline(&mut self.file_name_input)
2055 .cursor_at_end(false)
2056 .margin(egui::Margin::symmetric(4, 3))
2057 .desired_width(scroll_bar_width - item_spacing.x)
2058 .show(ui);
2059
2060 if self.file_name_input_request_focus {
2061 self.highlight_file_name_input(&mut output);
2062 output.state.store(ui.ctx(), output.response.id);
2063
2064 output.response.request_focus();
2065 self.file_name_input_request_focus = false;
2066 }
2067
2068 if output.response.changed() {
2069 self.file_name_input_error = self.validate_file_name_input();
2070 }
2071
2072 if output.response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
2073 {
2074 self.submit();
2075 }
2076 }
2077 }
2078
2079 if !filter_selection_separate_line && render_filter_selection {
2080 if self.mode == DialogMode::SaveFile {
2081 self.ui_update_save_extension_selection(ui, filter_selection_width);
2082 } else {
2083 self.ui_update_file_filter_selection(ui, filter_selection_width);
2084 }
2085 }
2086 });
2087
2088 if filter_selection_separate_line && render_filter_selection {
2089 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2090 if self.mode == DialogMode::SaveFile {
2091 self.ui_update_save_extension_selection(ui, filter_selection_width);
2092 } else {
2093 self.ui_update_file_filter_selection(ui, filter_selection_width);
2094 }
2095 });
2096 }
2097 }
2098
2099 fn highlight_file_name_input(&self, output: &mut egui::text_edit::TextEditOutput) {
2103 if let Some(pos) = self.file_name_input.rfind('.') {
2104 let range = if pos == 0 {
2105 CCursorRange::two(CCursor::new(0), CCursor::new(0))
2106 } else {
2107 CCursorRange::two(CCursor::new(0), CCursor::new(pos))
2108 };
2109
2110 output.state.cursor.set_char_range(Some(range));
2111 }
2112 }
2113
2114 fn get_selection_preview_text(&self) -> String {
2115 if self.is_selection_valid() {
2116 match &self.mode {
2117 DialogMode::PickDirectory | DialogMode::PickFile => self
2118 .selected_item
2119 .as_ref()
2120 .map_or_else(String::new, |item| item.file_name().to_string()),
2121 DialogMode::PickMultiple => {
2122 let mut result = String::new();
2123
2124 for (i, item) in self
2125 .get_dir_content_filtered_iter()
2126 .filter(|p| p.selected)
2127 .enumerate()
2128 {
2129 if i == 0 {
2130 result += item.file_name();
2131 continue;
2132 }
2133
2134 result += format!(", {}", item.file_name()).as_str();
2135 }
2136
2137 result
2138 }
2139 DialogMode::SaveFile => String::new(),
2140 }
2141 } else {
2142 String::new()
2143 }
2144 }
2145
2146 fn ui_update_file_filter_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2147 let selected_filter = self.get_selected_file_filter();
2148 let selected_text = match selected_filter {
2149 Some(f) => &f.name,
2150 None => &self.config.labels.file_filter_all_files,
2151 };
2152
2153 let mut select_filter: Option<Option<FileFilter>> = None;
2156
2157 egui::containers::ComboBox::from_id_salt(self.window_id.with("file_filter_selection"))
2158 .width(width)
2159 .selected_text(selected_text)
2160 .wrap_mode(egui::TextWrapMode::Truncate)
2161 .show_ui(ui, |ui| {
2162 for filter in &self.config.file_filters {
2163 let selected = selected_filter.is_some_and(|f| f.id == filter.id);
2164
2165 if ui.selectable_label(selected, &filter.name).clicked() {
2166 select_filter = Some(Some(filter.clone()));
2167 }
2168 }
2169
2170 if ui
2171 .selectable_label(
2172 selected_filter.is_none(),
2173 &self.config.labels.file_filter_all_files,
2174 )
2175 .clicked()
2176 {
2177 select_filter = Some(None);
2178 }
2179 });
2180
2181 if let Some(i) = select_filter {
2182 self.select_file_filter(i);
2183 }
2184 }
2185
2186 fn ui_update_save_extension_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2187 let selected_extension = self.get_selected_save_extension();
2188 let selected_text = match selected_extension {
2189 Some(e) => &e.to_string(),
2190 None => &self.config.labels.save_extension_any,
2191 };
2192
2193 let mut select_extension: Option<Option<SaveExtension>> = None;
2196
2197 egui::containers::ComboBox::from_id_salt(self.window_id.with("save_extension_selection"))
2198 .width(width)
2199 .selected_text(selected_text)
2200 .wrap_mode(egui::TextWrapMode::Truncate)
2201 .show_ui(ui, |ui| {
2202 for extension in &self.config.save_extensions {
2203 let selected = selected_extension.is_some_and(|s| s.id == extension.id);
2204
2205 if ui
2206 .selectable_label(selected, extension.to_string())
2207 .clicked()
2208 {
2209 select_extension = Some(Some(extension.clone()));
2210 }
2211 }
2212 });
2213
2214 if let Some(i) = select_extension {
2215 self.file_name_input_request_focus = true;
2216 self.select_save_extension(i);
2217 }
2218 }
2219
2220 fn ui_update_action_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2222 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2223 let label = match &self.mode {
2224 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2225 self.config.labels.open_button.as_str()
2226 }
2227 DialogMode::SaveFile => self.config.labels.save_button.as_str(),
2228 };
2229
2230 if self.ui_button_sized(
2231 ui,
2232 self.is_selection_valid(),
2233 button_size,
2234 label,
2235 self.file_name_input_error.as_deref(),
2236 ) {
2237 self.submit();
2238 }
2239
2240 if ui
2241 .add_sized(
2242 button_size,
2243 egui::Button::new(self.config.labels.cancel_button.as_str()),
2244 )
2245 .clicked()
2246 {
2247 self.cancel();
2248 }
2249 });
2250 }
2251
2252 fn ui_update_central_panel(&mut self, ui: &mut egui::Ui) {
2255 if self.update_directory_content(ui) {
2256 return;
2257 }
2258
2259 self.ui_update_central_panel_content(ui);
2260 }
2261
2262 fn update_directory_content(&mut self, ui: &mut egui::Ui) -> bool {
2267 const SHOW_SPINNER_AFTER: f32 = 0.2;
2268
2269 match self.directory_content.update() {
2270 DirectoryContentState::Pending(timestamp) => {
2271 let now = std::time::SystemTime::now();
2272
2273 if now
2274 .duration_since(*timestamp)
2275 .unwrap_or_default()
2276 .as_secs_f32()
2277 > SHOW_SPINNER_AFTER
2278 {
2279 ui.centered_and_justified(egui::Ui::spinner);
2280 }
2281
2282 ui.ctx().request_repaint();
2284
2285 true
2286 }
2287 DirectoryContentState::Errored(err) => {
2288 ui.centered_and_justified(|ui| ui.colored_label(ui.visuals().error_fg_color, err));
2289 true
2290 }
2291 DirectoryContentState::Finished => {
2292 if self.mode == DialogMode::PickDirectory {
2293 if let Some(dir) = self.current_directory() {
2294 let mut dir_entry =
2295 DirectoryEntry::from_path(&self.config, dir, &*self.config.file_system);
2296 self.select_item(&mut dir_entry);
2297 }
2298 }
2299
2300 false
2301 }
2302 DirectoryContentState::Success => false,
2303 }
2304 }
2305
2306 fn ui_update_central_panel_content(&mut self, ui: &mut egui::Ui) {
2309 let mut data = std::mem::take(&mut self.directory_content);
2311
2312 let mut reset_multi_selection = false;
2315
2316 let mut batch_select_item_b: Option<DirectoryEntry> = None;
2319
2320 let mut should_return = false;
2322
2323 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
2324 let scroll_area = egui::containers::ScrollArea::vertical().auto_shrink([false, false]);
2325
2326 if self.search_value.is_empty()
2327 && !self.create_directory_dialog.is_open()
2328 && !self.scroll_to_selection
2329 {
2330 scroll_area.show_rows(ui, ui.spacing().interact_size.y, data.len(), |ui, range| {
2334 for item in data.iter_range_mut(range) {
2335 if self.ui_update_central_panel_entry(
2336 ui,
2337 item,
2338 &mut reset_multi_selection,
2339 &mut batch_select_item_b,
2340 ) {
2341 should_return = true;
2342 }
2343 }
2344 });
2345 } else {
2346 scroll_area.show(ui, |ui| {
2352 for item in data.filtered_iter_mut(&self.search_value.clone()) {
2353 if self.ui_update_central_panel_entry(
2354 ui,
2355 item,
2356 &mut reset_multi_selection,
2357 &mut batch_select_item_b,
2358 ) {
2359 should_return = true;
2360 }
2361 }
2362
2363 if let Some(entry) = self.ui_update_create_directory_dialog(ui) {
2364 data.push(entry);
2365 }
2366 });
2367 }
2368 });
2369
2370 if should_return {
2371 return;
2372 }
2373
2374 if reset_multi_selection {
2376 for item in data.filtered_iter_mut(&self.search_value) {
2377 if let Some(selected_item) = &self.selected_item {
2378 if selected_item.path_eq(item) {
2379 continue;
2380 }
2381 }
2382
2383 item.selected = false;
2384 }
2385 }
2386
2387 if let Some(item_b) = batch_select_item_b {
2389 if let Some(item_a) = &self.selected_item {
2390 self.batch_select_between(&mut data, item_a, &item_b);
2391 }
2392 }
2393
2394 self.directory_content = data;
2395 self.scroll_to_selection = false;
2396 }
2397
2398 fn ui_update_central_panel_entry(
2401 &mut self,
2402 ui: &mut egui::Ui,
2403 item: &mut DirectoryEntry,
2404 reset_multi_selection: &mut bool,
2405 batch_select_item_b: &mut Option<DirectoryEntry>,
2406 ) -> bool {
2407 let file_name = item.file_name();
2408 let primary_selected = self.is_primary_selected(item);
2409 let pinned = self.is_pinned(item.as_path());
2410
2411 let icons = if pinned {
2412 format!("{} {} ", item.icon(), self.config.pinned_icon)
2413 } else {
2414 format!("{} ", item.icon())
2415 };
2416
2417 let icons_width = Self::calc_text_width(ui, &icons);
2418
2419 let available_width = ui.available_width() - icons_width - 15.0;
2421
2422 let truncate = self.config.truncate_filenames
2423 && available_width < Self::calc_text_width(ui, file_name);
2424
2425 let text = if truncate {
2426 Self::truncate_filename(ui, item, available_width)
2427 } else {
2428 file_name.to_owned()
2429 };
2430
2431 let mut re =
2432 ui.selectable_label(primary_selected || item.selected, format!("{icons}{text}"));
2433
2434 if truncate {
2435 re = re.on_hover_text(file_name);
2436 }
2437
2438 if item.is_dir() {
2439 self.ui_update_central_panel_path_context_menu(&re, item.as_path());
2440
2441 if re.context_menu_opened() {
2442 self.select_item(item);
2443 }
2444 }
2445
2446 if primary_selected && self.scroll_to_selection {
2447 re.scroll_to_me(Some(egui::Align::Center));
2448 self.scroll_to_selection = false;
2449 }
2450
2451 if re.clicked()
2453 && !ui.input(|i| i.modifiers.command)
2454 && !ui.input(|i| i.modifiers.shift_only())
2455 {
2456 self.select_item(item);
2457
2458 if self.mode == DialogMode::PickMultiple {
2460 *reset_multi_selection = true;
2461 }
2462 }
2463
2464 if self.mode == DialogMode::PickMultiple
2467 && re.clicked()
2468 && ui.input(|i| i.modifiers.command)
2469 {
2470 if primary_selected {
2471 item.selected = false;
2474 self.selected_item = None;
2475 } else {
2476 item.selected = !item.selected;
2477
2478 if item.selected {
2480 self.select_item(item);
2481 }
2482 }
2483 }
2484
2485 if self.mode == DialogMode::PickMultiple
2488 && re.clicked()
2489 && ui.input(|i| i.modifiers.shift_only())
2490 {
2491 if let Some(selected_item) = self.selected_item.clone() {
2492 *batch_select_item_b = Some(selected_item);
2495
2496 item.selected = true;
2498 self.select_item(item);
2499 }
2500 }
2501
2502 if re.double_clicked() && !ui.input(|i| i.modifiers.command) {
2505 if item.is_dir() {
2506 self.load_directory(&item.to_path_buf());
2507 return true;
2508 }
2509
2510 self.select_item(item);
2511
2512 self.submit();
2513 }
2514
2515 false
2516 }
2517
2518 fn ui_update_create_directory_dialog(&mut self, ui: &mut egui::Ui) -> Option<DirectoryEntry> {
2519 self.create_directory_dialog
2520 .update(ui, &self.config)
2521 .directory()
2522 .map(|path| self.process_new_folder(&path))
2523 }
2524
2525 fn batch_select_between(
2528 &self,
2529 directory_content: &mut DirectoryContent,
2530 item_a: &DirectoryEntry,
2531 item_b: &DirectoryEntry,
2532 ) {
2533 let pos_a = directory_content
2535 .filtered_iter(&self.search_value)
2536 .position(|p| p.path_eq(item_a));
2537 let pos_b = directory_content
2538 .filtered_iter(&self.search_value)
2539 .position(|p| p.path_eq(item_b));
2540
2541 if let Some(pos_a) = pos_a {
2544 if let Some(pos_b) = pos_b {
2545 if pos_a == pos_b {
2546 return;
2547 }
2548
2549 let mut min = pos_a;
2552 let mut max = pos_b;
2553
2554 if min > max {
2555 min = pos_b;
2556 max = pos_a;
2557 }
2558
2559 for item in directory_content
2560 .filtered_iter_mut(&self.search_value)
2561 .enumerate()
2562 .filter(|(i, _)| i > &min && i < &max)
2563 .map(|(_, p)| p)
2564 {
2565 item.selected = true;
2566 }
2567 }
2568 }
2569 }
2570
2571 fn ui_button_sized(
2573 &self,
2574 ui: &mut egui::Ui,
2575 enabled: bool,
2576 size: egui::Vec2,
2577 label: &str,
2578 err_tooltip: Option<&str>,
2579 ) -> bool {
2580 let mut clicked = false;
2581
2582 ui.add_enabled_ui(enabled, |ui| {
2583 let response = ui.add_sized(size, egui::Button::new(label));
2584 clicked = response.clicked();
2585
2586 if let Some(err) = err_tooltip {
2587 response.on_disabled_hover_ui(|ui| {
2588 ui.horizontal_wrapped(|ui| {
2589 ui.spacing_mut().item_spacing.x = 0.0;
2590
2591 ui.colored_label(
2592 ui.ctx().style().visuals.error_fg_color,
2593 format!("{} ", self.config.err_icon),
2594 );
2595
2596 ui.label(err);
2597 });
2598 });
2599 }
2600 });
2601
2602 clicked
2603 }
2604
2605 fn ui_update_central_panel_path_context_menu(&mut self, item: &egui::Response, path: &Path) {
2612 if !self.config.show_pinned_folders {
2614 return;
2615 }
2616
2617 item.context_menu(|ui| {
2618 let pinned = self.is_pinned(path);
2619
2620 if pinned {
2621 if ui.button(&self.config.labels.unpin_folder).clicked() {
2622 self.unpin_path(path);
2623 ui.close_menu();
2624 }
2625 } else if ui.button(&self.config.labels.pin_folder).clicked() {
2626 self.pin_path(path.to_path_buf());
2627 ui.close_menu();
2628 }
2629 });
2630 }
2631
2632 fn set_cursor_to_end(re: &egui::Response, data: &str) {
2639 if let Some(mut state) = egui::TextEdit::load_state(&re.ctx, re.id) {
2641 state
2642 .cursor
2643 .set_char_range(Some(CCursorRange::one(CCursor::new(data.len()))));
2644 state.store(&re.ctx, re.id);
2645 }
2646 }
2647
2648 fn calc_char_width(ui: &egui::Ui, char: char) -> f32 {
2650 ui.fonts(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char))
2651 }
2652
2653 fn calc_text_width(ui: &egui::Ui, text: &str) -> f32 {
2656 let mut width = 0.0;
2657
2658 for char in text.chars() {
2659 width += Self::calc_char_width(ui, char);
2660 }
2661
2662 width
2663 }
2664
2665 fn truncate_filename(ui: &egui::Ui, item: &DirectoryEntry, max_length: f32) -> String {
2666 const TRUNCATE_STR: &str = "...";
2667
2668 let path = item.as_path();
2669
2670 let file_stem = if item.is_file() {
2671 path.file_stem().and_then(|f| f.to_str()).unwrap_or("")
2672 } else {
2673 item.file_name()
2674 };
2675
2676 let extension = if item.is_file() {
2677 path.extension().map_or(String::new(), |ext| {
2678 format!(".{}", ext.to_str().unwrap_or(""))
2679 })
2680 } else {
2681 String::new()
2682 };
2683
2684 let extension_width = Self::calc_text_width(ui, &extension);
2685 let reserved = extension_width + Self::calc_text_width(ui, TRUNCATE_STR);
2686
2687 if max_length <= reserved {
2688 return format!("{TRUNCATE_STR}{extension}");
2689 }
2690
2691 let mut width = reserved;
2692 let mut front = String::new();
2693 let mut back = String::new();
2694
2695 for (i, char) in file_stem.chars().enumerate() {
2696 let w = Self::calc_char_width(ui, char);
2697
2698 if width + w > max_length {
2699 break;
2700 }
2701
2702 front.push(char);
2703 width += w;
2704
2705 let back_index = file_stem.len() - i - 1;
2706
2707 if back_index <= i {
2708 break;
2709 }
2710
2711 if let Some(char) = file_stem.chars().nth(back_index) {
2712 let w = Self::calc_char_width(ui, char);
2713
2714 if width + w > max_length {
2715 break;
2716 }
2717
2718 back.push(char);
2719 width += w;
2720 }
2721 }
2722
2723 format!(
2724 "{front}{TRUNCATE_STR}{}{extension}",
2725 back.chars().rev().collect::<String>()
2726 )
2727 }
2728}
2729
2730impl FileDialog {
2732 fn update_keybindings(&mut self, ctx: &egui::Context) {
2734 if let Some(modal) = self.modals.last_mut() {
2737 modal.update_keybindings(&self.config, ctx);
2738 return;
2739 }
2740
2741 let keybindings = std::mem::take(&mut self.config.keybindings);
2742
2743 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.submit, false) {
2744 self.exec_keybinding_submit();
2745 }
2746
2747 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.cancel, false) {
2748 self.exec_keybinding_cancel();
2749 }
2750
2751 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.parent, true) {
2752 self.load_parent_directory();
2753 }
2754
2755 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.back, true) {
2756 self.load_previous_directory();
2757 }
2758
2759 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.forward, true) {
2760 self.load_next_directory();
2761 }
2762
2763 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.reload, true) {
2764 self.refresh();
2765 }
2766
2767 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.new_folder, true) {
2768 self.open_new_folder_dialog();
2769 }
2770
2771 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.edit_path, true) {
2772 self.open_path_edit();
2773 }
2774
2775 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.home_edit_path, true) {
2776 if let Some(dirs) = &self.user_directories {
2777 if let Some(home) = dirs.home_dir() {
2778 self.load_directory(home.to_path_buf().as_path());
2779 self.open_path_edit();
2780 }
2781 }
2782 }
2783
2784 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_up, false) {
2785 self.exec_keybinding_selection_up();
2786
2787 if let Some(id) = ctx.memory(egui::Memory::focused) {
2789 ctx.memory_mut(|w| w.surrender_focus(id));
2790 }
2791 }
2792
2793 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_down, false) {
2794 self.exec_keybinding_selection_down();
2795
2796 if let Some(id) = ctx.memory(egui::Memory::focused) {
2798 ctx.memory_mut(|w| w.surrender_focus(id));
2799 }
2800 }
2801
2802 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.select_all, true)
2803 && self.mode == DialogMode::PickMultiple
2804 {
2805 for item in self.directory_content.filtered_iter_mut(&self.search_value) {
2806 item.selected = true;
2807 }
2808 }
2809
2810 self.config.keybindings = keybindings;
2811 }
2812
2813 fn exec_keybinding_submit(&mut self) {
2815 if self.path_edit_visible {
2816 self.submit_path_edit();
2817 return;
2818 }
2819
2820 if self.create_directory_dialog.is_open() {
2821 if let Some(dir) = self.create_directory_dialog.submit().directory() {
2822 self.process_new_folder(&dir);
2823 }
2824 return;
2825 }
2826
2827 if self.any_focused_last_frame {
2828 return;
2829 }
2830
2831 if let Some(item) = &self.selected_item {
2833 let is_visible = self
2835 .get_dir_content_filtered_iter()
2836 .any(|p| p.path_eq(item));
2837
2838 if is_visible && item.is_dir() {
2839 self.load_directory(&item.to_path_buf());
2840 return;
2841 }
2842 }
2843
2844 self.submit();
2845 }
2846
2847 fn exec_keybinding_cancel(&mut self) {
2849 if self.create_directory_dialog.is_open() {
2867 self.create_directory_dialog.close();
2868 } else if self.path_edit_visible {
2869 self.close_path_edit();
2870 } else if !self.any_focused_last_frame {
2871 self.cancel();
2872 return;
2873 }
2874 }
2875
2876 fn exec_keybinding_selection_up(&mut self) {
2878 if self.directory_content.len() == 0 {
2879 return;
2880 }
2881
2882 self.directory_content.reset_multi_selection();
2883
2884 if let Some(item) = &self.selected_item {
2885 if self.select_next_visible_item_before(&item.clone()) {
2886 return;
2887 }
2888 }
2889
2890 self.select_last_visible_item();
2893 }
2894
2895 fn exec_keybinding_selection_down(&mut self) {
2897 if self.directory_content.len() == 0 {
2898 return;
2899 }
2900
2901 self.directory_content.reset_multi_selection();
2902
2903 if let Some(item) = &self.selected_item {
2904 if self.select_next_visible_item_after(&item.clone()) {
2905 return;
2906 }
2907 }
2908
2909 self.select_first_visible_item();
2912 }
2913}
2914
2915impl FileDialog {
2917 fn get_selected_file_filter(&self) -> Option<&FileFilter> {
2919 self.selected_file_filter
2920 .and_then(|id| self.config.file_filters.iter().find(|p| p.id == id))
2921 }
2922
2923 fn set_default_file_filter(&mut self) {
2925 if let Some(name) = &self.config.default_file_filter {
2926 for filter in &self.config.file_filters {
2927 if filter.name == name.as_str() {
2928 self.selected_file_filter = Some(filter.id);
2929 }
2930 }
2931 }
2932 }
2933
2934 fn select_file_filter(&mut self, filter: Option<FileFilter>) {
2936 self.selected_file_filter = filter.map(|f| f.id);
2937 self.selected_item = None;
2938 self.refresh();
2939 }
2940
2941 fn get_selected_save_extension(&self) -> Option<&SaveExtension> {
2943 self.selected_save_extension
2944 .and_then(|id| self.config.save_extensions.iter().find(|p| p.id == id))
2945 }
2946
2947 fn set_default_save_extension(&mut self) {
2949 let config = std::mem::take(&mut self.config);
2950
2951 if let Some(name) = &config.default_save_extension {
2952 for extension in &config.save_extensions {
2953 if extension.name == name.as_str() {
2954 self.selected_save_extension = Some(extension.id);
2955 self.set_file_name_extension(&extension.file_extension);
2956 }
2957 }
2958 }
2959
2960 self.config = config;
2961 }
2962
2963 fn select_save_extension(&mut self, extension: Option<SaveExtension>) {
2965 if let Some(ex) = extension {
2966 self.selected_save_extension = Some(ex.id);
2967 self.set_file_name_extension(&ex.file_extension);
2968 }
2969
2970 self.selected_item = None;
2971 self.refresh();
2972 }
2973
2974 fn set_file_name_extension(&mut self, extension: &str) {
2976 let dot_count = self.file_name_input.chars().filter(|c| *c == '.').count();
2980 let use_simple = dot_count == 1 && self.file_name_input.chars().nth(0) == Some('.');
2981
2982 let mut p = PathBuf::from(&self.file_name_input);
2983 if !use_simple && p.set_extension(extension) {
2984 self.file_name_input = p.to_string_lossy().into_owned();
2985 } else {
2986 self.file_name_input = format!(".{extension}");
2987 }
2988 }
2989
2990 fn get_dir_content_filtered_iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
2992 self.directory_content.filtered_iter(&self.search_value)
2993 }
2994
2995 fn open_new_folder_dialog(&mut self) {
2997 if let Some(x) = self.current_directory() {
2998 self.create_directory_dialog.open(x.to_path_buf());
2999 }
3000 }
3001
3002 fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry {
3004 let mut entry =
3005 DirectoryEntry::from_path(&self.config, created_dir, &*self.config.file_system);
3006
3007 self.directory_content.push(entry.clone());
3008
3009 self.select_item(&mut entry);
3010
3011 entry
3012 }
3013
3014 fn open_modal(&mut self, modal: Box<dyn FileDialogModal + Send + Sync>) {
3016 self.modals.push(modal);
3017 }
3018
3019 fn exec_modal_action(&mut self, action: ModalAction) {
3021 match action {
3022 ModalAction::None => {}
3023 ModalAction::SaveFile(path) => self.state = DialogState::Picked(path),
3024 }
3025 }
3026
3027 fn canonicalize_path(&self, path: &Path) -> PathBuf {
3030 if self.config.canonicalize_paths {
3031 dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
3032 } else {
3033 path.to_path_buf()
3034 }
3035 }
3036
3037 fn pin_path(&mut self, path: PathBuf) {
3039 let pinned = PinnedFolder::from_path(path);
3040 self.storage.pinned_folders.push(pinned);
3041 }
3042
3043 fn unpin_path(&mut self, path: &Path) {
3045 self.storage
3046 .pinned_folders
3047 .retain(|p| p.path.as_path() != path);
3048 }
3049
3050 fn is_pinned(&self, path: &Path) -> bool {
3052 self.storage
3053 .pinned_folders
3054 .iter()
3055 .any(|p| p.path.as_path() == path)
3056 }
3057
3058 fn begin_rename_pinned_folder(&mut self, pinned: PinnedFolder) {
3060 self.rename_pinned_folder = Some(pinned);
3061 self.rename_pinned_folder_request_focus = true;
3062 }
3063
3064 fn end_rename_pinned_folder(&mut self) {
3067 let renamed = std::mem::take(&mut self.rename_pinned_folder);
3068
3069 if let Some(renamed) = renamed {
3070 let old = self
3071 .storage
3072 .pinned_folders
3073 .iter_mut()
3074 .find(|p| p.path == renamed.path);
3075 if let Some(old) = old {
3076 old.label = renamed.label;
3077 }
3078 }
3079 }
3080
3081 fn is_pinned_folder_being_renamed(&self, pinned: &PinnedFolder) -> bool {
3083 self.rename_pinned_folder
3084 .as_ref()
3085 .is_some_and(|p| p.path == pinned.path)
3086 }
3087
3088 fn is_primary_selected(&self, item: &DirectoryEntry) -> bool {
3089 self.selected_item.as_ref().is_some_and(|x| x.path_eq(item))
3090 }
3091
3092 fn reset(&mut self) {
3095 let storage = self.storage.clone();
3096 let config = self.config.clone();
3097 *self = Self::with_config(config);
3098 self.storage = storage;
3099 }
3100
3101 fn refresh(&mut self) {
3104 self.user_directories = self
3105 .config
3106 .file_system
3107 .user_dirs(self.config.canonicalize_paths);
3108 self.system_disks = self
3109 .config
3110 .file_system
3111 .get_disks(self.config.canonicalize_paths);
3112
3113 self.reload_directory();
3114 }
3115
3116 fn submit(&mut self) {
3118 if !self.is_selection_valid() {
3120 return;
3121 }
3122
3123 self.storage.last_picked_dir = self.current_directory().map(PathBuf::from);
3124
3125 match &self.mode {
3126 DialogMode::PickDirectory | DialogMode::PickFile => {
3127 if let Some(item) = self.selected_item.clone() {
3130 self.state = DialogState::Picked(item.to_path_buf());
3131 }
3132 }
3133 DialogMode::PickMultiple => {
3134 let result: Vec<PathBuf> = self
3135 .selected_entries()
3136 .map(crate::DirectoryEntry::to_path_buf)
3137 .collect();
3138
3139 self.state = DialogState::PickedMultiple(result);
3140 }
3141 DialogMode::SaveFile => {
3142 if let Some(path) = self.current_directory() {
3145 let full_path = path.join(&self.file_name_input);
3146 self.submit_save_file(full_path);
3147 }
3148 }
3149 }
3150 }
3151
3152 fn submit_save_file(&mut self, path: PathBuf) {
3155 if path.exists() {
3156 self.open_modal(Box::new(OverwriteFileModal::new(path)));
3157
3158 return;
3159 }
3160
3161 self.state = DialogState::Picked(path);
3162 }
3163
3164 fn cancel(&mut self) {
3166 self.state = DialogState::Cancelled;
3167 }
3168
3169 fn get_initial_directory(&self) -> PathBuf {
3175 let path = match self.config.opening_mode {
3176 OpeningMode::AlwaysInitialDir => &self.config.initial_directory,
3177 OpeningMode::LastVisitedDir => self
3178 .storage
3179 .last_visited_dir
3180 .as_deref()
3181 .unwrap_or(&self.config.initial_directory),
3182 OpeningMode::LastPickedDir => self
3183 .storage
3184 .last_picked_dir
3185 .as_deref()
3186 .unwrap_or(&self.config.initial_directory),
3187 };
3188
3189 let mut path = self.canonicalize_path(path);
3190
3191 if self.config.file_system.is_file(&path) {
3192 if let Some(parent) = path.parent() {
3193 path = parent.to_path_buf();
3194 }
3195 }
3196
3197 path
3198 }
3199
3200 fn current_directory(&self) -> Option<&Path> {
3202 if let Some(x) = self.directory_stack.iter().nth_back(self.directory_offset) {
3203 return Some(x.as_path());
3204 }
3205
3206 None
3207 }
3208
3209 fn is_selection_valid(&self) -> bool {
3212 match &self.mode {
3213 DialogMode::PickDirectory => self
3214 .selected_item
3215 .as_ref()
3216 .is_some_and(crate::DirectoryEntry::is_dir),
3217 DialogMode::PickFile => self
3218 .selected_item
3219 .as_ref()
3220 .is_some_and(DirectoryEntry::is_file),
3221 DialogMode::PickMultiple => self.get_dir_content_filtered_iter().any(|p| p.selected),
3222 DialogMode::SaveFile => self.file_name_input_error.is_none(),
3223 }
3224 }
3225
3226 fn validate_file_name_input(&self) -> Option<String> {
3230 if self.file_name_input.is_empty() {
3231 return Some(self.config.labels.err_empty_file_name.clone());
3232 }
3233
3234 if let Some(x) = self.current_directory() {
3235 let mut full_path = x.to_path_buf();
3236 full_path.push(self.file_name_input.as_str());
3237
3238 if self.config.file_system.is_dir(&full_path) {
3239 return Some(self.config.labels.err_directory_exists.clone());
3240 }
3241
3242 if !self.config.allow_file_overwrite && self.config.file_system.is_file(&full_path) {
3243 return Some(self.config.labels.err_file_exists.clone());
3244 }
3245 } else {
3246 return Some("Currently not in a directory".to_string());
3248 }
3249
3250 None
3251 }
3252
3253 fn select_item(&mut self, item: &mut DirectoryEntry) {
3256 if self.mode == DialogMode::PickMultiple {
3257 item.selected = true;
3258 }
3259 self.selected_item = Some(item.clone());
3260
3261 if self.mode == DialogMode::SaveFile && item.is_file() {
3262 self.file_name_input = item.file_name().to_string();
3263 self.file_name_input_error = self.validate_file_name_input();
3264 }
3265 }
3266
3267 fn select_next_visible_item_before(&mut self, item: &DirectoryEntry) -> bool {
3272 let mut return_val = false;
3273
3274 self.directory_content.reset_multi_selection();
3275
3276 let mut directory_content = std::mem::take(&mut self.directory_content);
3277 let search_value = std::mem::take(&mut self.search_value);
3278
3279 let index = directory_content
3280 .filtered_iter(&search_value)
3281 .position(|p| p.path_eq(item));
3282
3283 if let Some(index) = index {
3284 if index != 0 {
3285 if let Some(item) = directory_content
3286 .filtered_iter_mut(&search_value)
3287 .nth(index.saturating_sub(1))
3288 {
3289 self.select_item(item);
3290 self.scroll_to_selection = true;
3291 return_val = true;
3292 }
3293 }
3294 }
3295
3296 self.directory_content = directory_content;
3297 self.search_value = search_value;
3298
3299 return_val
3300 }
3301
3302 fn select_next_visible_item_after(&mut self, item: &DirectoryEntry) -> bool {
3307 let mut return_val = false;
3308
3309 self.directory_content.reset_multi_selection();
3310
3311 let mut directory_content = std::mem::take(&mut self.directory_content);
3312 let search_value = std::mem::take(&mut self.search_value);
3313
3314 let index = directory_content
3315 .filtered_iter(&search_value)
3316 .position(|p| p.path_eq(item));
3317
3318 if let Some(index) = index {
3319 if let Some(item) = directory_content
3320 .filtered_iter_mut(&search_value)
3321 .nth(index.saturating_add(1))
3322 {
3323 self.select_item(item);
3324 self.scroll_to_selection = true;
3325 return_val = true;
3326 }
3327 }
3328
3329 self.directory_content = directory_content;
3330 self.search_value = search_value;
3331
3332 return_val
3333 }
3334
3335 fn select_first_visible_item(&mut self) {
3337 self.directory_content.reset_multi_selection();
3338
3339 let mut directory_content = std::mem::take(&mut self.directory_content);
3340
3341 if let Some(item) = directory_content
3342 .filtered_iter_mut(&self.search_value.clone())
3343 .next()
3344 {
3345 self.select_item(item);
3346 self.scroll_to_selection = true;
3347 }
3348
3349 self.directory_content = directory_content;
3350 }
3351
3352 fn select_last_visible_item(&mut self) {
3354 self.directory_content.reset_multi_selection();
3355
3356 let mut directory_content = std::mem::take(&mut self.directory_content);
3357
3358 if let Some(item) = directory_content
3359 .filtered_iter_mut(&self.search_value.clone())
3360 .last()
3361 {
3362 self.select_item(item);
3363 self.scroll_to_selection = true;
3364 }
3365
3366 self.directory_content = directory_content;
3367 }
3368
3369 fn open_path_edit(&mut self) {
3371 let path = self.current_directory().map_or_else(String::new, |path| {
3372 path.to_str().unwrap_or_default().to_string()
3373 });
3374
3375 self.path_edit_value = path;
3376 self.path_edit_activate = true;
3377 self.path_edit_visible = true;
3378 }
3379
3380 fn submit_path_edit(&mut self) {
3382 self.close_path_edit();
3383
3384 let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value));
3385
3386 if self.mode == DialogMode::PickFile && self.config.file_system.is_file(&path) {
3387 self.state = DialogState::Picked(path);
3388 return;
3389 }
3390
3391 if self.mode == DialogMode::SaveFile
3398 && (path.extension().is_some()
3399 || self.config.allow_path_edit_to_save_file_without_extension)
3400 && !self.config.file_system.is_dir(&path)
3401 && path.parent().is_some_and(std::path::Path::exists)
3402 {
3403 self.submit_save_file(path);
3404 return;
3405 }
3406
3407 self.load_directory(&path);
3408 }
3409
3410 const fn close_path_edit(&mut self) {
3413 self.path_edit_visible = false;
3414 }
3415
3416 fn load_next_directory(&mut self) {
3421 if self.directory_offset == 0 {
3422 return;
3424 }
3425
3426 self.directory_offset -= 1;
3427
3428 if let Some(path) = self.current_directory() {
3430 self.load_directory_content(path.to_path_buf().as_path());
3431 }
3432 }
3433
3434 fn load_previous_directory(&mut self) {
3438 if self.directory_offset + 1 >= self.directory_stack.len() {
3439 return;
3441 }
3442
3443 self.directory_offset += 1;
3444
3445 if let Some(path) = self.current_directory() {
3447 self.load_directory_content(path.to_path_buf().as_path());
3448 }
3449 }
3450
3451 fn load_parent_directory(&mut self) {
3455 if let Some(x) = self.current_directory() {
3456 if let Some(x) = x.to_path_buf().parent() {
3457 self.load_directory(x);
3458 }
3459 }
3460 }
3461
3462 fn reload_directory(&mut self) {
3469 if let Some(x) = self.current_directory() {
3470 self.load_directory_content(x.to_path_buf().as_path());
3471 }
3472 }
3473
3474 fn load_directory(&mut self, path: &Path) {
3480 if let Some(x) = self.current_directory() {
3483 if x == path {
3484 return;
3485 }
3486 }
3487
3488 if self.directory_offset != 0 && self.directory_stack.len() > self.directory_offset {
3489 self.directory_stack
3490 .drain(self.directory_stack.len() - self.directory_offset..);
3491 }
3492
3493 self.directory_stack.push(path.to_path_buf());
3494 self.directory_offset = 0;
3495
3496 self.load_directory_content(path);
3497
3498 self.search_value.clear();
3501 }
3502
3503 fn load_directory_content(&mut self, path: &Path) {
3505 self.storage.last_visited_dir = Some(path.to_path_buf());
3506
3507 let selected_file_filter = match self.mode {
3508 DialogMode::PickFile | DialogMode::PickMultiple => self.get_selected_file_filter(),
3509 _ => None,
3510 };
3511
3512 let selected_save_extension = if self.mode == DialogMode::SaveFile {
3513 self.get_selected_save_extension()
3514 .map(|e| e.file_extension.as_str())
3515 } else {
3516 None
3517 };
3518
3519 let filter = DirectoryFilter {
3520 show_files: self.show_files,
3521 show_hidden: self.storage.show_hidden,
3522 show_system_files: self.storage.show_system_files,
3523 file_filter: selected_file_filter.cloned(),
3524 filter_extension: selected_save_extension.map(str::to_string),
3525 };
3526
3527 self.directory_content = DirectoryContent::from_path(
3528 &self.config,
3529 path,
3530 self.config.file_system.clone(),
3531 filter,
3532 );
3533
3534 self.create_directory_dialog.close();
3535 self.scroll_to_selection = true;
3536
3537 if self.mode == DialogMode::SaveFile {
3538 self.file_name_input_error = self.validate_file_name_input();
3539 }
3540 }
3541}