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::any::Any;
14use std::fmt::Debug;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17
18#[derive(Debug, PartialEq, Eq, Clone, Copy)]
20pub enum DialogMode {
21 PickFile,
23
24 PickDirectory,
26
27 PickMultiple,
29
30 SaveFile,
32}
33
34#[derive(Debug, PartialEq, Eq, Clone)]
36pub enum DialogState {
37 Open,
39
40 Closed,
42
43 Picked(PathBuf),
45
46 PickedMultiple(Vec<PathBuf>),
48
49 Cancelled,
51}
52
53#[derive(Debug, Clone)]
55#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
56pub struct FileDialogStorage {
57 pub pinned_folders: Vec<PinnedFolder>,
59 pub show_hidden: bool,
61 pub show_system_files: bool,
63 pub last_visited_dir: Option<PathBuf>,
65 pub last_picked_dir: Option<PathBuf>,
67}
68
69impl Default for FileDialogStorage {
70 fn default() -> Self {
72 Self {
73 pinned_folders: Vec::new(),
74 show_hidden: false,
75 show_system_files: false,
76 last_visited_dir: None,
77 last_picked_dir: None,
78 }
79 }
80}
81
82#[derive(Debug)]
108pub struct FileDialog {
109 config: FileDialogConfig,
111 storage: FileDialogStorage,
113
114 modals: Vec<Box<dyn FileDialogModal + Send + Sync>>,
117
118 mode: DialogMode,
120 state: DialogState,
122 show_files: bool,
125 user_data: Option<Box<dyn Any + Send + Sync>>,
128 window_id: egui::Id,
130
131 user_directories: Option<UserDirectories>,
134 system_disks: Disks,
137
138 directory_stack: Vec<PathBuf>,
142 directory_offset: usize,
147 directory_content: DirectoryContent,
149
150 create_directory_dialog: CreateDirectoryDialog,
152
153 path_edit_visible: bool,
155 path_edit_value: String,
157 path_edit_activate: bool,
160 path_edit_request_focus: bool,
162
163 selected_item: Option<DirectoryEntry>,
166 file_name_input: String,
168 file_name_input_error: Option<String>,
171 file_name_input_request_focus: bool,
173 selected_file_filter: Option<egui::Id>,
175 selected_save_extension: Option<egui::Id>,
177
178 scroll_to_selection: bool,
180 search_value: String,
182 init_search: bool,
184
185 any_focused_last_frame: bool,
189
190 rename_pinned_folder: Option<PinnedFolder>,
193 rename_pinned_folder_request_focus: bool,
196}
197
198#[cfg(test)]
200const fn test_prop<T: Send + Sync>() {}
201
202#[test]
203const fn test() {
204 test_prop::<FileDialog>();
205}
206
207impl Default for FileDialog {
208 fn default() -> Self {
210 Self::new()
211 }
212}
213
214impl Debug for dyn FileDialogModal + Send + Sync {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 write!(f, "<FileDialogModal>")
217 }
218}
219
220type FileDialogUiCallback<'a> = dyn FnMut(&mut egui::Ui, &mut FileDialog) + 'a;
225
226impl FileDialog {
227 #[must_use]
232 pub fn new() -> Self {
233 let file_system = Arc::new(NativeFileSystem);
234
235 Self {
236 config: FileDialogConfig::default_from_filesystem(file_system.clone()),
237 storage: FileDialogStorage::default(),
238
239 modals: Vec::new(),
240
241 mode: DialogMode::PickDirectory,
242 state: DialogState::Closed,
243 show_files: true,
244 user_data: None,
245
246 window_id: egui::Id::new("file_dialog"),
247
248 user_directories: None,
249 system_disks: Disks::new_empty(),
250
251 directory_stack: Vec::new(),
252 directory_offset: 0,
253 directory_content: DirectoryContent::default(),
254
255 create_directory_dialog: CreateDirectoryDialog::from_filesystem(file_system),
256
257 path_edit_visible: false,
258 path_edit_value: String::new(),
259 path_edit_activate: false,
260 path_edit_request_focus: false,
261
262 selected_item: None,
263 file_name_input: String::new(),
264 file_name_input_error: None,
265 file_name_input_request_focus: true,
266 selected_file_filter: None,
267 selected_save_extension: None,
268
269 scroll_to_selection: false,
270 search_value: String::new(),
271 init_search: false,
272
273 any_focused_last_frame: false,
274
275 rename_pinned_folder: None,
276 rename_pinned_folder_request_focus: false,
277 }
278 }
279
280 pub fn with_config(config: FileDialogConfig) -> Self {
282 let mut obj = Self::new();
283 *obj.config_mut() = config;
284 obj.create_directory_dialog =
285 CreateDirectoryDialog::from_filesystem(obj.config.file_system.clone());
286 obj
287 }
288
289 #[must_use]
291 pub fn with_file_system(file_system: Arc<dyn FileSystem + Send + Sync>) -> Self {
292 let mut obj = Self::new();
293 obj.config.initial_directory = file_system.current_dir().unwrap_or_default();
294 obj.config.file_system = file_system;
295 obj.create_directory_dialog =
296 CreateDirectoryDialog::from_filesystem(obj.config.file_system.clone());
297 obj
298 }
299
300 #[deprecated(
346 since = "0.10.0",
347 note = "Use `pick_file` / `pick_directory` / `pick_multiple` in combination with \
348 `set_user_data` instead"
349 )]
350 pub fn open(&mut self, mode: DialogMode, mut show_files: bool) {
351 self.reset();
352 self.refresh();
353
354 if mode == DialogMode::PickFile {
355 show_files = true;
356 }
357
358 if mode == DialogMode::SaveFile {
359 self.file_name_input_request_focus = true;
360 self.file_name_input
361 .clone_from(&self.config.default_file_name);
362 }
363
364 self.selected_file_filter = None;
365 self.selected_save_extension = None;
366
367 self.set_default_file_filter();
368 self.set_default_save_extension();
369
370 self.mode = mode;
371 self.state = DialogState::Open;
372 self.show_files = show_files;
373
374 self.window_id = self
375 .config
376 .id
377 .map_or_else(|| egui::Id::new(self.get_window_title()), |id| id);
378
379 self.load_directory(&self.get_initial_directory());
380 }
381
382 pub fn pick_directory(&mut self) {
390 #[allow(deprecated)]
392 self.open(DialogMode::PickDirectory, false);
393 }
394
395 pub fn pick_file(&mut self) {
401 #[allow(deprecated)]
403 self.open(DialogMode::PickFile, true);
404 }
405
406 pub fn pick_multiple(&mut self) {
413 #[allow(deprecated)]
415 self.open(DialogMode::PickMultiple, true);
416 }
417
418 pub fn save_file(&mut self) {
424 #[allow(deprecated)]
426 self.open(DialogMode::SaveFile, true);
427 }
428
429 pub fn update(&mut self, ctx: &egui::Context) -> &Self {
433 if self.state != DialogState::Open {
434 return self;
435 }
436
437 self.update_keybindings(ctx);
438 self.update_ui(ctx, None);
439
440 self
441 }
442
443 pub fn set_right_panel_width(&mut self, width: f32) {
445 self.config.right_panel_width = Some(width);
446 }
447
448 pub fn clear_right_panel_width(&mut self) {
450 self.config.right_panel_width = None;
451 }
452
453 pub fn update_with_right_panel_ui(
465 &mut self,
466 ctx: &egui::Context,
467 f: &mut FileDialogUiCallback,
468 ) -> &Self {
469 if self.state != DialogState::Open {
470 return self;
471 }
472
473 self.update_keybindings(ctx);
474 self.update_ui(ctx, Some(f));
475
476 self
477 }
478
479 pub fn config_mut(&mut self) -> &mut FileDialogConfig {
484 &mut self.config
485 }
486
487 pub fn storage(mut self, storage: FileDialogStorage) -> Self {
491 self.storage = storage;
492 self
493 }
494
495 pub fn storage_mut(&mut self) -> &mut FileDialogStorage {
497 &mut self.storage
498 }
499
500 pub fn keybindings(mut self, keybindings: FileDialogKeyBindings) -> Self {
502 self.config.keybindings = keybindings;
503 self
504 }
505
506 pub fn labels(mut self, labels: FileDialogLabels) -> Self {
512 self.config.labels = labels;
513 self
514 }
515
516 pub fn labels_mut(&mut self) -> &mut FileDialogLabels {
518 &mut self.config.labels
519 }
520
521 pub const fn opening_mode(mut self, opening_mode: OpeningMode) -> Self {
523 self.config.opening_mode = opening_mode;
524 self
525 }
526
527 pub const fn as_modal(mut self, as_modal: bool) -> Self {
532 self.config.as_modal = as_modal;
533 self
534 }
535
536 pub const fn modal_overlay_color(mut self, modal_overlay_color: egui::Color32) -> Self {
538 self.config.modal_overlay_color = modal_overlay_color;
539 self
540 }
541
542 pub fn initial_directory(mut self, directory: PathBuf) -> Self {
551 self.config.initial_directory = directory;
552 self
553 }
554
555 pub fn default_file_name(mut self, name: &str) -> Self {
557 name.clone_into(&mut self.config.default_file_name);
558 self
559 }
560
561 pub const fn allow_file_overwrite(mut self, allow_file_overwrite: bool) -> Self {
567 self.config.allow_file_overwrite = allow_file_overwrite;
568 self
569 }
570
571 pub const fn allow_path_edit_to_save_file_without_extension(mut self, allow: bool) -> Self {
580 self.config.allow_path_edit_to_save_file_without_extension = allow;
581 self
582 }
583
584 pub fn directory_separator(mut self, separator: &str) -> Self {
587 self.config.directory_separator = separator.to_string();
588 self
589 }
590
591 pub const fn canonicalize_paths(mut self, canonicalize: bool) -> Self {
608 self.config.canonicalize_paths = canonicalize;
609 self
610 }
611
612 pub const fn load_via_thread(mut self, load_via_thread: bool) -> Self {
616 self.config.load_via_thread = load_via_thread;
617 self
618 }
619
620 pub const fn truncate_filenames(mut self, truncate_filenames: bool) -> Self {
626 self.config.truncate_filenames = truncate_filenames;
627 self
628 }
629
630 pub fn err_icon(mut self, icon: &str) -> Self {
632 self.config.err_icon = icon.to_string();
633 self
634 }
635
636 pub fn default_file_icon(mut self, icon: &str) -> Self {
638 self.config.default_file_icon = icon.to_string();
639 self
640 }
641
642 pub fn default_folder_icon(mut self, icon: &str) -> Self {
644 self.config.default_folder_icon = icon.to_string();
645 self
646 }
647
648 pub fn device_icon(mut self, icon: &str) -> Self {
650 self.config.device_icon = icon.to_string();
651 self
652 }
653
654 pub fn removable_device_icon(mut self, icon: &str) -> Self {
656 self.config.removable_device_icon = icon.to_string();
657 self
658 }
659
660 pub fn add_file_filter(mut self, name: &str, filter: Filter<Path>) -> Self {
686 self.config = self.config.add_file_filter(name, filter);
687 self
688 }
689
690 pub fn add_file_filter_extensions(mut self, name: &str, extensions: Vec<&'static str>) -> Self {
706 self.config = self.config.add_file_filter_extensions(name, extensions);
707 self
708 }
709
710 pub fn default_file_filter(mut self, name: &str) -> Self {
714 self.config.default_file_filter = Some(name.to_string());
715 self
716 }
717
718 pub fn add_save_extension(mut self, name: &str, file_extension: &str) -> Self {
740 self.config = self.config.add_save_extension(name, file_extension);
741 self
742 }
743
744 pub fn default_save_extension(mut self, name: &str) -> Self {
748 self.config.default_save_extension = Some(name.to_string());
749 self
750 }
751
752 pub fn set_file_icon(mut self, icon: &str, filter: Filter<std::path::Path>) -> Self {
773 self.config = self.config.set_file_icon(icon, filter);
774 self
775 }
776
777 pub fn add_quick_access(
793 mut self,
794 heading: &str,
795 builder: impl FnOnce(&mut QuickAccess),
796 ) -> Self {
797 self.config = self.config.add_quick_access(heading, builder);
798 self
799 }
800
801 pub fn title(mut self, title: &str) -> Self {
806 self.config.title = Some(title.to_string());
807 self
808 }
809
810 pub fn id(mut self, id: impl Into<egui::Id>) -> Self {
812 self.config.id = Some(id.into());
813 self
814 }
815
816 pub fn default_pos(mut self, default_pos: impl Into<egui::Pos2>) -> Self {
818 self.config.default_pos = Some(default_pos.into());
819 self
820 }
821
822 pub fn fixed_pos(mut self, pos: impl Into<egui::Pos2>) -> Self {
824 self.config.fixed_pos = Some(pos.into());
825 self
826 }
827
828 pub fn default_size(mut self, size: impl Into<egui::Vec2>) -> Self {
830 self.config.default_size = size.into();
831 self
832 }
833
834 pub fn max_size(mut self, max_size: impl Into<egui::Vec2>) -> Self {
836 self.config.max_size = Some(max_size.into());
837 self
838 }
839
840 pub fn min_size(mut self, min_size: impl Into<egui::Vec2>) -> Self {
844 self.config.min_size = min_size.into();
845 self
846 }
847
848 pub fn anchor(mut self, align: egui::Align2, offset: impl Into<egui::Vec2>) -> Self {
850 self.config.anchor = Some((align, offset.into()));
851 self
852 }
853
854 pub const fn resizable(mut self, resizable: bool) -> Self {
856 self.config.resizable = resizable;
857 self
858 }
859
860 pub const fn movable(mut self, movable: bool) -> Self {
864 self.config.movable = movable;
865 self
866 }
867
868 pub const fn title_bar(mut self, title_bar: bool) -> Self {
870 self.config.title_bar = title_bar;
871 self
872 }
873
874 pub const fn show_top_panel(mut self, show_top_panel: bool) -> Self {
877 self.config.show_top_panel = show_top_panel;
878 self
879 }
880
881 pub const fn show_parent_button(mut self, show_parent_button: bool) -> Self {
885 self.config.show_parent_button = show_parent_button;
886 self
887 }
888
889 pub const fn show_back_button(mut self, show_back_button: bool) -> Self {
893 self.config.show_back_button = show_back_button;
894 self
895 }
896
897 pub const fn show_forward_button(mut self, show_forward_button: bool) -> Self {
901 self.config.show_forward_button = show_forward_button;
902 self
903 }
904
905 pub const fn show_new_folder_button(mut self, show_new_folder_button: bool) -> Self {
909 self.config.show_new_folder_button = show_new_folder_button;
910 self
911 }
912
913 pub const fn show_current_path(mut self, show_current_path: bool) -> Self {
917 self.config.show_current_path = show_current_path;
918 self
919 }
920
921 pub const fn show_path_edit_button(mut self, show_path_edit_button: bool) -> Self {
925 self.config.show_path_edit_button = show_path_edit_button;
926 self
927 }
928
929 pub const fn show_menu_button(mut self, show_menu_button: bool) -> Self {
934 self.config.show_menu_button = show_menu_button;
935 self
936 }
937
938 pub const fn show_reload_button(mut self, show_reload_button: bool) -> Self {
943 self.config.show_reload_button = show_reload_button;
944 self
945 }
946
947 pub const fn show_working_directory_button(
954 mut self,
955 show_working_directory_button: bool,
956 ) -> Self {
957 self.config.show_working_directory_button = show_working_directory_button;
958 self
959 }
960
961 pub const fn show_hidden_option(mut self, show_hidden_option: bool) -> Self {
967 self.config.show_hidden_option = show_hidden_option;
968 self
969 }
970
971 pub const fn show_system_files_option(mut self, show_system_files_option: bool) -> Self {
977 self.config.show_system_files_option = show_system_files_option;
978 self
979 }
980
981 pub const fn show_search(mut self, show_search: bool) -> Self {
985 self.config.show_search = show_search;
986 self
987 }
988
989 pub const fn show_left_panel(mut self, show_left_panel: bool) -> Self {
992 self.config.show_left_panel = show_left_panel;
993 self
994 }
995
996 pub const fn show_pinned_folders(mut self, show_pinned_folders: bool) -> Self {
999 self.config.show_pinned_folders = show_pinned_folders;
1000 self
1001 }
1002
1003 pub const fn show_places(mut self, show_places: bool) -> Self {
1008 self.config.show_places = show_places;
1009 self
1010 }
1011
1012 pub const fn show_devices(mut self, show_devices: bool) -> Self {
1017 self.config.show_devices = show_devices;
1018 self
1019 }
1020
1021 pub const fn show_removable_devices(mut self, show_removable_devices: bool) -> Self {
1026 self.config.show_removable_devices = show_removable_devices;
1027 self
1028 }
1029
1030 pub fn picked(&self) -> Option<&Path> {
1038 match &self.state {
1039 DialogState::Picked(path) => Some(path),
1040 _ => None,
1041 }
1042 }
1043
1044 pub fn take_picked(&mut self) -> Option<PathBuf> {
1051 match &mut self.state {
1052 DialogState::Picked(path) => {
1053 let path = std::mem::take(path);
1054 self.state = DialogState::Closed;
1055 Some(path)
1056 }
1057 _ => None,
1058 }
1059 }
1060
1061 pub fn picked_multiple(&self) -> Option<Vec<&Path>> {
1066 match &self.state {
1067 DialogState::PickedMultiple(items) => {
1068 Some(items.iter().map(std::path::PathBuf::as_path).collect())
1069 }
1070 _ => None,
1071 }
1072 }
1073
1074 pub fn take_picked_multiple(&mut self) -> Option<Vec<PathBuf>> {
1081 match &mut self.state {
1082 DialogState::PickedMultiple(items) => {
1083 let items = std::mem::take(items);
1084 self.state = DialogState::Closed;
1085 Some(items)
1086 }
1087 _ => None,
1088 }
1089 }
1090
1091 pub const fn selected_entry(&self) -> Option<&DirectoryEntry> {
1099 self.selected_item.as_ref()
1100 }
1101
1102 pub fn selected_entries(&self) -> impl Iterator<Item = &DirectoryEntry> {
1108 self.get_dir_content_filtered_iter().filter(|p| p.selected)
1109 }
1110
1111 pub fn user_data<U: Any>(&self) -> Option<&U> {
1115 self.user_data.as_ref().and_then(|u| u.downcast_ref())
1116 }
1117
1118 pub fn user_data_mut<U: Any>(&mut self) -> Option<&mut U> {
1122 self.user_data.as_mut().and_then(|u| u.downcast_mut())
1123 }
1124
1125 pub fn set_user_data<U: Any + Send + Sync>(&mut self, user_data: U) {
1149 self.user_data = Some(Box::new(user_data));
1150 }
1151
1152 pub const fn mode(&self) -> DialogMode {
1154 self.mode
1155 }
1156
1157 pub fn state(&self) -> DialogState {
1159 self.state.clone()
1160 }
1161
1162 pub const fn get_window_id(&self) -> egui::Id {
1164 self.window_id
1165 }
1166}
1167
1168impl FileDialog {
1170 fn update_ui(
1174 &mut self,
1175 ctx: &egui::Context,
1176 right_panel_fn: Option<&mut FileDialogUiCallback>,
1177 ) {
1178 let mut is_open = true;
1179
1180 if self.config.as_modal {
1181 let re = self.ui_update_modal_background(ctx);
1182 ctx.move_to_top(re.response.layer_id);
1183 }
1184
1185 let re = self.create_window(&mut is_open).show(ctx, |ui| {
1186 if !self.modals.is_empty() {
1187 self.ui_update_modals(ui);
1188 return;
1189 }
1190
1191 if self.config.show_top_panel {
1192 egui::TopBottomPanel::top(self.window_id.with("top_panel"))
1193 .resizable(false)
1194 .show_inside(ui, |ui| {
1195 self.ui_update_top_panel(ui);
1196 });
1197 }
1198
1199 if self.config.show_left_panel {
1200 egui::SidePanel::left(self.window_id.with("left_panel"))
1201 .resizable(true)
1202 .default_width(150.0)
1203 .width_range(90.0..=250.0)
1204 .show_inside(ui, |ui| {
1205 self.ui_update_left_panel(ui);
1206 });
1207 }
1208
1209 if let Some(f) = right_panel_fn {
1211 let mut right_panel = egui::SidePanel::right(self.window_id.with("right_panel"))
1212 .resizable(true);
1215 if let Some(width) = self.config.right_panel_width {
1216 right_panel = right_panel.default_width(width);
1217 }
1218 right_panel.show_inside(ui, |ui| {
1219 f(ui, self);
1220 });
1221 }
1222
1223 egui::TopBottomPanel::bottom(self.window_id.with("bottom_panel"))
1224 .resizable(false)
1225 .show_inside(ui, |ui| {
1226 self.ui_update_bottom_panel(ui);
1227 });
1228
1229 egui::CentralPanel::default().show_inside(ui, |ui| {
1230 self.ui_update_central_panel(ui);
1231 });
1232 });
1233
1234 if self.config.as_modal {
1235 if let Some(inner_response) = re {
1236 ctx.move_to_top(inner_response.response.layer_id);
1237 }
1238 }
1239
1240 self.any_focused_last_frame = ctx.memory(egui::Memory::focused).is_some();
1241
1242 if !is_open {
1244 self.cancel();
1245 }
1246
1247 let mut repaint = false;
1248
1249 ctx.input(|i| {
1251 if let Some(dropped_file) = i.raw.dropped_files.last() {
1253 if let Some(path) = &dropped_file.path {
1254 if self.config.file_system.is_dir(path) {
1255 self.load_directory(path.as_path());
1257 repaint = true;
1258 } else if let Some(parent) = path.parent() {
1259 self.load_directory(parent);
1261 self.select_item(&mut DirectoryEntry::from_path(
1262 &self.config,
1263 path,
1264 &*self.config.file_system,
1265 ));
1266 self.scroll_to_selection = true;
1267 repaint = true;
1268 }
1269 }
1270 }
1271 });
1272
1273 if repaint {
1275 ctx.request_repaint();
1276 }
1277 }
1278
1279 fn ui_update_modal_background(&self, ctx: &egui::Context) -> egui::InnerResponse<()> {
1281 egui::Area::new(self.window_id.with("modal_overlay"))
1282 .interactable(true)
1283 .fixed_pos(egui::Pos2::ZERO)
1284 .show(ctx, |ui| {
1285 let screen_rect = ctx.input(|i| i.screen_rect);
1286
1287 ui.allocate_response(screen_rect.size(), egui::Sense::click());
1288
1289 ui.painter().rect_filled(
1290 screen_rect,
1291 egui::CornerRadius::ZERO,
1292 self.config.modal_overlay_color,
1293 );
1294 })
1295 }
1296
1297 fn ui_update_modals(&mut self, ui: &mut egui::Ui) {
1298 egui::TopBottomPanel::bottom(self.window_id.with("modal_bottom_panel"))
1303 .resizable(false)
1304 .show_separator_line(false)
1305 .show_inside(ui, |_| {});
1306
1307 egui::CentralPanel::default().show_inside(ui, |ui| {
1310 if let Some(modal) = self.modals.last_mut() {
1311 #[allow(clippy::single_match)]
1312 match modal.update(&self.config, ui) {
1313 ModalState::Close(action) => {
1314 self.exec_modal_action(action);
1315 self.modals.pop();
1316 }
1317 ModalState::Pending => {}
1318 }
1319 }
1320 });
1321 }
1322
1323 fn create_window<'a>(&self, is_open: &'a mut bool) -> egui::Window<'a> {
1325 let mut window = egui::Window::new(self.get_window_title())
1326 .id(self.window_id)
1327 .open(is_open)
1328 .default_size(self.config.default_size)
1329 .min_size(self.config.min_size)
1330 .resizable(self.config.resizable)
1331 .movable(self.config.movable)
1332 .title_bar(self.config.title_bar)
1333 .collapsible(false);
1334
1335 if let Some(pos) = self.config.default_pos {
1336 window = window.default_pos(pos);
1337 }
1338
1339 if let Some(pos) = self.config.fixed_pos {
1340 window = window.fixed_pos(pos);
1341 }
1342
1343 if let Some((anchor, offset)) = self.config.anchor {
1344 window = window.anchor(anchor, offset);
1345 }
1346
1347 if let Some(size) = self.config.max_size {
1348 window = window.max_size(size);
1349 }
1350
1351 window
1352 }
1353
1354 const fn get_window_title(&self) -> &String {
1357 match &self.config.title {
1358 Some(title) => title,
1359 None => match &self.mode {
1360 DialogMode::PickDirectory => &self.config.labels.title_select_directory,
1361 DialogMode::PickFile => &self.config.labels.title_select_file,
1362 DialogMode::PickMultiple => &self.config.labels.title_select_multiple,
1363 DialogMode::SaveFile => &self.config.labels.title_save_file,
1364 },
1365 }
1366 }
1367
1368 fn ui_update_top_panel(&mut self, ui: &mut egui::Ui) {
1371 const BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(25.0, 25.0);
1372
1373 ui.horizontal(|ui| {
1374 self.ui_update_nav_buttons(ui, BUTTON_SIZE);
1375
1376 let mut path_display_width = ui.available_width();
1377
1378 if self.config.show_reload_button {
1380 path_display_width -= ui
1381 .style()
1382 .spacing
1383 .item_spacing
1384 .x
1385 .mul_add(2.5, BUTTON_SIZE.x);
1386 }
1387
1388 if self.config.show_search {
1389 path_display_width -= 140.0;
1390 }
1391
1392 if self.config.show_current_path {
1393 self.ui_update_current_path(ui, path_display_width);
1394 }
1395
1396 if self.config.show_menu_button
1398 && (self.config.show_reload_button
1399 || self.config.show_working_directory_button
1400 || self.config.show_hidden_option
1401 || self.config.show_system_files_option)
1402 {
1403 ui.allocate_ui_with_layout(
1404 BUTTON_SIZE,
1405 egui::Layout::centered_and_justified(egui::Direction::LeftToRight),
1406 |ui| {
1407 ui.menu_button("☰", |ui| {
1408 self.ui_update_hamburger_menu(ui);
1409 });
1410 },
1411 );
1412 }
1413
1414 if self.config.show_search {
1415 self.ui_update_search(ui);
1416 }
1417 });
1418
1419 ui.add_space(ui.ctx().style().spacing.item_spacing.y);
1420 }
1421
1422 fn ui_update_nav_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
1424 if self.config.show_parent_button {
1425 if let Some(x) = self.current_directory() {
1426 if self.ui_button_sized(ui, x.parent().is_some(), button_size, "⏶", None) {
1427 self.load_parent_directory();
1428 }
1429 } else {
1430 let _ = self.ui_button_sized(ui, false, button_size, "⏶", None);
1431 }
1432 }
1433
1434 if self.config.show_back_button
1435 && self.ui_button_sized(
1436 ui,
1437 self.directory_offset + 1 < self.directory_stack.len(),
1438 button_size,
1439 "⏴",
1440 None,
1441 )
1442 {
1443 self.load_previous_directory();
1444 }
1445
1446 if self.config.show_forward_button
1447 && self.ui_button_sized(ui, self.directory_offset != 0, button_size, "⏵", None)
1448 {
1449 self.load_next_directory();
1450 }
1451
1452 if self.config.show_new_folder_button
1453 && self.ui_button_sized(
1454 ui,
1455 !self.create_directory_dialog.is_open(),
1456 button_size,
1457 "+",
1458 None,
1459 )
1460 {
1461 self.open_new_folder_dialog();
1462 }
1463 }
1464
1465 fn ui_update_current_path(&mut self, ui: &mut egui::Ui, width: f32) {
1469 egui::Frame::default()
1470 .stroke(egui::Stroke::new(
1471 1.0,
1472 ui.ctx().style().visuals.window_stroke.color,
1473 ))
1474 .inner_margin(egui::Margin::from(4))
1475 .corner_radius(egui::CornerRadius::from(4))
1476 .show(ui, |ui| {
1477 const EDIT_BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(22.0, 20.0);
1478
1479 if self.path_edit_visible {
1480 self.ui_update_path_edit(ui, width, EDIT_BUTTON_SIZE);
1481 } else {
1482 self.ui_update_path_display(ui, width, EDIT_BUTTON_SIZE);
1483 }
1484 });
1485 }
1486
1487 fn ui_update_path_display(
1489 &mut self,
1490 ui: &mut egui::Ui,
1491 width: f32,
1492 edit_button_size: egui::Vec2,
1493 ) {
1494 ui.style_mut().always_scroll_the_only_direction = true;
1495 ui.style_mut().spacing.scroll.bar_width = 8.0;
1496
1497 let max_width = if self.config.show_path_edit_button {
1498 ui.style()
1499 .spacing
1500 .item_spacing
1501 .x
1502 .mul_add(-2.0, width - edit_button_size.x)
1503 } else {
1504 width
1505 };
1506
1507 egui::ScrollArea::horizontal()
1508 .auto_shrink([false, false])
1509 .stick_to_right(true)
1510 .max_width(max_width)
1511 .show(ui, |ui| {
1512 ui.horizontal(|ui| {
1513 ui.style_mut().spacing.item_spacing.x /= 2.5;
1514 ui.style_mut().spacing.button_padding = egui::Vec2::new(5.0, 3.0);
1515
1516 let mut path = PathBuf::new();
1517
1518 if let Some(data) = self.current_directory().map(Path::to_path_buf) {
1519 for (i, segment) in data.iter().enumerate() {
1520 path.push(segment);
1521
1522 let mut segment_str = segment.to_str().unwrap_or_default().to_string();
1523
1524 if self.is_pinned(&path) {
1525 segment_str =
1526 format!("{} {}", &self.config.pinned_icon, segment_str);
1527 }
1528
1529 if i != 0 {
1530 ui.label(self.config.directory_separator.as_str());
1531 }
1532
1533 let re = ui.button(segment_str);
1534
1535 if re.clicked() {
1536 self.load_directory(path.as_path());
1537 return;
1538 }
1539
1540 self.ui_update_central_panel_path_context_menu(&re, &path.clone());
1541 }
1542 }
1543 });
1544 });
1545
1546 if !self.config.show_path_edit_button {
1547 return;
1548 }
1549
1550 if ui
1551 .add_sized(
1552 edit_button_size,
1553 egui::Button::new("🖊").fill(egui::Color32::TRANSPARENT),
1554 )
1555 .clicked()
1556 {
1557 self.open_path_edit();
1558 }
1559 }
1560
1561 fn ui_update_path_edit(&mut self, ui: &mut egui::Ui, width: f32, edit_button_size: egui::Vec2) {
1563 let desired_width: f32 = ui
1564 .style()
1565 .spacing
1566 .item_spacing
1567 .x
1568 .mul_add(-3.0, width - edit_button_size.x);
1569
1570 let response = egui::TextEdit::singleline(&mut self.path_edit_value)
1571 .desired_width(desired_width)
1572 .show(ui)
1573 .response;
1574
1575 if self.path_edit_activate {
1576 response.request_focus();
1577 Self::set_cursor_to_end(&response, &self.path_edit_value);
1578 self.path_edit_activate = false;
1579 }
1580
1581 if self.path_edit_request_focus {
1582 response.request_focus();
1583 self.path_edit_request_focus = false;
1584 }
1585
1586 let btn_response = ui.add_sized(edit_button_size, egui::Button::new("✔"));
1587
1588 if btn_response.clicked() {
1589 self.submit_path_edit();
1590 }
1591
1592 if !response.has_focus() && !btn_response.contains_pointer() {
1593 self.path_edit_visible = false;
1594 }
1595 }
1596
1597 fn ui_update_hamburger_menu(&mut self, ui: &mut egui::Ui) {
1599 const SEPARATOR_SPACING: f32 = 2.0;
1600
1601 if self.config.show_reload_button && ui.button(&self.config.labels.reload).clicked() {
1602 self.refresh();
1603 ui.close();
1604 }
1605
1606 let working_dir = self.config.file_system.current_dir();
1607
1608 if self.config.show_working_directory_button
1609 && working_dir.is_ok()
1610 && ui.button(&self.config.labels.working_directory).clicked()
1611 {
1612 self.load_directory(&working_dir.unwrap_or_default());
1613 ui.close();
1614 }
1615
1616 if (self.config.show_reload_button || self.config.show_working_directory_button)
1617 && (self.config.show_hidden_option || self.config.show_system_files_option)
1618 {
1619 ui.add_space(SEPARATOR_SPACING);
1620 ui.separator();
1621 ui.add_space(SEPARATOR_SPACING);
1622 }
1623
1624 if self.config.show_hidden_option
1625 && ui
1626 .checkbox(
1627 &mut self.storage.show_hidden,
1628 &self.config.labels.show_hidden,
1629 )
1630 .clicked()
1631 {
1632 self.refresh();
1633 ui.close();
1634 }
1635
1636 if self.config.show_system_files_option
1637 && ui
1638 .checkbox(
1639 &mut self.storage.show_system_files,
1640 &self.config.labels.show_system_files,
1641 )
1642 .clicked()
1643 {
1644 self.refresh();
1645 ui.close();
1646 }
1647 }
1648
1649 fn ui_update_search(&mut self, ui: &mut egui::Ui) {
1651 egui::Frame::default()
1652 .stroke(egui::Stroke::new(
1653 1.0,
1654 ui.ctx().style().visuals.window_stroke.color,
1655 ))
1656 .inner_margin(egui::Margin::symmetric(4, 4))
1657 .corner_radius(egui::CornerRadius::from(4))
1658 .show(ui, |ui| {
1659 ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1660 ui.add_space(ui.ctx().style().spacing.item_spacing.y);
1661
1662 ui.label(egui::RichText::from("🔍").size(15.0));
1663
1664 let re = ui.add_sized(
1665 egui::Vec2::new(ui.available_width(), 0.0),
1666 egui::TextEdit::singleline(&mut self.search_value),
1667 );
1668
1669 self.edit_search_on_text_input(ui);
1670
1671 if re.changed() || self.init_search {
1672 self.selected_item = None;
1673 self.select_first_visible_item();
1674 }
1675
1676 if self.init_search {
1677 re.request_focus();
1678 Self::set_cursor_to_end(&re, &self.search_value);
1679 self.directory_content.reset_multi_selection();
1680
1681 self.init_search = false;
1682 }
1683 });
1684 });
1685 }
1686
1687 fn edit_search_on_text_input(&mut self, ui: &egui::Ui) {
1694 if ui.memory(|mem| mem.focused().is_some()) {
1695 return;
1696 }
1697
1698 ui.input(|inp| {
1699 if inp.modifiers.any() && !inp.modifiers.shift_only() {
1701 return;
1702 }
1703
1704 for text in inp.events.iter().filter_map(|ev| match ev {
1707 egui::Event::Text(t) => Some(t),
1708 _ => None,
1709 }) {
1710 self.search_value.push_str(text);
1711 self.init_search = true;
1712 }
1713 });
1714 }
1715
1716 fn ui_update_left_panel(&mut self, ui: &mut egui::Ui) {
1719 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
1720 const SPACING_MULTIPLIER: f32 = 4.0;
1722
1723 egui::containers::ScrollArea::vertical()
1724 .auto_shrink([false, false])
1725 .show(ui, |ui| {
1726 let mut spacing = ui.ctx().style().spacing.item_spacing.y * 2.0;
1728
1729 if self.config.show_pinned_folders && self.ui_update_pinned_folders(ui, spacing)
1731 {
1732 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1733 }
1734
1735 let quick_accesses = std::mem::take(&mut self.config.quick_accesses);
1737
1738 for quick_access in &quick_accesses {
1739 ui.add_space(spacing);
1740 self.ui_update_quick_access(ui, quick_access);
1741 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1742 }
1743
1744 self.config.quick_accesses = quick_accesses;
1745
1746 if self.config.show_places && self.ui_update_user_directories(ui, spacing) {
1748 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1749 }
1750
1751 let disks = std::mem::take(&mut self.system_disks);
1752
1753 if self.config.show_devices && self.ui_update_devices(ui, spacing, &disks) {
1754 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1755 }
1756
1757 if self.config.show_removable_devices
1758 && self.ui_update_removable_devices(ui, spacing, &disks)
1759 {
1760 }
1763
1764 self.system_disks = disks;
1765 });
1766 });
1767 }
1768
1769 fn ui_update_left_panel_entry(
1773 &mut self,
1774 ui: &mut egui::Ui,
1775 display_name: &str,
1776 path: &Path,
1777 ) -> egui::Response {
1778 let response = ui.selectable_label(self.current_directory() == Some(path), display_name);
1779
1780 if response.clicked() {
1781 self.load_directory(path);
1782 }
1783
1784 response
1785 }
1786
1787 fn ui_update_quick_access(&mut self, ui: &mut egui::Ui, quick_access: &QuickAccess) {
1789 ui.label(&quick_access.heading);
1790
1791 for entry in &quick_access.paths {
1792 self.ui_update_left_panel_entry(ui, &entry.display_name, &entry.path);
1793 }
1794 }
1795
1796 fn ui_update_pinned_folders(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1801 let mut visible = false;
1802
1803 for (i, pinned) in self.storage.pinned_folders.clone().iter().enumerate() {
1804 if i == 0 {
1805 ui.add_space(spacing);
1806 ui.label(self.config.labels.heading_pinned.as_str());
1807
1808 visible = true;
1809 }
1810
1811 if self.is_pinned_folder_being_renamed(pinned) {
1812 self.ui_update_pinned_folder_rename(ui);
1813 continue;
1814 }
1815
1816 let response = self.ui_update_left_panel_entry(
1817 ui,
1818 &format!("{} {}", self.config.pinned_icon, &pinned.label),
1819 pinned.path.as_path(),
1820 );
1821
1822 self.ui_update_pinned_folder_context_menu(&response, pinned);
1823 }
1824
1825 visible
1826 }
1827
1828 fn ui_update_pinned_folder_rename(&mut self, ui: &mut egui::Ui) {
1829 if let Some(r) = &mut self.rename_pinned_folder {
1830 let id = self.window_id.with("pinned_folder_rename").with(&r.path);
1831 let mut output = egui::TextEdit::singleline(&mut r.label)
1832 .id(id)
1833 .cursor_at_end(true)
1834 .show(ui);
1835
1836 if self.rename_pinned_folder_request_focus {
1837 output.state.cursor.set_char_range(Some(CCursorRange::two(
1838 CCursor::new(0),
1839 CCursor::new(r.label.chars().count()),
1840 )));
1841 output.state.store(ui.ctx(), output.response.id);
1842
1843 output.response.request_focus();
1844
1845 self.rename_pinned_folder_request_focus = false;
1846 }
1847
1848 if output.response.lost_focus() {
1849 self.end_rename_pinned_folder();
1850 }
1851 }
1852 }
1853
1854 fn ui_update_pinned_folder_context_menu(
1855 &mut self,
1856 item: &egui::Response,
1857 pinned: &PinnedFolder,
1858 ) {
1859 item.context_menu(|ui| {
1860 if ui.button(&self.config.labels.unpin_folder).clicked() {
1861 self.unpin_path(&pinned.path);
1862 ui.close();
1863 }
1864
1865 if ui
1866 .button(&self.config.labels.rename_pinned_folder)
1867 .clicked()
1868 {
1869 self.begin_rename_pinned_folder(pinned.clone());
1870 ui.close();
1871 }
1872 });
1873 }
1874
1875 fn ui_update_user_directories(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1880 let user_directories = std::mem::take(&mut self.user_directories);
1884 let labels = std::mem::take(&mut self.config.labels);
1885
1886 let mut visible = false;
1887
1888 if let Some(dirs) = &user_directories {
1889 ui.add_space(spacing);
1890 ui.label(labels.heading_places.as_str());
1891
1892 if let Some(path) = dirs.home_dir() {
1893 self.ui_update_left_panel_entry(ui, &labels.home_dir, path);
1894 }
1895 if let Some(path) = dirs.desktop_dir() {
1896 self.ui_update_left_panel_entry(ui, &labels.desktop_dir, path);
1897 }
1898 if let Some(path) = dirs.document_dir() {
1899 self.ui_update_left_panel_entry(ui, &labels.documents_dir, path);
1900 }
1901 if let Some(path) = dirs.download_dir() {
1902 self.ui_update_left_panel_entry(ui, &labels.downloads_dir, path);
1903 }
1904 if let Some(path) = dirs.audio_dir() {
1905 self.ui_update_left_panel_entry(ui, &labels.audio_dir, path);
1906 }
1907 if let Some(path) = dirs.picture_dir() {
1908 self.ui_update_left_panel_entry(ui, &labels.pictures_dir, path);
1909 }
1910 if let Some(path) = dirs.video_dir() {
1911 self.ui_update_left_panel_entry(ui, &labels.videos_dir, path);
1912 }
1913
1914 visible = true;
1915 }
1916
1917 self.user_directories = user_directories;
1918 self.config.labels = labels;
1919
1920 visible
1921 }
1922
1923 fn ui_update_devices(&mut self, ui: &mut egui::Ui, spacing: f32, disks: &Disks) -> bool {
1928 let mut visible = false;
1929
1930 for (i, disk) in disks.iter().filter(|x| !x.is_removable()).enumerate() {
1931 if i == 0 {
1932 ui.add_space(spacing);
1933 ui.label(self.config.labels.heading_devices.as_str());
1934
1935 visible = true;
1936 }
1937
1938 self.ui_update_device_entry(ui, disk);
1939 }
1940
1941 visible
1942 }
1943
1944 fn ui_update_removable_devices(
1949 &mut self,
1950 ui: &mut egui::Ui,
1951 spacing: f32,
1952 disks: &Disks,
1953 ) -> bool {
1954 let mut visible = false;
1955
1956 for (i, disk) in disks.iter().filter(|x| x.is_removable()).enumerate() {
1957 if i == 0 {
1958 ui.add_space(spacing);
1959 ui.label(self.config.labels.heading_removable_devices.as_str());
1960
1961 visible = true;
1962 }
1963
1964 self.ui_update_device_entry(ui, disk);
1965 }
1966
1967 visible
1968 }
1969
1970 fn ui_update_device_entry(&mut self, ui: &mut egui::Ui, device: &Disk) {
1972 let label = if device.is_removable() {
1973 format!(
1974 "{} {}",
1975 self.config.removable_device_icon,
1976 device.display_name()
1977 )
1978 } else {
1979 format!("{} {}", self.config.device_icon, device.display_name())
1980 };
1981
1982 self.ui_update_left_panel_entry(ui, &label, device.mount_point());
1983 }
1984
1985 fn ui_update_bottom_panel(&mut self, ui: &mut egui::Ui) {
1987 const BUTTON_HEIGHT: f32 = 20.0;
1988 ui.add_space(5.0);
1989
1990 let label_submit_width = match self.mode {
1992 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
1993 Self::calc_text_width(ui, &self.config.labels.open_button)
1994 }
1995 DialogMode::SaveFile => Self::calc_text_width(ui, &self.config.labels.save_button),
1996 };
1997
1998 let mut btn_width = Self::calc_text_width(ui, &self.config.labels.cancel_button);
1999 if label_submit_width > btn_width {
2000 btn_width = label_submit_width;
2001 }
2002
2003 btn_width += ui.spacing().button_padding.x * 4.0;
2004
2005 let button_size: egui::Vec2 = egui::Vec2::new(btn_width, BUTTON_HEIGHT);
2007
2008 self.ui_update_selection_preview(ui, button_size);
2009
2010 if self.mode == DialogMode::SaveFile && self.config.save_extensions.is_empty() {
2011 ui.add_space(ui.style().spacing.item_spacing.y);
2012 }
2013
2014 self.ui_update_action_buttons(ui, button_size);
2015 }
2016
2017 fn ui_update_selection_preview(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2019 const SELECTION_PREVIEW_MIN_WIDTH: f32 = 50.0;
2020 let item_spacing = ui.style().spacing.item_spacing;
2021
2022 let render_filter_selection = (!self.config.file_filters.is_empty()
2023 && (self.mode == DialogMode::PickFile || self.mode == DialogMode::PickMultiple))
2024 || (!self.config.save_extensions.is_empty() && self.mode == DialogMode::SaveFile);
2025
2026 let filter_selection_width = button_size.x.mul_add(2.0, item_spacing.x);
2027 let mut filter_selection_separate_line = false;
2028
2029 ui.horizontal(|ui| {
2030 match &self.mode {
2031 DialogMode::PickDirectory => ui.label(&self.config.labels.selected_directory),
2032 DialogMode::PickFile => ui.label(&self.config.labels.selected_file),
2033 DialogMode::PickMultiple => ui.label(&self.config.labels.selected_items),
2034 DialogMode::SaveFile => ui.label(&self.config.labels.file_name),
2035 };
2036
2037 let mut scroll_bar_width: f32 =
2042 ui.available_width() - filter_selection_width - item_spacing.x;
2043
2044 if scroll_bar_width < SELECTION_PREVIEW_MIN_WIDTH || !render_filter_selection {
2045 filter_selection_separate_line = true;
2046 scroll_bar_width = ui.available_width();
2047 }
2048
2049 match &self.mode {
2050 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2051 use egui::containers::scroll_area::ScrollBarVisibility;
2052
2053 let text = self.get_selection_preview_text();
2054
2055 egui::containers::ScrollArea::horizontal()
2056 .auto_shrink([false, false])
2057 .max_width(scroll_bar_width)
2058 .stick_to_right(true)
2059 .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
2060 .show(ui, |ui| {
2061 ui.colored_label(ui.style().visuals.selection.bg_fill, text);
2062 });
2063 }
2064 DialogMode::SaveFile => {
2065 let mut output = egui::TextEdit::singleline(&mut self.file_name_input)
2066 .cursor_at_end(false)
2067 .margin(egui::Margin::symmetric(4, 3))
2068 .desired_width(scroll_bar_width - item_spacing.x)
2069 .show(ui);
2070
2071 if self.file_name_input_request_focus {
2072 self.highlight_file_name_input(&mut output);
2073 output.state.store(ui.ctx(), output.response.id);
2074
2075 output.response.request_focus();
2076 self.file_name_input_request_focus = false;
2077 }
2078
2079 if output.response.changed() {
2080 self.file_name_input_error = self.validate_file_name_input();
2081 }
2082
2083 if output.response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
2084 {
2085 self.submit();
2086 }
2087 }
2088 }
2089
2090 if !filter_selection_separate_line && render_filter_selection {
2091 if self.mode == DialogMode::SaveFile {
2092 self.ui_update_save_extension_selection(ui, filter_selection_width);
2093 } else {
2094 self.ui_update_file_filter_selection(ui, filter_selection_width);
2095 }
2096 }
2097 });
2098
2099 if filter_selection_separate_line && render_filter_selection {
2100 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2101 if self.mode == DialogMode::SaveFile {
2102 self.ui_update_save_extension_selection(ui, filter_selection_width);
2103 } else {
2104 self.ui_update_file_filter_selection(ui, filter_selection_width);
2105 }
2106 });
2107 }
2108 }
2109
2110 fn highlight_file_name_input(&self, output: &mut egui::text_edit::TextEditOutput) {
2114 if let Some(pos) = self.file_name_input.rfind('.') {
2115 let range = if pos == 0 {
2116 CCursorRange::two(CCursor::new(0), CCursor::new(0))
2117 } else {
2118 CCursorRange::two(CCursor::new(0), CCursor::new(pos))
2119 };
2120
2121 output.state.cursor.set_char_range(Some(range));
2122 }
2123 }
2124
2125 fn get_selection_preview_text(&self) -> String {
2126 if self.is_selection_valid() {
2127 match &self.mode {
2128 DialogMode::PickDirectory | DialogMode::PickFile => self
2129 .selected_item
2130 .as_ref()
2131 .map_or_else(String::new, |item| item.file_name().to_string()),
2132 DialogMode::PickMultiple => {
2133 let mut result = String::new();
2134
2135 for (i, item) in self
2136 .get_dir_content_filtered_iter()
2137 .filter(|p| p.selected)
2138 .enumerate()
2139 {
2140 if i == 0 {
2141 result += item.file_name();
2142 continue;
2143 }
2144
2145 result += format!(", {}", item.file_name()).as_str();
2146 }
2147
2148 result
2149 }
2150 DialogMode::SaveFile => String::new(),
2151 }
2152 } else {
2153 String::new()
2154 }
2155 }
2156
2157 fn ui_update_file_filter_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2158 let selected_filter = self.get_selected_file_filter();
2159 let selected_text = match selected_filter {
2160 Some(f) => &f.name,
2161 None => &self.config.labels.file_filter_all_files,
2162 };
2163
2164 let mut select_filter: Option<Option<FileFilter>> = None;
2167
2168 egui::containers::ComboBox::from_id_salt(self.window_id.with("file_filter_selection"))
2169 .width(width)
2170 .selected_text(selected_text)
2171 .wrap_mode(egui::TextWrapMode::Truncate)
2172 .show_ui(ui, |ui| {
2173 for filter in &self.config.file_filters {
2174 let selected = selected_filter.is_some_and(|f| f.id == filter.id);
2175
2176 if ui.selectable_label(selected, &filter.name).clicked() {
2177 select_filter = Some(Some(filter.clone()));
2178 }
2179 }
2180
2181 if ui
2182 .selectable_label(
2183 selected_filter.is_none(),
2184 &self.config.labels.file_filter_all_files,
2185 )
2186 .clicked()
2187 {
2188 select_filter = Some(None);
2189 }
2190 });
2191
2192 if let Some(i) = select_filter {
2193 self.select_file_filter(i);
2194 }
2195 }
2196
2197 fn ui_update_save_extension_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2198 let selected_extension = self.get_selected_save_extension();
2199 let selected_text = match selected_extension {
2200 Some(e) => &e.to_string(),
2201 None => &self.config.labels.save_extension_any,
2202 };
2203
2204 let mut select_extension: Option<Option<SaveExtension>> = None;
2207
2208 egui::containers::ComboBox::from_id_salt(self.window_id.with("save_extension_selection"))
2209 .width(width)
2210 .selected_text(selected_text)
2211 .wrap_mode(egui::TextWrapMode::Truncate)
2212 .show_ui(ui, |ui| {
2213 for extension in &self.config.save_extensions {
2214 let selected = selected_extension.is_some_and(|s| s.id == extension.id);
2215
2216 if ui
2217 .selectable_label(selected, extension.to_string())
2218 .clicked()
2219 {
2220 select_extension = Some(Some(extension.clone()));
2221 }
2222 }
2223 });
2224
2225 if let Some(i) = select_extension {
2226 self.file_name_input_request_focus = true;
2227 self.select_save_extension(i);
2228 }
2229 }
2230
2231 fn ui_update_action_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2233 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2234 let label = match &self.mode {
2235 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2236 self.config.labels.open_button.as_str()
2237 }
2238 DialogMode::SaveFile => self.config.labels.save_button.as_str(),
2239 };
2240
2241 if self.ui_button_sized(
2242 ui,
2243 self.is_selection_valid(),
2244 button_size,
2245 label,
2246 self.file_name_input_error.as_deref(),
2247 ) {
2248 self.submit();
2249 }
2250
2251 if ui
2252 .add_sized(
2253 button_size,
2254 egui::Button::new(self.config.labels.cancel_button.as_str()),
2255 )
2256 .clicked()
2257 {
2258 self.cancel();
2259 }
2260 });
2261 }
2262
2263 fn ui_update_central_panel(&mut self, ui: &mut egui::Ui) {
2266 if self.update_directory_content(ui) {
2267 return;
2268 }
2269
2270 self.ui_update_central_panel_content(ui);
2271 }
2272
2273 fn update_directory_content(&mut self, ui: &mut egui::Ui) -> bool {
2278 const SHOW_SPINNER_AFTER: f32 = 0.2;
2279
2280 match self.directory_content.update() {
2281 DirectoryContentState::Pending(timestamp) => {
2282 let now = std::time::SystemTime::now();
2283
2284 if now
2285 .duration_since(*timestamp)
2286 .unwrap_or_default()
2287 .as_secs_f32()
2288 > SHOW_SPINNER_AFTER
2289 {
2290 ui.centered_and_justified(egui::Ui::spinner);
2291 }
2292
2293 ui.ctx().request_repaint();
2295
2296 true
2297 }
2298 DirectoryContentState::Errored(err) => {
2299 ui.centered_and_justified(|ui| ui.colored_label(ui.visuals().error_fg_color, err));
2300 true
2301 }
2302 DirectoryContentState::Finished => {
2303 if self.mode == DialogMode::PickDirectory {
2304 if let Some(dir) = self.current_directory() {
2305 let mut dir_entry =
2306 DirectoryEntry::from_path(&self.config, dir, &*self.config.file_system);
2307 self.select_item(&mut dir_entry);
2308 }
2309 }
2310
2311 false
2312 }
2313 DirectoryContentState::Success => false,
2314 }
2315 }
2316
2317 fn ui_update_central_panel_content(&mut self, ui: &mut egui::Ui) {
2320 let mut data = std::mem::take(&mut self.directory_content);
2322
2323 let mut reset_multi_selection = false;
2326
2327 let mut batch_select_item_b: Option<DirectoryEntry> = None;
2330
2331 let mut should_return = false;
2333
2334 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
2335 let scroll_area = egui::containers::ScrollArea::vertical().auto_shrink([false, false]);
2336
2337 if self.search_value.is_empty()
2338 && !self.create_directory_dialog.is_open()
2339 && !self.scroll_to_selection
2340 {
2341 scroll_area.show_rows(ui, ui.spacing().interact_size.y, data.len(), |ui, range| {
2345 for item in data.iter_range_mut(range) {
2346 if self.ui_update_central_panel_entry(
2347 ui,
2348 item,
2349 &mut reset_multi_selection,
2350 &mut batch_select_item_b,
2351 ) {
2352 should_return = true;
2353 }
2354 }
2355 });
2356 } else {
2357 scroll_area.show(ui, |ui| {
2363 for item in data.filtered_iter_mut(&self.search_value.clone()) {
2364 if self.ui_update_central_panel_entry(
2365 ui,
2366 item,
2367 &mut reset_multi_selection,
2368 &mut batch_select_item_b,
2369 ) {
2370 should_return = true;
2371 }
2372 }
2373
2374 if let Some(entry) = self.ui_update_create_directory_dialog(ui) {
2375 data.push(entry);
2376 }
2377 });
2378 }
2379 });
2380
2381 if should_return {
2382 return;
2383 }
2384
2385 if reset_multi_selection {
2387 for item in data.filtered_iter_mut(&self.search_value) {
2388 if let Some(selected_item) = &self.selected_item {
2389 if selected_item.path_eq(item) {
2390 continue;
2391 }
2392 }
2393
2394 item.selected = false;
2395 }
2396 }
2397
2398 if let Some(item_b) = batch_select_item_b {
2400 if let Some(item_a) = &self.selected_item {
2401 self.batch_select_between(&mut data, item_a, &item_b);
2402 }
2403 }
2404
2405 self.directory_content = data;
2406 self.scroll_to_selection = false;
2407 }
2408
2409 fn ui_update_central_panel_entry(
2412 &mut self,
2413 ui: &mut egui::Ui,
2414 item: &mut DirectoryEntry,
2415 reset_multi_selection: &mut bool,
2416 batch_select_item_b: &mut Option<DirectoryEntry>,
2417 ) -> bool {
2418 let file_name = item.file_name();
2419 let primary_selected = self.is_primary_selected(item);
2420 let pinned = self.is_pinned(item.as_path());
2421
2422 let icons = if pinned {
2423 format!("{} {} ", item.icon(), self.config.pinned_icon)
2424 } else {
2425 format!("{} ", item.icon())
2426 };
2427
2428 let icons_width = Self::calc_text_width(ui, &icons);
2429
2430 let available_width = ui.available_width() - icons_width - 15.0;
2432
2433 let truncate = self.config.truncate_filenames
2434 && available_width < Self::calc_text_width(ui, file_name);
2435
2436 let text = if truncate {
2437 Self::truncate_filename(ui, item, available_width)
2438 } else {
2439 file_name.to_owned()
2440 };
2441
2442 let mut re =
2443 ui.selectable_label(primary_selected || item.selected, format!("{icons}{text}"));
2444
2445 if truncate {
2446 re = re.on_hover_text(file_name);
2447 }
2448
2449 if item.is_dir() {
2450 self.ui_update_central_panel_path_context_menu(&re, item.as_path());
2451
2452 if re.context_menu_opened() {
2453 self.select_item(item);
2454 }
2455 }
2456
2457 if primary_selected && self.scroll_to_selection {
2458 re.scroll_to_me(Some(egui::Align::Center));
2459 self.scroll_to_selection = false;
2460 }
2461
2462 if re.clicked()
2464 && !ui.input(|i| i.modifiers.command)
2465 && !ui.input(|i| i.modifiers.shift_only())
2466 {
2467 self.select_item(item);
2468
2469 if self.mode == DialogMode::PickMultiple {
2471 *reset_multi_selection = true;
2472 }
2473 }
2474
2475 if self.mode == DialogMode::PickMultiple
2478 && re.clicked()
2479 && ui.input(|i| i.modifiers.command)
2480 {
2481 if primary_selected {
2482 item.selected = false;
2485 self.selected_item = None;
2486 } else {
2487 item.selected = !item.selected;
2488
2489 if item.selected {
2491 self.select_item(item);
2492 }
2493 }
2494 }
2495
2496 if self.mode == DialogMode::PickMultiple
2499 && re.clicked()
2500 && ui.input(|i| i.modifiers.shift_only())
2501 {
2502 if let Some(selected_item) = self.selected_item.clone() {
2503 *batch_select_item_b = Some(selected_item);
2506
2507 item.selected = true;
2509 self.select_item(item);
2510 }
2511 }
2512
2513 if re.double_clicked() && !ui.input(|i| i.modifiers.command) {
2516 if item.is_dir() {
2517 self.load_directory(&item.to_path_buf());
2518 return true;
2519 }
2520
2521 self.select_item(item);
2522
2523 self.submit();
2524 }
2525
2526 false
2527 }
2528
2529 fn ui_update_create_directory_dialog(&mut self, ui: &mut egui::Ui) -> Option<DirectoryEntry> {
2530 self.create_directory_dialog
2531 .update(ui, &self.config)
2532 .directory()
2533 .map(|path| self.process_new_folder(&path))
2534 }
2535
2536 fn batch_select_between(
2539 &self,
2540 directory_content: &mut DirectoryContent,
2541 item_a: &DirectoryEntry,
2542 item_b: &DirectoryEntry,
2543 ) {
2544 let pos_a = directory_content
2546 .filtered_iter(&self.search_value)
2547 .position(|p| p.path_eq(item_a));
2548 let pos_b = directory_content
2549 .filtered_iter(&self.search_value)
2550 .position(|p| p.path_eq(item_b));
2551
2552 if let Some(pos_a) = pos_a {
2555 if let Some(pos_b) = pos_b {
2556 if pos_a == pos_b {
2557 return;
2558 }
2559
2560 let mut min = pos_a;
2563 let mut max = pos_b;
2564
2565 if min > max {
2566 min = pos_b;
2567 max = pos_a;
2568 }
2569
2570 for item in directory_content
2571 .filtered_iter_mut(&self.search_value)
2572 .enumerate()
2573 .filter(|(i, _)| i > &min && i < &max)
2574 .map(|(_, p)| p)
2575 {
2576 item.selected = true;
2577 }
2578 }
2579 }
2580 }
2581
2582 fn ui_button_sized(
2584 &self,
2585 ui: &mut egui::Ui,
2586 enabled: bool,
2587 size: egui::Vec2,
2588 label: &str,
2589 err_tooltip: Option<&str>,
2590 ) -> bool {
2591 let mut clicked = false;
2592
2593 ui.add_enabled_ui(enabled, |ui| {
2594 let response = ui.add_sized(size, egui::Button::new(label));
2595 clicked = response.clicked();
2596
2597 if let Some(err) = err_tooltip {
2598 response.on_disabled_hover_ui(|ui| {
2599 ui.horizontal_wrapped(|ui| {
2600 ui.spacing_mut().item_spacing.x = 0.0;
2601
2602 ui.colored_label(
2603 ui.ctx().style().visuals.error_fg_color,
2604 format!("{} ", self.config.err_icon),
2605 );
2606
2607 ui.label(err);
2608 });
2609 });
2610 }
2611 });
2612
2613 clicked
2614 }
2615
2616 fn ui_update_central_panel_path_context_menu(&mut self, item: &egui::Response, path: &Path) {
2623 if !self.config.show_pinned_folders {
2625 return;
2626 }
2627
2628 item.context_menu(|ui| {
2629 let pinned = self.is_pinned(path);
2630
2631 if pinned {
2632 if ui.button(&self.config.labels.unpin_folder).clicked() {
2633 self.unpin_path(path);
2634 ui.close();
2635 }
2636 } else if ui.button(&self.config.labels.pin_folder).clicked() {
2637 self.pin_path(path.to_path_buf());
2638 ui.close();
2639 }
2640 });
2641 }
2642
2643 fn set_cursor_to_end(re: &egui::Response, data: &str) {
2650 if let Some(mut state) = egui::TextEdit::load_state(&re.ctx, re.id) {
2652 state
2653 .cursor
2654 .set_char_range(Some(CCursorRange::one(CCursor::new(data.len()))));
2655 state.store(&re.ctx, re.id);
2656 }
2657 }
2658
2659 fn calc_char_width(ui: &egui::Ui, char: char) -> f32 {
2661 ui.fonts(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char))
2662 }
2663
2664 fn calc_text_width(ui: &egui::Ui, text: &str) -> f32 {
2667 let mut width = 0.0;
2668
2669 for char in text.chars() {
2670 width += Self::calc_char_width(ui, char);
2671 }
2672
2673 width
2674 }
2675
2676 fn truncate_filename(ui: &egui::Ui, item: &DirectoryEntry, max_length: f32) -> String {
2677 const TRUNCATE_STR: &str = "...";
2678
2679 let path = item.as_path();
2680
2681 let file_stem = if item.is_file() {
2682 path.file_stem().and_then(|f| f.to_str()).unwrap_or("")
2683 } else {
2684 item.file_name()
2685 };
2686
2687 let extension = if item.is_file() {
2688 path.extension().map_or(String::new(), |ext| {
2689 format!(".{}", ext.to_str().unwrap_or(""))
2690 })
2691 } else {
2692 String::new()
2693 };
2694
2695 let extension_width = Self::calc_text_width(ui, &extension);
2696 let reserved = extension_width + Self::calc_text_width(ui, TRUNCATE_STR);
2697
2698 if max_length <= reserved {
2699 return format!("{TRUNCATE_STR}{extension}");
2700 }
2701
2702 let mut width = reserved;
2703 let mut front = String::new();
2704 let mut back = String::new();
2705
2706 for (i, char) in file_stem.chars().enumerate() {
2707 let w = Self::calc_char_width(ui, char);
2708
2709 if width + w > max_length {
2710 break;
2711 }
2712
2713 front.push(char);
2714 width += w;
2715
2716 let back_index = file_stem.len() - i - 1;
2717
2718 if back_index <= i {
2719 break;
2720 }
2721
2722 if let Some(char) = file_stem.chars().nth(back_index) {
2723 let w = Self::calc_char_width(ui, char);
2724
2725 if width + w > max_length {
2726 break;
2727 }
2728
2729 back.push(char);
2730 width += w;
2731 }
2732 }
2733
2734 format!(
2735 "{front}{TRUNCATE_STR}{}{extension}",
2736 back.chars().rev().collect::<String>()
2737 )
2738 }
2739}
2740
2741impl FileDialog {
2743 fn update_keybindings(&mut self, ctx: &egui::Context) {
2745 if let Some(modal) = self.modals.last_mut() {
2748 modal.update_keybindings(&self.config, ctx);
2749 return;
2750 }
2751
2752 let keybindings = std::mem::take(&mut self.config.keybindings);
2753
2754 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.submit, false) {
2755 self.exec_keybinding_submit();
2756 }
2757
2758 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.cancel, false) {
2759 self.exec_keybinding_cancel();
2760 }
2761
2762 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.parent, true) {
2763 self.load_parent_directory();
2764 }
2765
2766 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.back, true) {
2767 self.load_previous_directory();
2768 }
2769
2770 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.forward, true) {
2771 self.load_next_directory();
2772 }
2773
2774 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.reload, true) {
2775 self.refresh();
2776 }
2777
2778 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.new_folder, true) {
2779 self.open_new_folder_dialog();
2780 }
2781
2782 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.edit_path, true) {
2783 self.open_path_edit();
2784 }
2785
2786 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.home_edit_path, true) {
2787 if let Some(dirs) = &self.user_directories {
2788 if let Some(home) = dirs.home_dir() {
2789 self.load_directory(home.to_path_buf().as_path());
2790 self.open_path_edit();
2791 }
2792 }
2793 }
2794
2795 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_up, false) {
2796 self.exec_keybinding_selection_up();
2797
2798 if let Some(id) = ctx.memory(egui::Memory::focused) {
2800 ctx.memory_mut(|w| w.surrender_focus(id));
2801 }
2802 }
2803
2804 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_down, false) {
2805 self.exec_keybinding_selection_down();
2806
2807 if let Some(id) = ctx.memory(egui::Memory::focused) {
2809 ctx.memory_mut(|w| w.surrender_focus(id));
2810 }
2811 }
2812
2813 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.select_all, true)
2814 && self.mode == DialogMode::PickMultiple
2815 {
2816 for item in self.directory_content.filtered_iter_mut(&self.search_value) {
2817 item.selected = true;
2818 }
2819 }
2820
2821 self.config.keybindings = keybindings;
2822 }
2823
2824 fn exec_keybinding_submit(&mut self) {
2826 if self.path_edit_visible {
2827 self.submit_path_edit();
2828 return;
2829 }
2830
2831 if self.create_directory_dialog.is_open() {
2832 if let Some(dir) = self.create_directory_dialog.submit().directory() {
2833 self.process_new_folder(&dir);
2834 }
2835 return;
2836 }
2837
2838 if self.any_focused_last_frame {
2839 return;
2840 }
2841
2842 if let Some(item) = &self.selected_item {
2844 let is_visible = self
2846 .get_dir_content_filtered_iter()
2847 .any(|p| p.path_eq(item));
2848
2849 if is_visible && item.is_dir() {
2850 self.load_directory(&item.to_path_buf());
2851 return;
2852 }
2853 }
2854
2855 self.submit();
2856 }
2857
2858 fn exec_keybinding_cancel(&mut self) {
2860 if self.create_directory_dialog.is_open() {
2878 self.create_directory_dialog.close();
2879 } else if self.path_edit_visible {
2880 self.close_path_edit();
2881 } else if !self.any_focused_last_frame {
2882 self.cancel();
2883 return;
2884 }
2885 }
2886
2887 fn exec_keybinding_selection_up(&mut self) {
2889 if self.directory_content.len() == 0 {
2890 return;
2891 }
2892
2893 self.directory_content.reset_multi_selection();
2894
2895 if let Some(item) = &self.selected_item {
2896 if self.select_next_visible_item_before(&item.clone()) {
2897 return;
2898 }
2899 }
2900
2901 self.select_last_visible_item();
2904 }
2905
2906 fn exec_keybinding_selection_down(&mut self) {
2908 if self.directory_content.len() == 0 {
2909 return;
2910 }
2911
2912 self.directory_content.reset_multi_selection();
2913
2914 if let Some(item) = &self.selected_item {
2915 if self.select_next_visible_item_after(&item.clone()) {
2916 return;
2917 }
2918 }
2919
2920 self.select_first_visible_item();
2923 }
2924}
2925
2926impl FileDialog {
2928 fn get_selected_file_filter(&self) -> Option<&FileFilter> {
2930 self.selected_file_filter
2931 .and_then(|id| self.config.file_filters.iter().find(|p| p.id == id))
2932 }
2933
2934 fn set_default_file_filter(&mut self) {
2936 if let Some(name) = &self.config.default_file_filter {
2937 for filter in &self.config.file_filters {
2938 if filter.name == name.as_str() {
2939 self.selected_file_filter = Some(filter.id);
2940 }
2941 }
2942 }
2943 }
2944
2945 fn select_file_filter(&mut self, filter: Option<FileFilter>) {
2947 self.selected_file_filter = filter.map(|f| f.id);
2948 self.selected_item = None;
2949 self.refresh();
2950 }
2951
2952 fn get_selected_save_extension(&self) -> Option<&SaveExtension> {
2954 self.selected_save_extension
2955 .and_then(|id| self.config.save_extensions.iter().find(|p| p.id == id))
2956 }
2957
2958 fn set_default_save_extension(&mut self) {
2960 let config = std::mem::take(&mut self.config);
2961
2962 if let Some(name) = &config.default_save_extension {
2963 for extension in &config.save_extensions {
2964 if extension.name == name.as_str() {
2965 self.selected_save_extension = Some(extension.id);
2966 self.set_file_name_extension(&extension.file_extension);
2967 }
2968 }
2969 }
2970
2971 self.config = config;
2972 }
2973
2974 fn select_save_extension(&mut self, extension: Option<SaveExtension>) {
2976 if let Some(ex) = extension {
2977 self.selected_save_extension = Some(ex.id);
2978 self.set_file_name_extension(&ex.file_extension);
2979 }
2980
2981 self.selected_item = None;
2982 self.refresh();
2983 }
2984
2985 fn set_file_name_extension(&mut self, extension: &str) {
2987 let dot_count = self.file_name_input.chars().filter(|c| *c == '.').count();
2991 let use_simple = dot_count == 1 && self.file_name_input.chars().nth(0) == Some('.');
2992
2993 let mut p = PathBuf::from(&self.file_name_input);
2994 if !use_simple && p.set_extension(extension) {
2995 self.file_name_input = p.to_string_lossy().into_owned();
2996 } else {
2997 self.file_name_input = format!(".{extension}");
2998 }
2999 }
3000
3001 fn get_dir_content_filtered_iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
3003 self.directory_content.filtered_iter(&self.search_value)
3004 }
3005
3006 fn open_new_folder_dialog(&mut self) {
3008 if let Some(x) = self.current_directory() {
3009 self.create_directory_dialog.open(x.to_path_buf());
3010 }
3011 }
3012
3013 fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry {
3015 let mut entry =
3016 DirectoryEntry::from_path(&self.config, created_dir, &*self.config.file_system);
3017
3018 self.directory_content.push(entry.clone());
3019
3020 self.select_item(&mut entry);
3021
3022 entry
3023 }
3024
3025 fn open_modal(&mut self, modal: Box<dyn FileDialogModal + Send + Sync>) {
3027 self.modals.push(modal);
3028 }
3029
3030 fn exec_modal_action(&mut self, action: ModalAction) {
3032 match action {
3033 ModalAction::None => {}
3034 ModalAction::SaveFile(path) => self.state = DialogState::Picked(path),
3035 }
3036 }
3037
3038 fn canonicalize_path(&self, path: &Path) -> PathBuf {
3041 if self.config.canonicalize_paths {
3042 dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
3043 } else {
3044 path.to_path_buf()
3045 }
3046 }
3047
3048 fn pin_path(&mut self, path: PathBuf) {
3050 let pinned = PinnedFolder::from_path(path);
3051 self.storage.pinned_folders.push(pinned);
3052 }
3053
3054 fn unpin_path(&mut self, path: &Path) {
3056 self.storage
3057 .pinned_folders
3058 .retain(|p| p.path.as_path() != path);
3059 }
3060
3061 fn is_pinned(&self, path: &Path) -> bool {
3063 self.storage
3064 .pinned_folders
3065 .iter()
3066 .any(|p| p.path.as_path() == path)
3067 }
3068
3069 fn begin_rename_pinned_folder(&mut self, pinned: PinnedFolder) {
3071 self.rename_pinned_folder = Some(pinned);
3072 self.rename_pinned_folder_request_focus = true;
3073 }
3074
3075 fn end_rename_pinned_folder(&mut self) {
3078 let renamed = std::mem::take(&mut self.rename_pinned_folder);
3079
3080 if let Some(renamed) = renamed {
3081 let old = self
3082 .storage
3083 .pinned_folders
3084 .iter_mut()
3085 .find(|p| p.path == renamed.path);
3086 if let Some(old) = old {
3087 old.label = renamed.label;
3088 }
3089 }
3090 }
3091
3092 fn is_pinned_folder_being_renamed(&self, pinned: &PinnedFolder) -> bool {
3094 self.rename_pinned_folder
3095 .as_ref()
3096 .is_some_and(|p| p.path == pinned.path)
3097 }
3098
3099 fn is_primary_selected(&self, item: &DirectoryEntry) -> bool {
3100 self.selected_item.as_ref().is_some_and(|x| x.path_eq(item))
3101 }
3102
3103 fn reset(&mut self) {
3106 let storage = self.storage.clone();
3107 let config = self.config.clone();
3108 *self = Self::with_config(config);
3109 self.storage = storage;
3110 }
3111
3112 fn refresh(&mut self) {
3115 self.user_directories = self
3116 .config
3117 .file_system
3118 .user_dirs(self.config.canonicalize_paths);
3119 self.system_disks = self
3120 .config
3121 .file_system
3122 .get_disks(self.config.canonicalize_paths);
3123
3124 self.reload_directory();
3125 }
3126
3127 fn submit(&mut self) {
3129 if !self.is_selection_valid() {
3131 return;
3132 }
3133
3134 self.storage.last_picked_dir = self.current_directory().map(PathBuf::from);
3135
3136 match &self.mode {
3137 DialogMode::PickDirectory | DialogMode::PickFile => {
3138 if let Some(item) = self.selected_item.clone() {
3141 self.state = DialogState::Picked(item.to_path_buf());
3142 }
3143 }
3144 DialogMode::PickMultiple => {
3145 let result: Vec<PathBuf> = self
3146 .selected_entries()
3147 .map(crate::DirectoryEntry::to_path_buf)
3148 .collect();
3149
3150 self.state = DialogState::PickedMultiple(result);
3151 }
3152 DialogMode::SaveFile => {
3153 if let Some(path) = self.current_directory() {
3156 let full_path = path.join(&self.file_name_input);
3157 self.submit_save_file(full_path);
3158 }
3159 }
3160 }
3161 }
3162
3163 fn submit_save_file(&mut self, path: PathBuf) {
3166 if path.exists() {
3167 self.open_modal(Box::new(OverwriteFileModal::new(path)));
3168
3169 return;
3170 }
3171
3172 self.state = DialogState::Picked(path);
3173 }
3174
3175 fn cancel(&mut self) {
3177 self.state = DialogState::Cancelled;
3178 }
3179
3180 fn get_initial_directory(&self) -> PathBuf {
3186 let path = match self.config.opening_mode {
3187 OpeningMode::AlwaysInitialDir => &self.config.initial_directory,
3188 OpeningMode::LastVisitedDir => self
3189 .storage
3190 .last_visited_dir
3191 .as_deref()
3192 .unwrap_or(&self.config.initial_directory),
3193 OpeningMode::LastPickedDir => self
3194 .storage
3195 .last_picked_dir
3196 .as_deref()
3197 .unwrap_or(&self.config.initial_directory),
3198 };
3199
3200 let mut path = self.canonicalize_path(path);
3201
3202 if self.config.file_system.is_file(&path) {
3203 if let Some(parent) = path.parent() {
3204 path = parent.to_path_buf();
3205 }
3206 }
3207
3208 path
3209 }
3210
3211 fn current_directory(&self) -> Option<&Path> {
3213 if let Some(x) = self.directory_stack.iter().nth_back(self.directory_offset) {
3214 return Some(x.as_path());
3215 }
3216
3217 None
3218 }
3219
3220 fn is_selection_valid(&self) -> bool {
3223 match &self.mode {
3224 DialogMode::PickDirectory => self
3225 .selected_item
3226 .as_ref()
3227 .is_some_and(crate::DirectoryEntry::is_dir),
3228 DialogMode::PickFile => self
3229 .selected_item
3230 .as_ref()
3231 .is_some_and(DirectoryEntry::is_file),
3232 DialogMode::PickMultiple => self.get_dir_content_filtered_iter().any(|p| p.selected),
3233 DialogMode::SaveFile => self.file_name_input_error.is_none(),
3234 }
3235 }
3236
3237 fn validate_file_name_input(&self) -> Option<String> {
3241 if self.file_name_input.is_empty() {
3242 return Some(self.config.labels.err_empty_file_name.clone());
3243 }
3244
3245 if let Some(x) = self.current_directory() {
3246 let mut full_path = x.to_path_buf();
3247 full_path.push(self.file_name_input.as_str());
3248
3249 if self.config.file_system.is_dir(&full_path) {
3250 return Some(self.config.labels.err_directory_exists.clone());
3251 }
3252
3253 if !self.config.allow_file_overwrite && self.config.file_system.is_file(&full_path) {
3254 return Some(self.config.labels.err_file_exists.clone());
3255 }
3256 } else {
3257 return Some("Currently not in a directory".to_string());
3259 }
3260
3261 None
3262 }
3263
3264 fn select_item(&mut self, item: &mut DirectoryEntry) {
3267 if self.mode == DialogMode::PickMultiple {
3268 item.selected = true;
3269 }
3270 self.selected_item = Some(item.clone());
3271
3272 if self.mode == DialogMode::SaveFile && item.is_file() {
3273 self.file_name_input = item.file_name().to_string();
3274 self.file_name_input_error = self.validate_file_name_input();
3275 }
3276 }
3277
3278 fn select_next_visible_item_before(&mut self, item: &DirectoryEntry) -> bool {
3283 let mut return_val = false;
3284
3285 self.directory_content.reset_multi_selection();
3286
3287 let mut directory_content = std::mem::take(&mut self.directory_content);
3288 let search_value = std::mem::take(&mut self.search_value);
3289
3290 let index = directory_content
3291 .filtered_iter(&search_value)
3292 .position(|p| p.path_eq(item));
3293
3294 if let Some(index) = index {
3295 if index != 0 {
3296 if let Some(item) = directory_content
3297 .filtered_iter_mut(&search_value)
3298 .nth(index.saturating_sub(1))
3299 {
3300 self.select_item(item);
3301 self.scroll_to_selection = true;
3302 return_val = true;
3303 }
3304 }
3305 }
3306
3307 self.directory_content = directory_content;
3308 self.search_value = search_value;
3309
3310 return_val
3311 }
3312
3313 fn select_next_visible_item_after(&mut self, item: &DirectoryEntry) -> bool {
3318 let mut return_val = false;
3319
3320 self.directory_content.reset_multi_selection();
3321
3322 let mut directory_content = std::mem::take(&mut self.directory_content);
3323 let search_value = std::mem::take(&mut self.search_value);
3324
3325 let index = directory_content
3326 .filtered_iter(&search_value)
3327 .position(|p| p.path_eq(item));
3328
3329 if let Some(index) = index {
3330 if let Some(item) = directory_content
3331 .filtered_iter_mut(&search_value)
3332 .nth(index.saturating_add(1))
3333 {
3334 self.select_item(item);
3335 self.scroll_to_selection = true;
3336 return_val = true;
3337 }
3338 }
3339
3340 self.directory_content = directory_content;
3341 self.search_value = search_value;
3342
3343 return_val
3344 }
3345
3346 fn select_first_visible_item(&mut self) {
3348 self.directory_content.reset_multi_selection();
3349
3350 let mut directory_content = std::mem::take(&mut self.directory_content);
3351
3352 if let Some(item) = directory_content
3353 .filtered_iter_mut(&self.search_value.clone())
3354 .next()
3355 {
3356 self.select_item(item);
3357 self.scroll_to_selection = true;
3358 }
3359
3360 self.directory_content = directory_content;
3361 }
3362
3363 fn select_last_visible_item(&mut self) {
3365 self.directory_content.reset_multi_selection();
3366
3367 let mut directory_content = std::mem::take(&mut self.directory_content);
3368
3369 if let Some(item) = directory_content
3370 .filtered_iter_mut(&self.search_value.clone())
3371 .last()
3372 {
3373 self.select_item(item);
3374 self.scroll_to_selection = true;
3375 }
3376
3377 self.directory_content = directory_content;
3378 }
3379
3380 fn open_path_edit(&mut self) {
3382 let path = self.current_directory().map_or_else(String::new, |path| {
3383 path.to_str().unwrap_or_default().to_string()
3384 });
3385
3386 self.path_edit_value = path;
3387 self.path_edit_activate = true;
3388 self.path_edit_visible = true;
3389 }
3390
3391 fn submit_path_edit(&mut self) {
3393 self.close_path_edit();
3394
3395 let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value));
3396
3397 if self.mode == DialogMode::PickFile && self.config.file_system.is_file(&path) {
3398 self.state = DialogState::Picked(path);
3399 return;
3400 }
3401
3402 if self.mode == DialogMode::SaveFile
3409 && (path.extension().is_some()
3410 || self.config.allow_path_edit_to_save_file_without_extension)
3411 && !self.config.file_system.is_dir(&path)
3412 && path.parent().is_some_and(std::path::Path::exists)
3413 {
3414 self.submit_save_file(path);
3415 return;
3416 }
3417
3418 self.load_directory(&path);
3419 }
3420
3421 const fn close_path_edit(&mut self) {
3424 self.path_edit_visible = false;
3425 }
3426
3427 fn load_next_directory(&mut self) {
3432 if self.directory_offset == 0 {
3433 return;
3435 }
3436
3437 self.directory_offset -= 1;
3438
3439 if let Some(path) = self.current_directory() {
3441 self.load_directory_content(path.to_path_buf().as_path());
3442 }
3443 }
3444
3445 fn load_previous_directory(&mut self) {
3449 if self.directory_offset + 1 >= self.directory_stack.len() {
3450 return;
3452 }
3453
3454 self.directory_offset += 1;
3455
3456 if let Some(path) = self.current_directory() {
3458 self.load_directory_content(path.to_path_buf().as_path());
3459 }
3460 }
3461
3462 fn load_parent_directory(&mut self) {
3466 if let Some(x) = self.current_directory() {
3467 if let Some(x) = x.to_path_buf().parent() {
3468 self.load_directory(x);
3469 }
3470 }
3471 }
3472
3473 fn reload_directory(&mut self) {
3480 if let Some(x) = self.current_directory() {
3481 self.load_directory_content(x.to_path_buf().as_path());
3482 }
3483 }
3484
3485 fn load_directory(&mut self, path: &Path) {
3491 if let Some(x) = self.current_directory() {
3494 if x == path {
3495 return;
3496 }
3497 }
3498
3499 if self.directory_offset != 0 && self.directory_stack.len() > self.directory_offset {
3500 self.directory_stack
3501 .drain(self.directory_stack.len() - self.directory_offset..);
3502 }
3503
3504 self.directory_stack.push(path.to_path_buf());
3505 self.directory_offset = 0;
3506
3507 self.load_directory_content(path);
3508
3509 self.search_value.clear();
3512 }
3513
3514 fn load_directory_content(&mut self, path: &Path) {
3516 self.storage.last_visited_dir = Some(path.to_path_buf());
3517
3518 let selected_file_filter = match self.mode {
3519 DialogMode::PickFile | DialogMode::PickMultiple => self.get_selected_file_filter(),
3520 _ => None,
3521 };
3522
3523 let selected_save_extension = if self.mode == DialogMode::SaveFile {
3524 self.get_selected_save_extension()
3525 .map(|e| e.file_extension.as_str())
3526 } else {
3527 None
3528 };
3529
3530 let filter = DirectoryFilter {
3531 show_files: self.show_files,
3532 show_hidden: self.storage.show_hidden,
3533 show_system_files: self.storage.show_system_files,
3534 file_filter: selected_file_filter.cloned(),
3535 filter_extension: selected_save_extension.map(str::to_string),
3536 };
3537
3538 self.directory_content = DirectoryContent::from_path(
3539 &self.config,
3540 path,
3541 self.config.file_system.clone(),
3542 filter,
3543 );
3544
3545 self.create_directory_dialog.close();
3546 self.scroll_to_selection = true;
3547
3548 if self.mode == DialogMode::SaveFile {
3549 self.file_name_input_error = self.validate_file_name_input();
3550 }
3551 }
3552}