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 #[allow(clippy::coerce_container_to_any)]
1116 self.user_data.as_ref().and_then(|u| u.downcast_ref())
1117 }
1118
1119 pub fn user_data_mut<U: Any>(&mut self) -> Option<&mut U> {
1123 #[allow(clippy::coerce_container_to_any)]
1124 self.user_data.as_mut().and_then(|u| u.downcast_mut())
1125 }
1126
1127 pub fn set_user_data<U: Any + Send + Sync>(&mut self, user_data: U) {
1151 self.user_data = Some(Box::new(user_data));
1152 }
1153
1154 pub const fn mode(&self) -> DialogMode {
1156 self.mode
1157 }
1158
1159 pub const fn state(&self) -> &DialogState {
1161 &self.state
1162 }
1163
1164 pub const fn get_window_id(&self) -> egui::Id {
1166 self.window_id
1167 }
1168}
1169
1170impl FileDialog {
1172 fn update_ui(
1176 &mut self,
1177 ctx: &egui::Context,
1178 right_panel_fn: Option<&mut FileDialogUiCallback>,
1179 ) {
1180 let mut is_open = true;
1181
1182 if self.config.as_modal {
1183 let re = self.ui_update_modal_background(ctx);
1184 ctx.move_to_top(re.response.layer_id);
1185 }
1186
1187 let re = self.create_window(&mut is_open).show(ctx, |ui| {
1188 if !self.modals.is_empty() {
1189 self.ui_update_modals(ui);
1190 return;
1191 }
1192
1193 if self.config.show_top_panel {
1194 egui::TopBottomPanel::top(self.window_id.with("top_panel"))
1195 .resizable(false)
1196 .show_inside(ui, |ui| {
1197 self.ui_update_top_panel(ui);
1198 });
1199 }
1200
1201 if self.config.show_left_panel {
1202 egui::SidePanel::left(self.window_id.with("left_panel"))
1203 .resizable(true)
1204 .default_width(150.0)
1205 .width_range(90.0..=250.0)
1206 .show_inside(ui, |ui| {
1207 self.ui_update_left_panel(ui);
1208 });
1209 }
1210
1211 if let Some(f) = right_panel_fn {
1213 let mut right_panel = egui::SidePanel::right(self.window_id.with("right_panel"))
1214 .resizable(true);
1217 if let Some(width) = self.config.right_panel_width {
1218 right_panel = right_panel.default_width(width);
1219 }
1220 right_panel.show_inside(ui, |ui| {
1221 f(ui, self);
1222 });
1223 }
1224
1225 egui::TopBottomPanel::bottom(self.window_id.with("bottom_panel"))
1226 .resizable(false)
1227 .show_inside(ui, |ui| {
1228 self.ui_update_bottom_panel(ui);
1229 });
1230
1231 egui::CentralPanel::default().show_inside(ui, |ui| {
1232 self.ui_update_central_panel(ui);
1233 });
1234 });
1235
1236 if self.config.as_modal {
1237 if let Some(inner_response) = re {
1238 ctx.move_to_top(inner_response.response.layer_id);
1239 }
1240 }
1241
1242 self.any_focused_last_frame = ctx.memory(egui::Memory::focused).is_some();
1243
1244 if !is_open {
1246 self.cancel();
1247 }
1248
1249 let mut repaint = false;
1250
1251 ctx.input(|i| {
1253 if let Some(dropped_file) = i.raw.dropped_files.last() {
1255 if let Some(path) = &dropped_file.path {
1256 if self.config.file_system.is_dir(path) {
1257 self.load_directory(path.as_path());
1259 repaint = true;
1260 } else if let Some(parent) = path.parent() {
1261 self.load_directory(parent);
1263 self.select_item(&mut DirectoryEntry::from_path(
1264 &self.config,
1265 path,
1266 &*self.config.file_system,
1267 ));
1268 self.scroll_to_selection = true;
1269 repaint = true;
1270 }
1271 }
1272 }
1273 });
1274
1275 if repaint {
1277 ctx.request_repaint();
1278 }
1279 }
1280
1281 fn ui_update_modal_background(&self, ctx: &egui::Context) -> egui::InnerResponse<()> {
1283 egui::Area::new(self.window_id.with("modal_overlay"))
1284 .interactable(true)
1285 .fixed_pos(egui::Pos2::ZERO)
1286 .show(ctx, |ui| {
1287 let content_rect = ctx.input(egui::InputState::content_rect);
1288
1289 ui.allocate_response(content_rect.size(), egui::Sense::click());
1290
1291 ui.painter().rect_filled(
1292 content_rect,
1293 egui::CornerRadius::ZERO,
1294 self.config.modal_overlay_color,
1295 );
1296 })
1297 }
1298
1299 fn ui_update_modals(&mut self, ui: &mut egui::Ui) {
1300 egui::TopBottomPanel::bottom(self.window_id.with("modal_bottom_panel"))
1305 .resizable(false)
1306 .show_separator_line(false)
1307 .show_inside(ui, |_| {});
1308
1309 egui::CentralPanel::default().show_inside(ui, |ui| {
1312 if let Some(modal) = self.modals.last_mut() {
1313 #[allow(clippy::single_match)]
1314 match modal.update(&self.config, ui) {
1315 ModalState::Close(action) => {
1316 self.exec_modal_action(action);
1317 self.modals.pop();
1318 }
1319 ModalState::Pending => {}
1320 }
1321 }
1322 });
1323 }
1324
1325 fn create_window<'a>(&self, is_open: &'a mut bool) -> egui::Window<'a> {
1327 let mut window = egui::Window::new(self.get_window_title())
1328 .id(self.window_id)
1329 .open(is_open)
1330 .default_size(self.config.default_size)
1331 .min_size(self.config.min_size)
1332 .resizable(self.config.resizable)
1333 .movable(self.config.movable)
1334 .title_bar(self.config.title_bar)
1335 .collapsible(false);
1336
1337 if let Some(pos) = self.config.default_pos {
1338 window = window.default_pos(pos);
1339 }
1340
1341 if let Some(pos) = self.config.fixed_pos {
1342 window = window.fixed_pos(pos);
1343 }
1344
1345 if let Some((anchor, offset)) = self.config.anchor {
1346 window = window.anchor(anchor, offset);
1347 }
1348
1349 if let Some(size) = self.config.max_size {
1350 window = window.max_size(size);
1351 }
1352
1353 window
1354 }
1355
1356 const fn get_window_title(&self) -> &String {
1359 match &self.config.title {
1360 Some(title) => title,
1361 None => match &self.mode {
1362 DialogMode::PickDirectory => &self.config.labels.title_select_directory,
1363 DialogMode::PickFile => &self.config.labels.title_select_file,
1364 DialogMode::PickMultiple => &self.config.labels.title_select_multiple,
1365 DialogMode::SaveFile => &self.config.labels.title_save_file,
1366 },
1367 }
1368 }
1369
1370 fn ui_update_top_panel(&mut self, ui: &mut egui::Ui) {
1373 const BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(25.0, 25.0);
1374
1375 ui.horizontal(|ui| {
1376 self.ui_update_nav_buttons(ui, BUTTON_SIZE);
1377
1378 let mut path_display_width = ui.available_width();
1379
1380 if self.config.show_reload_button {
1382 path_display_width -= ui
1383 .style()
1384 .spacing
1385 .item_spacing
1386 .x
1387 .mul_add(2.5, BUTTON_SIZE.x);
1388 }
1389
1390 if self.config.show_search {
1391 path_display_width -= 140.0;
1392 }
1393
1394 if self.config.show_current_path {
1395 self.ui_update_current_path(ui, path_display_width);
1396 }
1397
1398 if self.config.show_menu_button
1400 && (self.config.show_reload_button
1401 || self.config.show_working_directory_button
1402 || self.config.show_hidden_option
1403 || self.config.show_system_files_option)
1404 {
1405 ui.allocate_ui_with_layout(
1406 BUTTON_SIZE,
1407 egui::Layout::centered_and_justified(egui::Direction::LeftToRight),
1408 |ui| {
1409 ui.menu_button("☰", |ui| {
1410 self.ui_update_hamburger_menu(ui);
1411 });
1412 },
1413 );
1414 }
1415
1416 if self.config.show_search {
1417 self.ui_update_search(ui);
1418 }
1419 });
1420
1421 ui.add_space(ui.ctx().style().spacing.item_spacing.y);
1422 }
1423
1424 fn ui_update_nav_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
1426 if self.config.show_parent_button {
1427 if let Some(x) = self.current_directory() {
1428 if self.ui_button_sized(ui, x.parent().is_some(), button_size, "⏶", None) {
1429 self.load_parent_directory();
1430 }
1431 } else {
1432 let _ = self.ui_button_sized(ui, false, button_size, "⏶", None);
1433 }
1434 }
1435
1436 if self.config.show_back_button
1437 && self.ui_button_sized(
1438 ui,
1439 self.directory_offset + 1 < self.directory_stack.len(),
1440 button_size,
1441 "⏴",
1442 None,
1443 )
1444 {
1445 self.load_previous_directory();
1446 }
1447
1448 if self.config.show_forward_button
1449 && self.ui_button_sized(ui, self.directory_offset != 0, button_size, "⏵", None)
1450 {
1451 self.load_next_directory();
1452 }
1453
1454 if self.config.show_new_folder_button
1455 && self.ui_button_sized(
1456 ui,
1457 !self.create_directory_dialog.is_open(),
1458 button_size,
1459 "+",
1460 None,
1461 )
1462 {
1463 self.open_new_folder_dialog();
1464 }
1465 }
1466
1467 fn ui_update_current_path(&mut self, ui: &mut egui::Ui, width: f32) {
1471 egui::Frame::default()
1472 .stroke(egui::Stroke::new(
1473 1.0,
1474 ui.ctx().style().visuals.window_stroke.color,
1475 ))
1476 .inner_margin(egui::Margin::from(4))
1477 .corner_radius(egui::CornerRadius::from(4))
1478 .show(ui, |ui| {
1479 const EDIT_BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(22.0, 20.0);
1480
1481 if self.path_edit_visible {
1482 self.ui_update_path_edit(ui, width, EDIT_BUTTON_SIZE);
1483 } else {
1484 self.ui_update_path_display(ui, width, EDIT_BUTTON_SIZE);
1485 }
1486 });
1487 }
1488
1489 fn ui_update_path_display(
1491 &mut self,
1492 ui: &mut egui::Ui,
1493 width: f32,
1494 edit_button_size: egui::Vec2,
1495 ) {
1496 ui.style_mut().always_scroll_the_only_direction = true;
1497 ui.style_mut().spacing.scroll.bar_width = 8.0;
1498
1499 let max_width = if self.config.show_path_edit_button {
1500 ui.style()
1501 .spacing
1502 .item_spacing
1503 .x
1504 .mul_add(-2.0, width - edit_button_size.x)
1505 } else {
1506 width
1507 };
1508
1509 egui::ScrollArea::horizontal()
1510 .auto_shrink([false, false])
1511 .stick_to_right(true)
1512 .max_width(max_width)
1513 .show(ui, |ui| {
1514 ui.horizontal(|ui| {
1515 ui.style_mut().spacing.item_spacing.x /= 2.5;
1516 ui.style_mut().spacing.button_padding = egui::Vec2::new(5.0, 3.0);
1517
1518 let mut path = PathBuf::new();
1519
1520 if let Some(data) = self.current_directory().map(Path::to_path_buf) {
1521 for (i, segment) in data.iter().enumerate() {
1522 path.push(segment);
1523
1524 let mut segment_str = segment.to_str().unwrap_or_default().to_string();
1525
1526 if self.is_pinned(&path) {
1527 segment_str =
1528 format!("{} {}", &self.config.pinned_icon, segment_str);
1529 }
1530
1531 if i != 0 {
1532 ui.label(self.config.directory_separator.as_str());
1533 }
1534
1535 let re = ui.button(segment_str);
1536
1537 if re.clicked() {
1538 self.load_directory(path.as_path());
1539 return;
1540 }
1541
1542 self.ui_update_central_panel_path_context_menu(&re, &path.clone());
1543 }
1544 }
1545 });
1546 });
1547
1548 if !self.config.show_path_edit_button {
1549 return;
1550 }
1551
1552 if ui
1553 .add_sized(
1554 edit_button_size,
1555 egui::Button::new("🖊").fill(egui::Color32::TRANSPARENT),
1556 )
1557 .clicked()
1558 {
1559 self.open_path_edit();
1560 }
1561 }
1562
1563 fn ui_update_path_edit(&mut self, ui: &mut egui::Ui, width: f32, edit_button_size: egui::Vec2) {
1565 let desired_width: f32 = ui
1566 .style()
1567 .spacing
1568 .item_spacing
1569 .x
1570 .mul_add(-3.0, width - edit_button_size.x);
1571
1572 let response = egui::TextEdit::singleline(&mut self.path_edit_value)
1573 .desired_width(desired_width)
1574 .show(ui)
1575 .response;
1576
1577 if self.path_edit_activate {
1578 response.request_focus();
1579 Self::set_cursor_to_end(&response, &self.path_edit_value);
1580 self.path_edit_activate = false;
1581 }
1582
1583 if self.path_edit_request_focus {
1584 response.request_focus();
1585 self.path_edit_request_focus = false;
1586 }
1587
1588 let btn_response = ui.add_sized(edit_button_size, egui::Button::new("✔"));
1589
1590 if btn_response.clicked() {
1591 self.submit_path_edit();
1592 }
1593
1594 if !response.has_focus() && !btn_response.contains_pointer() {
1595 self.path_edit_visible = false;
1596 }
1597 }
1598
1599 fn ui_update_hamburger_menu(&mut self, ui: &mut egui::Ui) {
1601 const SEPARATOR_SPACING: f32 = 2.0;
1602
1603 if self.config.show_reload_button && ui.button(&self.config.labels.reload).clicked() {
1604 self.refresh();
1605 ui.close();
1606 }
1607
1608 let working_dir = self.config.file_system.current_dir();
1609
1610 if self.config.show_working_directory_button
1611 && working_dir.is_ok()
1612 && ui.button(&self.config.labels.working_directory).clicked()
1613 {
1614 self.load_directory(&working_dir.unwrap_or_default());
1615 ui.close();
1616 }
1617
1618 if (self.config.show_reload_button || self.config.show_working_directory_button)
1619 && (self.config.show_hidden_option || self.config.show_system_files_option)
1620 {
1621 ui.add_space(SEPARATOR_SPACING);
1622 ui.separator();
1623 ui.add_space(SEPARATOR_SPACING);
1624 }
1625
1626 if self.config.show_hidden_option
1627 && ui
1628 .checkbox(
1629 &mut self.storage.show_hidden,
1630 &self.config.labels.show_hidden,
1631 )
1632 .clicked()
1633 {
1634 self.refresh();
1635 ui.close();
1636 }
1637
1638 if self.config.show_system_files_option
1639 && ui
1640 .checkbox(
1641 &mut self.storage.show_system_files,
1642 &self.config.labels.show_system_files,
1643 )
1644 .clicked()
1645 {
1646 self.refresh();
1647 ui.close();
1648 }
1649 }
1650
1651 fn ui_update_search(&mut self, ui: &mut egui::Ui) {
1653 egui::Frame::default()
1654 .stroke(egui::Stroke::new(
1655 1.0,
1656 ui.ctx().style().visuals.window_stroke.color,
1657 ))
1658 .inner_margin(egui::Margin::symmetric(4, 4))
1659 .corner_radius(egui::CornerRadius::from(4))
1660 .show(ui, |ui| {
1661 ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
1662 ui.add_space(ui.ctx().style().spacing.item_spacing.y);
1663
1664 ui.label(egui::RichText::from("🔍").size(15.0));
1665
1666 let re = ui.add_sized(
1667 egui::Vec2::new(ui.available_width(), 0.0),
1668 egui::TextEdit::singleline(&mut self.search_value),
1669 );
1670
1671 self.edit_search_on_text_input(ui);
1672
1673 if re.changed() || self.init_search {
1674 self.selected_item = None;
1675 self.select_first_visible_item();
1676 }
1677
1678 if self.init_search {
1679 re.request_focus();
1680 Self::set_cursor_to_end(&re, &self.search_value);
1681 self.directory_content.reset_multi_selection();
1682
1683 self.init_search = false;
1684 }
1685 });
1686 });
1687 }
1688
1689 fn edit_search_on_text_input(&mut self, ui: &egui::Ui) {
1696 if ui.memory(|mem| mem.focused().is_some()) {
1697 return;
1698 }
1699
1700 ui.input(|inp| {
1701 if inp.modifiers.any() && !inp.modifiers.shift_only() {
1703 return;
1704 }
1705
1706 for text in inp.events.iter().filter_map(|ev| match ev {
1709 egui::Event::Text(t) => Some(t),
1710 _ => None,
1711 }) {
1712 self.search_value.push_str(text);
1713 self.init_search = true;
1714 }
1715 });
1716 }
1717
1718 fn ui_update_left_panel(&mut self, ui: &mut egui::Ui) {
1721 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
1722 const SPACING_MULTIPLIER: f32 = 4.0;
1724
1725 egui::containers::ScrollArea::vertical()
1726 .auto_shrink([false, false])
1727 .show(ui, |ui| {
1728 let mut spacing = ui.ctx().style().spacing.item_spacing.y * 2.0;
1730
1731 if self.config.show_pinned_folders && self.ui_update_pinned_folders(ui, spacing)
1733 {
1734 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1735 }
1736
1737 let quick_accesses = std::mem::take(&mut self.config.quick_accesses);
1739
1740 for quick_access in &quick_accesses {
1741 ui.add_space(spacing);
1742 self.ui_update_quick_access(ui, quick_access);
1743 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1744 }
1745
1746 self.config.quick_accesses = quick_accesses;
1747
1748 if self.config.show_places && self.ui_update_user_directories(ui, spacing) {
1750 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1751 }
1752
1753 let disks = std::mem::take(&mut self.system_disks);
1754
1755 if self.config.show_devices && self.ui_update_devices(ui, spacing, &disks) {
1756 spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER;
1757 }
1758
1759 if self.config.show_removable_devices
1760 && self.ui_update_removable_devices(ui, spacing, &disks)
1761 {
1762 }
1765
1766 self.system_disks = disks;
1767 });
1768 });
1769 }
1770
1771 fn ui_update_left_panel_entry(
1775 &mut self,
1776 ui: &mut egui::Ui,
1777 display_name: &str,
1778 path: &Path,
1779 ) -> egui::Response {
1780 let response = ui.selectable_label(self.current_directory() == Some(path), display_name);
1781
1782 if response.clicked() {
1783 self.load_directory(path);
1784 }
1785
1786 response
1787 }
1788
1789 fn ui_update_quick_access(&mut self, ui: &mut egui::Ui, quick_access: &QuickAccess) {
1791 ui.label(&quick_access.heading);
1792
1793 for entry in &quick_access.paths {
1794 self.ui_update_left_panel_entry(ui, &entry.display_name, &entry.path);
1795 }
1796 }
1797
1798 fn ui_update_pinned_folders(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1803 let mut visible = false;
1804
1805 for (i, pinned) in self.storage.pinned_folders.clone().iter().enumerate() {
1806 if i == 0 {
1807 ui.add_space(spacing);
1808 ui.label(self.config.labels.heading_pinned.as_str());
1809
1810 visible = true;
1811 }
1812
1813 if self.is_pinned_folder_being_renamed(pinned) {
1814 self.ui_update_pinned_folder_rename(ui);
1815 continue;
1816 }
1817
1818 let response = self.ui_update_left_panel_entry(
1819 ui,
1820 &format!("{} {}", self.config.pinned_icon, &pinned.label),
1821 pinned.path.as_path(),
1822 );
1823
1824 self.ui_update_pinned_folder_context_menu(&response, pinned);
1825 }
1826
1827 visible
1828 }
1829
1830 fn ui_update_pinned_folder_rename(&mut self, ui: &mut egui::Ui) {
1831 if let Some(r) = &mut self.rename_pinned_folder {
1832 let id = self.window_id.with("pinned_folder_rename").with(&r.path);
1833 let mut output = egui::TextEdit::singleline(&mut r.label)
1834 .id(id)
1835 .cursor_at_end(true)
1836 .show(ui);
1837
1838 if self.rename_pinned_folder_request_focus {
1839 output.state.cursor.set_char_range(Some(CCursorRange::two(
1840 CCursor::new(0),
1841 CCursor::new(r.label.chars().count()),
1842 )));
1843 output.state.store(ui.ctx(), output.response.id);
1844
1845 output.response.request_focus();
1846
1847 self.rename_pinned_folder_request_focus = false;
1848 }
1849
1850 if output.response.lost_focus() {
1851 self.end_rename_pinned_folder();
1852 }
1853 }
1854 }
1855
1856 fn ui_update_pinned_folder_context_menu(
1857 &mut self,
1858 item: &egui::Response,
1859 pinned: &PinnedFolder,
1860 ) {
1861 item.context_menu(|ui| {
1862 if ui.button(&self.config.labels.unpin_folder).clicked() {
1863 self.unpin_path(&pinned.path);
1864 ui.close();
1865 }
1866
1867 if ui
1868 .button(&self.config.labels.rename_pinned_folder)
1869 .clicked()
1870 {
1871 self.begin_rename_pinned_folder(pinned.clone());
1872 ui.close();
1873 }
1874 });
1875 }
1876
1877 fn ui_update_user_directories(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool {
1882 let user_directories = std::mem::take(&mut self.user_directories);
1886 let labels = std::mem::take(&mut self.config.labels);
1887
1888 let visible = 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 true
1915 } else {
1916 false
1917 };
1918
1919 self.user_directories = user_directories;
1920 self.config.labels = labels;
1921
1922 visible
1923 }
1924
1925 fn ui_update_devices(&mut self, ui: &mut egui::Ui, spacing: f32, disks: &Disks) -> bool {
1930 let mut visible = false;
1931
1932 for (i, disk) in disks.iter().filter(|x| !x.is_removable()).enumerate() {
1933 if i == 0 {
1934 ui.add_space(spacing);
1935 ui.label(self.config.labels.heading_devices.as_str());
1936
1937 visible = true;
1938 }
1939
1940 self.ui_update_device_entry(ui, disk);
1941 }
1942
1943 visible
1944 }
1945
1946 fn ui_update_removable_devices(
1951 &mut self,
1952 ui: &mut egui::Ui,
1953 spacing: f32,
1954 disks: &Disks,
1955 ) -> bool {
1956 let mut visible = false;
1957
1958 for (i, disk) in disks.iter().filter(|x| x.is_removable()).enumerate() {
1959 if i == 0 {
1960 ui.add_space(spacing);
1961 ui.label(self.config.labels.heading_removable_devices.as_str());
1962
1963 visible = true;
1964 }
1965
1966 self.ui_update_device_entry(ui, disk);
1967 }
1968
1969 visible
1970 }
1971
1972 fn ui_update_device_entry(&mut self, ui: &mut egui::Ui, device: &Disk) {
1974 let label = if device.is_removable() {
1975 format!(
1976 "{} {}",
1977 self.config.removable_device_icon,
1978 device.display_name()
1979 )
1980 } else {
1981 format!("{} {}", self.config.device_icon, device.display_name())
1982 };
1983
1984 self.ui_update_left_panel_entry(ui, &label, device.mount_point());
1985 }
1986
1987 fn ui_update_bottom_panel(&mut self, ui: &mut egui::Ui) {
1989 const BUTTON_HEIGHT: f32 = 20.0;
1990 ui.add_space(5.0);
1991
1992 let label_submit_width = match self.mode {
1994 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
1995 Self::calc_text_width(ui, &self.config.labels.open_button)
1996 }
1997 DialogMode::SaveFile => Self::calc_text_width(ui, &self.config.labels.save_button),
1998 };
1999
2000 let mut btn_width = Self::calc_text_width(ui, &self.config.labels.cancel_button);
2001 if label_submit_width > btn_width {
2002 btn_width = label_submit_width;
2003 }
2004
2005 btn_width += ui.spacing().button_padding.x * 4.0;
2006
2007 let button_size: egui::Vec2 = egui::Vec2::new(btn_width, BUTTON_HEIGHT);
2009
2010 self.ui_update_selection_preview(ui, button_size);
2011
2012 if self.mode == DialogMode::SaveFile && self.config.save_extensions.is_empty() {
2013 ui.add_space(ui.style().spacing.item_spacing.y);
2014 }
2015
2016 self.ui_update_action_buttons(ui, button_size);
2017 }
2018
2019 fn ui_update_selection_preview(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2021 const SELECTION_PREVIEW_MIN_WIDTH: f32 = 50.0;
2022 let item_spacing = ui.style().spacing.item_spacing;
2023
2024 let render_filter_selection = (!self.config.file_filters.is_empty()
2025 && (self.mode == DialogMode::PickFile || self.mode == DialogMode::PickMultiple))
2026 || (!self.config.save_extensions.is_empty() && self.mode == DialogMode::SaveFile);
2027
2028 let filter_selection_width = button_size.x.mul_add(2.0, item_spacing.x);
2029 let mut filter_selection_separate_line = false;
2030
2031 ui.horizontal(|ui| {
2032 match &self.mode {
2033 DialogMode::PickDirectory => ui.label(&self.config.labels.selected_directory),
2034 DialogMode::PickFile => ui.label(&self.config.labels.selected_file),
2035 DialogMode::PickMultiple => ui.label(&self.config.labels.selected_items),
2036 DialogMode::SaveFile => ui.label(&self.config.labels.file_name),
2037 };
2038
2039 let mut scroll_bar_width: f32 =
2044 ui.available_width() - filter_selection_width - item_spacing.x;
2045
2046 if scroll_bar_width < SELECTION_PREVIEW_MIN_WIDTH || !render_filter_selection {
2047 filter_selection_separate_line = true;
2048 scroll_bar_width = ui.available_width();
2049 }
2050
2051 match &self.mode {
2052 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2053 use egui::containers::scroll_area::ScrollBarVisibility;
2054
2055 let text = self.get_selection_preview_text();
2056
2057 egui::containers::ScrollArea::horizontal()
2058 .auto_shrink([false, false])
2059 .max_width(scroll_bar_width)
2060 .stick_to_right(true)
2061 .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
2062 .show(ui, |ui| {
2063 ui.colored_label(ui.style().visuals.selection.bg_fill, text);
2064 });
2065 }
2066 DialogMode::SaveFile => {
2067 let mut output = egui::TextEdit::singleline(&mut self.file_name_input)
2068 .cursor_at_end(false)
2069 .margin(egui::Margin::symmetric(4, 3))
2070 .desired_width(scroll_bar_width - item_spacing.x)
2071 .show(ui);
2072
2073 if self.file_name_input_request_focus {
2074 self.highlight_file_name_input(&mut output);
2075 output.state.store(ui.ctx(), output.response.id);
2076
2077 output.response.request_focus();
2078 self.file_name_input_request_focus = false;
2079 }
2080
2081 if output.response.changed() {
2082 self.file_name_input_error = self.validate_file_name_input();
2083 }
2084
2085 if output.response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
2086 {
2087 self.submit();
2088 }
2089 }
2090 }
2091
2092 if !filter_selection_separate_line && render_filter_selection {
2093 if self.mode == DialogMode::SaveFile {
2094 self.ui_update_save_extension_selection(ui, filter_selection_width);
2095 } else {
2096 self.ui_update_file_filter_selection(ui, filter_selection_width);
2097 }
2098 }
2099 });
2100
2101 if filter_selection_separate_line && render_filter_selection {
2102 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2103 if self.mode == DialogMode::SaveFile {
2104 self.ui_update_save_extension_selection(ui, filter_selection_width);
2105 } else {
2106 self.ui_update_file_filter_selection(ui, filter_selection_width);
2107 }
2108 });
2109 }
2110 }
2111
2112 fn highlight_file_name_input(&self, output: &mut egui::text_edit::TextEditOutput) {
2116 if let Some(pos) = self.file_name_input.rfind('.') {
2117 let range = if pos == 0 {
2118 CCursorRange::two(CCursor::new(0), CCursor::new(0))
2119 } else {
2120 CCursorRange::two(CCursor::new(0), CCursor::new(pos))
2121 };
2122
2123 output.state.cursor.set_char_range(Some(range));
2124 }
2125 }
2126
2127 fn get_selection_preview_text(&self) -> String {
2128 if self.is_selection_valid() {
2129 match &self.mode {
2130 DialogMode::PickDirectory | DialogMode::PickFile => self
2131 .selected_item
2132 .as_ref()
2133 .map_or_else(String::new, |item| item.file_name().to_string()),
2134 DialogMode::PickMultiple => {
2135 let mut result = String::new();
2136
2137 for (i, item) in self
2138 .get_dir_content_filtered_iter()
2139 .filter(|p| p.selected)
2140 .enumerate()
2141 {
2142 if i == 0 {
2143 result += item.file_name();
2144 continue;
2145 }
2146
2147 result += format!(", {}", item.file_name()).as_str();
2148 }
2149
2150 result
2151 }
2152 DialogMode::SaveFile => String::new(),
2153 }
2154 } else {
2155 String::new()
2156 }
2157 }
2158
2159 fn ui_update_file_filter_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2160 let selected_filter = self.get_selected_file_filter();
2161 let selected_text = match selected_filter {
2162 Some(f) => &f.name,
2163 None => &self.config.labels.file_filter_all_files,
2164 };
2165
2166 let mut select_filter: Option<Option<FileFilter>> = None;
2169
2170 egui::containers::ComboBox::from_id_salt(self.window_id.with("file_filter_selection"))
2171 .width(width)
2172 .selected_text(selected_text)
2173 .wrap_mode(egui::TextWrapMode::Truncate)
2174 .show_ui(ui, |ui| {
2175 for filter in &self.config.file_filters {
2176 let selected = selected_filter.is_some_and(|f| f.id == filter.id);
2177
2178 if ui.selectable_label(selected, &filter.name).clicked() {
2179 select_filter = Some(Some(filter.clone()));
2180 }
2181 }
2182
2183 if ui
2184 .selectable_label(
2185 selected_filter.is_none(),
2186 &self.config.labels.file_filter_all_files,
2187 )
2188 .clicked()
2189 {
2190 select_filter = Some(None);
2191 }
2192 });
2193
2194 if let Some(i) = select_filter {
2195 self.select_file_filter(i);
2196 }
2197 }
2198
2199 fn ui_update_save_extension_selection(&mut self, ui: &mut egui::Ui, width: f32) {
2200 let selected_extension = self.get_selected_save_extension();
2201 let selected_text = match selected_extension {
2202 Some(e) => &e.to_string(),
2203 None => &self.config.labels.save_extension_any,
2204 };
2205
2206 let mut select_extension: Option<Option<SaveExtension>> = None;
2209
2210 egui::containers::ComboBox::from_id_salt(self.window_id.with("save_extension_selection"))
2211 .width(width)
2212 .selected_text(selected_text)
2213 .wrap_mode(egui::TextWrapMode::Truncate)
2214 .show_ui(ui, |ui| {
2215 for extension in &self.config.save_extensions {
2216 let selected = selected_extension.is_some_and(|s| s.id == extension.id);
2217
2218 if ui
2219 .selectable_label(selected, extension.to_string())
2220 .clicked()
2221 {
2222 select_extension = Some(Some(extension.clone()));
2223 }
2224 }
2225 });
2226
2227 if let Some(i) = select_extension {
2228 self.file_name_input_request_focus = true;
2229 self.select_save_extension(i);
2230 }
2231 }
2232
2233 fn ui_update_action_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) {
2235 ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
2236 let label = match &self.mode {
2237 DialogMode::PickDirectory | DialogMode::PickFile | DialogMode::PickMultiple => {
2238 self.config.labels.open_button.as_str()
2239 }
2240 DialogMode::SaveFile => self.config.labels.save_button.as_str(),
2241 };
2242
2243 if self.ui_button_sized(
2244 ui,
2245 self.is_selection_valid(),
2246 button_size,
2247 label,
2248 self.file_name_input_error.as_deref(),
2249 ) {
2250 self.submit();
2251 }
2252
2253 if ui
2254 .add_sized(
2255 button_size,
2256 egui::Button::new(self.config.labels.cancel_button.as_str()),
2257 )
2258 .clicked()
2259 {
2260 self.cancel();
2261 }
2262 });
2263 }
2264
2265 fn ui_update_central_panel(&mut self, ui: &mut egui::Ui) {
2268 if self.update_directory_content(ui) {
2269 return;
2270 }
2271
2272 self.ui_update_central_panel_content(ui);
2273 }
2274
2275 fn update_directory_content(&mut self, ui: &mut egui::Ui) -> bool {
2280 const SHOW_SPINNER_AFTER: f32 = 0.2;
2281
2282 match self.directory_content.update() {
2283 DirectoryContentState::Pending(timestamp) => {
2284 let now = std::time::SystemTime::now();
2285
2286 if now
2287 .duration_since(*timestamp)
2288 .unwrap_or_default()
2289 .as_secs_f32()
2290 > SHOW_SPINNER_AFTER
2291 {
2292 ui.centered_and_justified(egui::Ui::spinner);
2293 }
2294
2295 ui.ctx().request_repaint();
2297
2298 true
2299 }
2300 DirectoryContentState::Errored(err) => {
2301 ui.centered_and_justified(|ui| ui.colored_label(ui.visuals().error_fg_color, err));
2302 true
2303 }
2304 DirectoryContentState::Finished => {
2305 if self.mode == DialogMode::PickDirectory {
2306 if let Some(dir) = self.current_directory() {
2307 let mut dir_entry =
2308 DirectoryEntry::from_path(&self.config, dir, &*self.config.file_system);
2309 self.select_item(&mut dir_entry);
2310 }
2311 }
2312
2313 false
2314 }
2315 DirectoryContentState::Success => false,
2316 }
2317 }
2318
2319 fn ui_update_central_panel_content(&mut self, ui: &mut egui::Ui) {
2322 let mut data = std::mem::take(&mut self.directory_content);
2324
2325 let mut reset_multi_selection = false;
2328
2329 let mut batch_select_item_b: Option<DirectoryEntry> = None;
2332
2333 let mut should_return = false;
2335
2336 ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
2337 let scroll_area = egui::containers::ScrollArea::vertical().auto_shrink([false, false]);
2338
2339 if self.search_value.is_empty()
2340 && !self.create_directory_dialog.is_open()
2341 && !self.scroll_to_selection
2342 {
2343 scroll_area.show_rows(ui, ui.spacing().interact_size.y, data.len(), |ui, range| {
2347 for item in data.iter_range_mut(range) {
2348 if self.ui_update_central_panel_entry(
2349 ui,
2350 item,
2351 &mut reset_multi_selection,
2352 &mut batch_select_item_b,
2353 ) {
2354 should_return = true;
2355 }
2356 }
2357 });
2358 } else {
2359 scroll_area.show(ui, |ui| {
2365 for item in data.filtered_iter_mut(&self.search_value.clone()) {
2366 if self.ui_update_central_panel_entry(
2367 ui,
2368 item,
2369 &mut reset_multi_selection,
2370 &mut batch_select_item_b,
2371 ) {
2372 should_return = true;
2373 }
2374 }
2375
2376 if let Some(entry) = self.ui_update_create_directory_dialog(ui) {
2377 data.push(entry);
2378 }
2379 });
2380 }
2381 });
2382
2383 if should_return {
2384 return;
2385 }
2386
2387 if reset_multi_selection {
2389 for item in data.filtered_iter_mut(&self.search_value) {
2390 if let Some(selected_item) = &self.selected_item {
2391 if selected_item.path_eq(item) {
2392 continue;
2393 }
2394 }
2395
2396 item.selected = false;
2397 }
2398 }
2399
2400 if let Some(item_b) = batch_select_item_b {
2402 if let Some(item_a) = &self.selected_item {
2403 self.batch_select_between(&mut data, item_a, &item_b);
2404 }
2405 }
2406
2407 self.directory_content = data;
2408 self.scroll_to_selection = false;
2409 }
2410
2411 fn ui_update_central_panel_entry(
2414 &mut self,
2415 ui: &mut egui::Ui,
2416 item: &mut DirectoryEntry,
2417 reset_multi_selection: &mut bool,
2418 batch_select_item_b: &mut Option<DirectoryEntry>,
2419 ) -> bool {
2420 let file_name = item.file_name();
2421 let primary_selected = self.is_primary_selected(item);
2422 let pinned = self.is_pinned(item.as_path());
2423
2424 let icons = if pinned {
2425 format!("{} {} ", item.icon(), self.config.pinned_icon)
2426 } else {
2427 format!("{} ", item.icon())
2428 };
2429
2430 let icons_width = Self::calc_text_width(ui, &icons);
2431
2432 let available_width = ui.available_width() - icons_width - 15.0;
2434
2435 let truncate = self.config.truncate_filenames
2436 && available_width < Self::calc_text_width(ui, file_name);
2437
2438 let text = if truncate {
2439 Self::truncate_filename(ui, item, available_width)
2440 } else {
2441 file_name.to_owned()
2442 };
2443
2444 let mut re =
2445 ui.selectable_label(primary_selected || item.selected, format!("{icons}{text}"));
2446
2447 if truncate {
2448 re = re.on_hover_text(file_name);
2449 }
2450
2451 if item.is_dir() {
2452 self.ui_update_central_panel_path_context_menu(&re, item.as_path());
2453
2454 if re.context_menu_opened() {
2455 self.select_item(item);
2456 }
2457 }
2458
2459 if primary_selected && self.scroll_to_selection {
2460 re.scroll_to_me(Some(egui::Align::Center));
2461 self.scroll_to_selection = false;
2462 }
2463
2464 if re.clicked()
2466 && !ui.input(|i| i.modifiers.command)
2467 && !ui.input(|i| i.modifiers.shift_only())
2468 {
2469 self.select_item(item);
2470
2471 if self.mode == DialogMode::PickMultiple {
2473 *reset_multi_selection = true;
2474 }
2475 }
2476
2477 if self.mode == DialogMode::PickMultiple
2480 && re.clicked()
2481 && ui.input(|i| i.modifiers.command)
2482 {
2483 if primary_selected {
2484 item.selected = false;
2487 self.selected_item = None;
2488 } else {
2489 item.selected = !item.selected;
2490
2491 if item.selected {
2493 self.select_item(item);
2494 }
2495 }
2496 }
2497
2498 if self.mode == DialogMode::PickMultiple
2501 && re.clicked()
2502 && ui.input(|i| i.modifiers.shift_only())
2503 {
2504 if let Some(selected_item) = self.selected_item.clone() {
2505 *batch_select_item_b = Some(selected_item);
2508
2509 item.selected = true;
2511 self.select_item(item);
2512 }
2513 }
2514
2515 if re.double_clicked() && !ui.input(|i| i.modifiers.command) {
2518 if item.is_dir() {
2519 self.load_directory(&item.to_path_buf());
2520 return true;
2521 }
2522
2523 self.select_item(item);
2524
2525 self.submit();
2526 }
2527
2528 false
2529 }
2530
2531 fn ui_update_create_directory_dialog(&mut self, ui: &mut egui::Ui) -> Option<DirectoryEntry> {
2532 self.create_directory_dialog
2533 .update(ui, &self.config)
2534 .directory()
2535 .map(|path| self.process_new_folder(&path))
2536 }
2537
2538 fn batch_select_between(
2541 &self,
2542 directory_content: &mut DirectoryContent,
2543 item_a: &DirectoryEntry,
2544 item_b: &DirectoryEntry,
2545 ) {
2546 let pos_a = directory_content
2548 .filtered_iter(&self.search_value)
2549 .position(|p| p.path_eq(item_a));
2550 let pos_b = directory_content
2551 .filtered_iter(&self.search_value)
2552 .position(|p| p.path_eq(item_b));
2553
2554 if let Some(pos_a) = pos_a {
2557 if let Some(pos_b) = pos_b {
2558 if pos_a == pos_b {
2559 return;
2560 }
2561
2562 let mut min = pos_a;
2565 let mut max = pos_b;
2566
2567 if min > max {
2568 min = pos_b;
2569 max = pos_a;
2570 }
2571
2572 for item in directory_content
2573 .filtered_iter_mut(&self.search_value)
2574 .enumerate()
2575 .filter(|(i, _)| i > &min && i < &max)
2576 .map(|(_, p)| p)
2577 {
2578 item.selected = true;
2579 }
2580 }
2581 }
2582 }
2583
2584 fn ui_button_sized(
2586 &self,
2587 ui: &mut egui::Ui,
2588 enabled: bool,
2589 size: egui::Vec2,
2590 label: &str,
2591 err_tooltip: Option<&str>,
2592 ) -> bool {
2593 let mut clicked = false;
2594
2595 ui.add_enabled_ui(enabled, |ui| {
2596 let response = ui.add_sized(size, egui::Button::new(label));
2597 clicked = response.clicked();
2598
2599 if let Some(err) = err_tooltip {
2600 response.on_disabled_hover_ui(|ui| {
2601 ui.horizontal_wrapped(|ui| {
2602 ui.spacing_mut().item_spacing.x = 0.0;
2603
2604 ui.colored_label(
2605 ui.ctx().style().visuals.error_fg_color,
2606 format!("{} ", self.config.err_icon),
2607 );
2608
2609 ui.label(err);
2610 });
2611 });
2612 }
2613 });
2614
2615 clicked
2616 }
2617
2618 fn ui_update_central_panel_path_context_menu(&mut self, item: &egui::Response, path: &Path) {
2625 if !self.config.show_pinned_folders {
2627 return;
2628 }
2629
2630 item.context_menu(|ui| {
2631 let pinned = self.is_pinned(path);
2632
2633 if pinned {
2634 if ui.button(&self.config.labels.unpin_folder).clicked() {
2635 self.unpin_path(path);
2636 ui.close();
2637 }
2638 } else if ui.button(&self.config.labels.pin_folder).clicked() {
2639 self.pin_path(path.to_path_buf());
2640 ui.close();
2641 }
2642 });
2643 }
2644
2645 fn set_cursor_to_end(re: &egui::Response, data: &str) {
2652 if let Some(mut state) = egui::TextEdit::load_state(&re.ctx, re.id) {
2654 state
2655 .cursor
2656 .set_char_range(Some(CCursorRange::one(CCursor::new(data.len()))));
2657 state.store(&re.ctx, re.id);
2658 }
2659 }
2660
2661 fn calc_char_width(ui: &egui::Ui, char: char) -> f32 {
2663 ui.fonts_mut(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char))
2664 }
2665
2666 fn calc_text_width(ui: &egui::Ui, text: &str) -> f32 {
2669 let mut width = 0.0;
2670
2671 for char in text.chars() {
2672 width += Self::calc_char_width(ui, char);
2673 }
2674
2675 width
2676 }
2677
2678 fn truncate_filename(ui: &egui::Ui, item: &DirectoryEntry, max_length: f32) -> String {
2679 const TRUNCATE_STR: &str = "...";
2680
2681 let path = item.as_path();
2682
2683 let file_stem = if item.is_file() {
2684 path.file_stem().and_then(|f| f.to_str()).unwrap_or("")
2685 } else {
2686 item.file_name()
2687 };
2688
2689 let extension = if item.is_file() {
2690 path.extension().map_or(String::new(), |ext| {
2691 format!(".{}", ext.to_str().unwrap_or(""))
2692 })
2693 } else {
2694 String::new()
2695 };
2696
2697 let extension_width = Self::calc_text_width(ui, &extension);
2698 let reserved = extension_width + Self::calc_text_width(ui, TRUNCATE_STR);
2699
2700 if max_length <= reserved {
2701 return format!("{TRUNCATE_STR}{extension}");
2702 }
2703
2704 let mut width = reserved;
2705 let mut front = String::new();
2706 let mut back = String::new();
2707
2708 for (i, char) in file_stem.chars().enumerate() {
2709 let w = Self::calc_char_width(ui, char);
2710
2711 if width + w > max_length {
2712 break;
2713 }
2714
2715 front.push(char);
2716 width += w;
2717
2718 let back_index = file_stem.len() - i - 1;
2719
2720 if back_index <= i {
2721 break;
2722 }
2723
2724 if let Some(char) = file_stem.chars().nth(back_index) {
2725 let w = Self::calc_char_width(ui, char);
2726
2727 if width + w > max_length {
2728 break;
2729 }
2730
2731 back.push(char);
2732 width += w;
2733 }
2734 }
2735
2736 format!(
2737 "{front}{TRUNCATE_STR}{}{extension}",
2738 back.chars().rev().collect::<String>()
2739 )
2740 }
2741}
2742
2743impl FileDialog {
2745 fn update_keybindings(&mut self, ctx: &egui::Context) {
2747 if let Some(modal) = self.modals.last_mut() {
2750 modal.update_keybindings(&self.config, ctx);
2751 return;
2752 }
2753
2754 let keybindings = std::mem::take(&mut self.config.keybindings);
2755
2756 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.submit, false) {
2757 self.exec_keybinding_submit();
2758 }
2759
2760 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.cancel, false) {
2761 self.exec_keybinding_cancel();
2762 }
2763
2764 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.parent, true) {
2765 self.load_parent_directory();
2766 }
2767
2768 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.back, true) {
2769 self.load_previous_directory();
2770 }
2771
2772 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.forward, true) {
2773 self.load_next_directory();
2774 }
2775
2776 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.reload, true) {
2777 self.refresh();
2778 }
2779
2780 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.new_folder, true) {
2781 self.open_new_folder_dialog();
2782 }
2783
2784 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.edit_path, true) {
2785 self.open_path_edit();
2786 }
2787
2788 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.home_edit_path, true) {
2789 if let Some(dirs) = &self.user_directories {
2790 if let Some(home) = dirs.home_dir() {
2791 self.load_directory(home.to_path_buf().as_path());
2792 self.open_path_edit();
2793 }
2794 }
2795 }
2796
2797 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_up, false) {
2798 self.exec_keybinding_selection_up();
2799
2800 if let Some(id) = ctx.memory(egui::Memory::focused) {
2802 ctx.memory_mut(|w| w.surrender_focus(id));
2803 }
2804 }
2805
2806 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_down, false) {
2807 self.exec_keybinding_selection_down();
2808
2809 if let Some(id) = ctx.memory(egui::Memory::focused) {
2811 ctx.memory_mut(|w| w.surrender_focus(id));
2812 }
2813 }
2814
2815 if FileDialogKeyBindings::any_pressed(ctx, &keybindings.select_all, true)
2816 && self.mode == DialogMode::PickMultiple
2817 {
2818 for item in self.directory_content.filtered_iter_mut(&self.search_value) {
2819 item.selected = true;
2820 }
2821 }
2822
2823 self.config.keybindings = keybindings;
2824 }
2825
2826 fn exec_keybinding_submit(&mut self) {
2828 if self.path_edit_visible {
2829 self.submit_path_edit();
2830 return;
2831 }
2832
2833 if self.create_directory_dialog.is_open() {
2834 if let Some(dir) = self.create_directory_dialog.submit().directory() {
2835 self.process_new_folder(&dir);
2836 }
2837 return;
2838 }
2839
2840 if self.any_focused_last_frame {
2841 return;
2842 }
2843
2844 if let Some(item) = &self.selected_item {
2846 let is_visible = self
2848 .get_dir_content_filtered_iter()
2849 .any(|p| p.path_eq(item));
2850
2851 if is_visible && item.is_dir() {
2852 self.load_directory(&item.to_path_buf());
2853 return;
2854 }
2855 }
2856
2857 self.submit();
2858 }
2859
2860 fn exec_keybinding_cancel(&mut self) {
2862 if self.create_directory_dialog.is_open() {
2880 self.create_directory_dialog.close();
2881 } else if self.path_edit_visible {
2882 self.close_path_edit();
2883 } else if !self.any_focused_last_frame {
2884 self.cancel();
2885 }
2886 }
2887
2888 fn exec_keybinding_selection_up(&mut self) {
2890 if self.directory_content.len() == 0 {
2891 return;
2892 }
2893
2894 self.directory_content.reset_multi_selection();
2895
2896 if let Some(item) = &self.selected_item {
2897 if self.select_next_visible_item_before(&item.clone()) {
2898 return;
2899 }
2900 }
2901
2902 self.select_last_visible_item();
2905 }
2906
2907 fn exec_keybinding_selection_down(&mut self) {
2909 if self.directory_content.len() == 0 {
2910 return;
2911 }
2912
2913 self.directory_content.reset_multi_selection();
2914
2915 if let Some(item) = &self.selected_item {
2916 if self.select_next_visible_item_after(&item.clone()) {
2917 return;
2918 }
2919 }
2920
2921 self.select_first_visible_item();
2924 }
2925}
2926
2927impl FileDialog {
2929 fn get_selected_file_filter(&self) -> Option<&FileFilter> {
2931 self.selected_file_filter
2932 .and_then(|id| self.config.file_filters.iter().find(|p| p.id == id))
2933 }
2934
2935 fn set_default_file_filter(&mut self) {
2937 if let Some(name) = &self.config.default_file_filter {
2938 for filter in &self.config.file_filters {
2939 if filter.name == name.as_str() {
2940 self.selected_file_filter = Some(filter.id);
2941 }
2942 }
2943 }
2944 }
2945
2946 fn select_file_filter(&mut self, filter: Option<FileFilter>) {
2948 self.selected_file_filter = filter.map(|f| f.id);
2949 self.selected_item = None;
2950 self.refresh();
2951 }
2952
2953 fn get_selected_save_extension(&self) -> Option<&SaveExtension> {
2955 self.selected_save_extension
2956 .and_then(|id| self.config.save_extensions.iter().find(|p| p.id == id))
2957 }
2958
2959 fn set_default_save_extension(&mut self) {
2961 let config = std::mem::take(&mut self.config);
2962
2963 if let Some(name) = &config.default_save_extension {
2964 for extension in &config.save_extensions {
2965 if extension.name == name.as_str() {
2966 self.selected_save_extension = Some(extension.id);
2967 self.set_file_name_extension(&extension.file_extension);
2968 }
2969 }
2970 }
2971
2972 self.config = config;
2973 }
2974
2975 fn select_save_extension(&mut self, extension: Option<SaveExtension>) {
2977 if let Some(ex) = extension {
2978 self.selected_save_extension = Some(ex.id);
2979 self.set_file_name_extension(&ex.file_extension);
2980 }
2981
2982 self.selected_item = None;
2983 self.refresh();
2984 }
2985
2986 fn set_file_name_extension(&mut self, extension: &str) {
2988 let dot_count = self.file_name_input.chars().filter(|c| *c == '.').count();
2992 let use_simple = dot_count == 1 && self.file_name_input.chars().nth(0) == Some('.');
2993
2994 let mut p = PathBuf::from(&self.file_name_input);
2995 if !use_simple && p.set_extension(extension) {
2996 self.file_name_input = p.to_string_lossy().into_owned();
2997 } else {
2998 self.file_name_input = format!(".{extension}");
2999 }
3000 }
3001
3002 fn get_dir_content_filtered_iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
3004 self.directory_content.filtered_iter(&self.search_value)
3005 }
3006
3007 fn open_new_folder_dialog(&mut self) {
3009 if let Some(x) = self.current_directory() {
3010 self.create_directory_dialog.open(x.to_path_buf());
3011 }
3012 }
3013
3014 fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry {
3016 let mut entry =
3017 DirectoryEntry::from_path(&self.config, created_dir, &*self.config.file_system);
3018
3019 self.directory_content.push(entry.clone());
3020
3021 self.select_item(&mut entry);
3022
3023 entry
3024 }
3025
3026 fn open_modal(&mut self, modal: Box<dyn FileDialogModal + Send + Sync>) {
3028 self.modals.push(modal);
3029 }
3030
3031 fn exec_modal_action(&mut self, action: ModalAction) {
3033 match action {
3034 ModalAction::None => {}
3035 ModalAction::SaveFile(path) => self.state = DialogState::Picked(path),
3036 }
3037 }
3038
3039 fn canonicalize_path(&self, path: &Path) -> PathBuf {
3042 if self.config.canonicalize_paths {
3043 dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
3044 } else {
3045 path.to_path_buf()
3046 }
3047 }
3048
3049 fn pin_path(&mut self, path: PathBuf) {
3051 let pinned = PinnedFolder::from_path(path);
3052 self.storage.pinned_folders.push(pinned);
3053 }
3054
3055 fn unpin_path(&mut self, path: &Path) {
3057 self.storage
3058 .pinned_folders
3059 .retain(|p| p.path.as_path() != path);
3060 }
3061
3062 fn is_pinned(&self, path: &Path) -> bool {
3064 self.storage
3065 .pinned_folders
3066 .iter()
3067 .any(|p| p.path.as_path() == path)
3068 }
3069
3070 fn begin_rename_pinned_folder(&mut self, pinned: PinnedFolder) {
3072 self.rename_pinned_folder = Some(pinned);
3073 self.rename_pinned_folder_request_focus = true;
3074 }
3075
3076 fn end_rename_pinned_folder(&mut self) {
3079 let renamed = std::mem::take(&mut self.rename_pinned_folder);
3080
3081 if let Some(renamed) = renamed {
3082 let old = self
3083 .storage
3084 .pinned_folders
3085 .iter_mut()
3086 .find(|p| p.path == renamed.path);
3087 if let Some(old) = old {
3088 old.label = renamed.label;
3089 }
3090 }
3091 }
3092
3093 fn is_pinned_folder_being_renamed(&self, pinned: &PinnedFolder) -> bool {
3095 self.rename_pinned_folder
3096 .as_ref()
3097 .is_some_and(|p| p.path == pinned.path)
3098 }
3099
3100 fn is_primary_selected(&self, item: &DirectoryEntry) -> bool {
3101 self.selected_item.as_ref().is_some_and(|x| x.path_eq(item))
3102 }
3103
3104 fn reset(&mut self) {
3107 let user_data = std::mem::take(&mut self.user_data);
3108 let storage = self.storage.clone();
3109 let config = self.config.clone();
3110
3111 *self = Self::with_config(config);
3112 self.storage = storage;
3113 self.user_data = user_data;
3114 }
3115
3116 fn refresh(&mut self) {
3119 self.user_directories = self
3120 .config
3121 .file_system
3122 .user_dirs(self.config.canonicalize_paths);
3123 self.system_disks = self
3124 .config
3125 .file_system
3126 .get_disks(self.config.canonicalize_paths);
3127
3128 self.reload_directory();
3129 }
3130
3131 fn submit(&mut self) {
3133 if !self.is_selection_valid() {
3135 return;
3136 }
3137
3138 self.storage.last_picked_dir = self.current_directory().map(PathBuf::from);
3139
3140 match &self.mode {
3141 DialogMode::PickDirectory | DialogMode::PickFile => {
3142 if let Some(item) = self.selected_item.clone() {
3145 self.state = DialogState::Picked(item.to_path_buf());
3146 }
3147 }
3148 DialogMode::PickMultiple => {
3149 let result: Vec<PathBuf> = self
3150 .selected_entries()
3151 .map(crate::DirectoryEntry::to_path_buf)
3152 .collect();
3153
3154 self.state = DialogState::PickedMultiple(result);
3155 }
3156 DialogMode::SaveFile => {
3157 if let Some(path) = self.current_directory() {
3160 let full_path = path.join(&self.file_name_input);
3161 self.submit_save_file(full_path);
3162 }
3163 }
3164 }
3165 }
3166
3167 fn submit_save_file(&mut self, path: PathBuf) {
3170 if path.exists() {
3171 self.open_modal(Box::new(OverwriteFileModal::new(path)));
3172
3173 return;
3174 }
3175
3176 self.state = DialogState::Picked(path);
3177 }
3178
3179 fn cancel(&mut self) {
3181 self.state = DialogState::Cancelled;
3182 }
3183
3184 fn get_initial_directory(&self) -> PathBuf {
3190 let path = match self.config.opening_mode {
3191 OpeningMode::AlwaysInitialDir => &self.config.initial_directory,
3192 OpeningMode::LastVisitedDir => self
3193 .storage
3194 .last_visited_dir
3195 .as_deref()
3196 .unwrap_or(&self.config.initial_directory),
3197 OpeningMode::LastPickedDir => self
3198 .storage
3199 .last_picked_dir
3200 .as_deref()
3201 .unwrap_or(&self.config.initial_directory),
3202 };
3203
3204 let mut path = self.canonicalize_path(path);
3205
3206 if self.config.file_system.is_file(&path) {
3207 if let Some(parent) = path.parent() {
3208 path = parent.to_path_buf();
3209 }
3210 }
3211
3212 path
3213 }
3214
3215 fn current_directory(&self) -> Option<&Path> {
3217 if let Some(x) = self.directory_stack.iter().nth_back(self.directory_offset) {
3218 return Some(x.as_path());
3219 }
3220
3221 None
3222 }
3223
3224 fn is_selection_valid(&self) -> bool {
3227 match &self.mode {
3228 DialogMode::PickDirectory => self
3229 .selected_item
3230 .as_ref()
3231 .is_some_and(crate::DirectoryEntry::is_dir),
3232 DialogMode::PickFile => self
3233 .selected_item
3234 .as_ref()
3235 .is_some_and(DirectoryEntry::is_file),
3236 DialogMode::PickMultiple => self.get_dir_content_filtered_iter().any(|p| p.selected),
3237 DialogMode::SaveFile => self.file_name_input_error.is_none(),
3238 }
3239 }
3240
3241 fn validate_file_name_input(&self) -> Option<String> {
3245 if self.file_name_input.is_empty() {
3246 return Some(self.config.labels.err_empty_file_name.clone());
3247 }
3248
3249 if let Some(x) = self.current_directory() {
3250 let mut full_path = x.to_path_buf();
3251 full_path.push(self.file_name_input.as_str());
3252
3253 if self.config.file_system.is_dir(&full_path) {
3254 return Some(self.config.labels.err_directory_exists.clone());
3255 }
3256
3257 if !self.config.allow_file_overwrite && self.config.file_system.is_file(&full_path) {
3258 return Some(self.config.labels.err_file_exists.clone());
3259 }
3260 } else {
3261 return Some("Currently not in a directory".to_string());
3263 }
3264
3265 None
3266 }
3267
3268 fn select_item(&mut self, item: &mut DirectoryEntry) {
3271 if self.mode == DialogMode::PickMultiple {
3272 item.selected = true;
3273 }
3274 self.selected_item = Some(item.clone());
3275
3276 if self.mode == DialogMode::SaveFile && item.is_file() {
3277 self.file_name_input = item.file_name().to_string();
3278 self.file_name_input_error = self.validate_file_name_input();
3279 }
3280 }
3281
3282 fn select_next_visible_item_before(&mut self, item: &DirectoryEntry) -> bool {
3287 let mut return_val = false;
3288
3289 self.directory_content.reset_multi_selection();
3290
3291 let mut directory_content = std::mem::take(&mut self.directory_content);
3292 let search_value = std::mem::take(&mut self.search_value);
3293
3294 let index = directory_content
3295 .filtered_iter(&search_value)
3296 .position(|p| p.path_eq(item));
3297
3298 if let Some(index) = index {
3299 if index != 0 {
3300 if let Some(item) = directory_content
3301 .filtered_iter_mut(&search_value)
3302 .nth(index.saturating_sub(1))
3303 {
3304 self.select_item(item);
3305 self.scroll_to_selection = true;
3306 return_val = true;
3307 }
3308 }
3309 }
3310
3311 self.directory_content = directory_content;
3312 self.search_value = search_value;
3313
3314 return_val
3315 }
3316
3317 fn select_next_visible_item_after(&mut self, item: &DirectoryEntry) -> bool {
3322 let mut return_val = false;
3323
3324 self.directory_content.reset_multi_selection();
3325
3326 let mut directory_content = std::mem::take(&mut self.directory_content);
3327 let search_value = std::mem::take(&mut self.search_value);
3328
3329 let index = directory_content
3330 .filtered_iter(&search_value)
3331 .position(|p| p.path_eq(item));
3332
3333 if let Some(index) = index {
3334 if let Some(item) = directory_content
3335 .filtered_iter_mut(&search_value)
3336 .nth(index.saturating_add(1))
3337 {
3338 self.select_item(item);
3339 self.scroll_to_selection = true;
3340 return_val = true;
3341 }
3342 }
3343
3344 self.directory_content = directory_content;
3345 self.search_value = search_value;
3346
3347 return_val
3348 }
3349
3350 fn select_first_visible_item(&mut self) {
3352 self.directory_content.reset_multi_selection();
3353
3354 let mut directory_content = std::mem::take(&mut self.directory_content);
3355
3356 if let Some(item) = directory_content
3357 .filtered_iter_mut(&self.search_value.clone())
3358 .next()
3359 {
3360 self.select_item(item);
3361 self.scroll_to_selection = true;
3362 }
3363
3364 self.directory_content = directory_content;
3365 }
3366
3367 fn select_last_visible_item(&mut self) {
3369 self.directory_content.reset_multi_selection();
3370
3371 let mut directory_content = std::mem::take(&mut self.directory_content);
3372
3373 if let Some(item) = directory_content
3374 .filtered_iter_mut(&self.search_value.clone())
3375 .last()
3376 {
3377 self.select_item(item);
3378 self.scroll_to_selection = true;
3379 }
3380
3381 self.directory_content = directory_content;
3382 }
3383
3384 fn open_path_edit(&mut self) {
3386 let path = self.current_directory().map_or_else(String::new, |path| {
3387 path.to_str().unwrap_or_default().to_string()
3388 });
3389
3390 self.path_edit_value = path;
3391 self.path_edit_activate = true;
3392 self.path_edit_visible = true;
3393 }
3394
3395 fn submit_path_edit(&mut self) {
3397 self.close_path_edit();
3398
3399 let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value));
3400
3401 if self.mode == DialogMode::PickFile && self.config.file_system.is_file(&path) {
3402 self.state = DialogState::Picked(path);
3403 return;
3404 }
3405
3406 if self.mode == DialogMode::SaveFile
3413 && (path.extension().is_some()
3414 || self.config.allow_path_edit_to_save_file_without_extension)
3415 && !self.config.file_system.is_dir(&path)
3416 && path.parent().is_some_and(std::path::Path::exists)
3417 {
3418 self.submit_save_file(path);
3419 return;
3420 }
3421
3422 self.load_directory(&path);
3423 }
3424
3425 const fn close_path_edit(&mut self) {
3428 self.path_edit_visible = false;
3429 }
3430
3431 fn load_next_directory(&mut self) {
3436 if self.directory_offset == 0 {
3437 return;
3439 }
3440
3441 self.directory_offset -= 1;
3442
3443 if let Some(path) = self.current_directory() {
3445 self.load_directory_content(path.to_path_buf().as_path());
3446 }
3447 }
3448
3449 fn load_previous_directory(&mut self) {
3453 if self.directory_offset + 1 >= self.directory_stack.len() {
3454 return;
3456 }
3457
3458 self.directory_offset += 1;
3459
3460 if let Some(path) = self.current_directory() {
3462 self.load_directory_content(path.to_path_buf().as_path());
3463 }
3464 }
3465
3466 fn load_parent_directory(&mut self) {
3470 if let Some(x) = self.current_directory() {
3471 if let Some(x) = x.to_path_buf().parent() {
3472 self.load_directory(x);
3473 }
3474 }
3475 }
3476
3477 fn reload_directory(&mut self) {
3484 if let Some(x) = self.current_directory() {
3485 self.load_directory_content(x.to_path_buf().as_path());
3486 }
3487 }
3488
3489 fn load_directory(&mut self, path: &Path) {
3495 if let Some(x) = self.current_directory() {
3498 if x == path {
3499 return;
3500 }
3501 }
3502
3503 if self.directory_offset != 0 && self.directory_stack.len() > self.directory_offset {
3504 self.directory_stack
3505 .drain(self.directory_stack.len() - self.directory_offset..);
3506 }
3507
3508 self.directory_stack.push(path.to_path_buf());
3509 self.directory_offset = 0;
3510
3511 self.load_directory_content(path);
3512
3513 self.search_value.clear();
3516 }
3517
3518 fn load_directory_content(&mut self, path: &Path) {
3520 self.storage.last_visited_dir = Some(path.to_path_buf());
3521
3522 let selected_file_filter = match self.mode {
3523 DialogMode::PickFile | DialogMode::PickMultiple => self.get_selected_file_filter(),
3524 _ => None,
3525 };
3526
3527 let selected_save_extension = if self.mode == DialogMode::SaveFile {
3528 self.get_selected_save_extension()
3529 .map(|e| e.file_extension.as_str())
3530 } else {
3531 None
3532 };
3533
3534 let filter = DirectoryFilter {
3535 show_files: self.show_files,
3536 show_hidden: self.storage.show_hidden,
3537 show_system_files: self.storage.show_system_files,
3538 file_filter: selected_file_filter.cloned(),
3539 filter_extension: selected_save_extension.map(str::to_string),
3540 };
3541
3542 self.directory_content = DirectoryContent::from_path(
3543 &self.config,
3544 path,
3545 self.config.file_system.clone(),
3546 filter,
3547 );
3548
3549 self.create_directory_dialog.close();
3550 self.scroll_to_selection = true;
3551
3552 if self.mode == DialogMode::SaveFile {
3553 self.file_name_input_error = self.validate_file_name_input();
3554 }
3555 }
3556}