egui_file/
lib.rs

1use std::{
2  borrow::Cow,
3  cmp,
4  fmt::Debug,
5  io::Error,
6  ops::Deref,
7  path::{Path, PathBuf},
8};
9
10use dyn_clone::clone_box;
11use egui::{Align2, Button, Context, Id, Key, Layout, Pos2, RichText, ScrollArea, TextEdit, Ui, Vec2, Vec2b, Window};
12use fs::FileInfo;
13use fs::Fs;
14
15mod fs;
16pub mod vfs;
17pub use vfs::Vfs;
18use vfs::VfsFile;
19
20/// Function that returns `true` if the path is accepted.
21pub type Filter<T> = Box<dyn Fn(&<T as Deref>::Target) -> bool + Send + Sync + 'static>;
22
23#[derive(Debug, PartialEq, Eq, Copy, Clone)]
24/// Dialog state.
25pub enum State {
26  /// Is currently visible.
27  Open,
28  /// Is currently not visible.
29  Closed,
30  /// Was canceled.
31  Cancelled,
32  /// File was selected.
33  Selected,
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37/// Dialog type.
38pub enum DialogType {
39  SelectFolder,
40  OpenFile,
41  SaveFile,
42}
43
44/// `egui` component that represents `OpenFileDialog` or `SaveFileDialog`.
45pub struct FileDialog {
46  /// Current opened path.
47  path: PathBuf,
48
49  /// Editable field with path.
50  path_edit: String,
51
52  /// Selected file path (single select mode).
53  selected_file: Option<Box<dyn VfsFile>>,
54
55  /// Editable field with filename.
56  filename_edit: String,
57
58  /// Dialog title text
59  title: Cow<'static, str>,
60
61  /// Open button text
62  open_button_text: Cow<'static, str>,
63
64  /// Save button text
65  save_button_text: Cow<'static, str>,
66
67  /// Cancel button text
68  cancel_button_text: Cow<'static, str>,
69
70  /// New Folder button text
71  new_folder_button_text: Cow<'static, str>,
72
73  /// New Folder name text
74  new_folder_name_text: Cow<'static, str>,
75
76  /// Rename button text
77  rename_button_text: Cow<'static, str>,
78
79  /// Refresh button hover text
80  refresh_button_hover_text: Cow<'static, str>,
81
82  /// Parent Folder button hover text
83  parent_folder_button_hover_text: Cow<'static, str>,
84
85  /// File label text
86  file_label_text: Cow<'static, str>,
87
88  /// Show Hidden checkbox text
89  show_hidden_checkbox_text: Cow<'static, str>,
90
91  /// Files in directory.
92  files: Result<Vec<Box<dyn VfsFile>>, Error>,
93
94  /// Current dialog state.
95  state: State,
96
97  /// Dialog type.
98  dialog_type: DialogType,
99
100  id: Option<Id>,
101  current_pos: Option<Pos2>,
102  default_pos: Option<Pos2>,
103  default_size: Vec2,
104  anchor: Option<(Align2, Vec2)>,
105  show_files_filter: Filter<PathBuf>,
106  filename_filter: Filter<String>,
107  range_start: Option<usize>,
108  resizable: Vec2b,
109  rename: bool,
110  new_folder: bool,
111  multi_select_enabled: bool,
112  keep_on_top: bool,
113  show_system_files: bool,
114
115  /// Show drive letters on Windows.
116  #[cfg(windows)]
117  show_drives: bool,
118
119  /// Show hidden files on unix systems.
120  #[cfg(unix)]
121  show_hidden: bool,
122
123  fs: Box<dyn Vfs + 'static>,
124}
125
126impl Debug for FileDialog {
127  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128    let mut dbg = f.debug_struct("FileDialog");
129    let dbg = dbg
130      .field("path", &self.path)
131      .field("path_edit", &self.path_edit)
132      .field("selected_file", &self.selected_file)
133      .field("filename_edit", &self.filename_edit)
134      .field("files", &self.files)
135      .field("state", &self.state)
136      .field("dialog_type", &self.dialog_type)
137      .field("current_pos", &self.current_pos)
138      .field("default_pos", &self.default_pos)
139      .field("default_size", &self.default_size)
140      .field("anchor", &self.anchor)
141      .field("resizable", &self.resizable)
142      .field("rename", &self.rename)
143      .field("new_folder", &self.new_folder)
144      .field("multi_select", &self.multi_select_enabled)
145      .field("range_start", &self.range_start)
146      .field("keep_on_top", &self.keep_on_top)
147      .field("show_system_files", &self.show_system_files);
148
149    // Closures don't implement std::fmt::Debug.
150    // let dbg = dbg
151    //   .field("shown_files_filter", &self.shown_files_filter)
152    //   .field("filename_filter", &self.filename_filter);
153
154    #[cfg(unix)]
155    let dbg = dbg.field("show_hidden", &self.show_hidden);
156
157    #[cfg(windows)]
158    let dbg = dbg.field("show_drives", &self.show_drives);
159
160    dbg.finish()
161  }
162}
163
164impl FileDialog {
165  /// Create dialog that prompts the user to select a folder.
166  pub fn select_folder() -> Self {
167    FileDialog::new(DialogType::SelectFolder)
168  }
169
170  /// Create dialog that prompts the user to open a file.
171  pub fn open_file() -> Self {
172    FileDialog::new(DialogType::OpenFile)
173  }
174
175  /// Create dialog that prompts the user to save a file.
176  pub fn save_file() -> Self {
177    FileDialog::new(DialogType::SaveFile)
178  }
179
180  /// Constructs new file dialog.
181  /// - `dialog_type`: Type of file dialog to create. See [DialogType].
182  fn new(dialog_type: DialogType) -> Self {
183    let path = std::env::current_dir().unwrap_or_default();
184    let filename_edit = String::new();
185    let path_edit = path.to_str().unwrap_or_default().to_string();
186    Self {
187      path,
188      path_edit,
189      selected_file: None,
190      filename_edit,
191      title: match dialog_type {
192        DialogType::SelectFolder => "📁  Select Folder",
193        DialogType::OpenFile => "📂  Open File",
194        DialogType::SaveFile => "💾  Save File",
195      }
196      .into(),
197      open_button_text: "Open".into(),
198      save_button_text: "Save".into(),
199      cancel_button_text: "Cancel".into(),
200      new_folder_button_text: "New Folder".into(),
201      new_folder_name_text: "New folder".into(),
202      rename_button_text: "Rename".into(),
203      refresh_button_hover_text: "Refresh".into(),
204      parent_folder_button_hover_text: "Parent Folder".into(),
205      file_label_text: "File:".into(),
206      show_hidden_checkbox_text: "Show Hidden".into(),
207      files: Ok(Vec::new()),
208      state: State::Closed,
209      dialog_type,
210
211      id: None,
212      current_pos: None,
213      default_pos: None,
214      default_size: egui::vec2(512.0, 512.0),
215      anchor: None,
216      show_files_filter: Box::new(|_| true),
217      filename_filter: Box::new(|_| true),
218      resizable: true.into(),
219      rename: true,
220      new_folder: true,
221
222      #[cfg(windows)]
223      show_drives: true,
224
225      #[cfg(unix)]
226      show_hidden: false,
227      multi_select_enabled: false,
228      range_start: None,
229      keep_on_top: false,
230      show_system_files: false,
231      fs: Box::new(Fs {}),
232    }
233  }
234
235  /// Set the initial path. Default is `env::current_dir`.
236  pub fn initial_path(mut self, path: impl Into<PathBuf>) -> Self {
237    self.path = path.into();
238    self
239  }
240
241  /// Set the default file name.
242  pub fn default_filename(mut self, filename: impl Into<String>) -> Self {
243    self.filename_edit = filename.into();
244    self
245  }
246
247  /// Set the window title text.
248  pub fn title(mut self, title: &str) -> Self {
249    self.title = (match self.dialog_type {
250      DialogType::SelectFolder => "📁  ",
251      DialogType::OpenFile => "📂  ",
252      DialogType::SaveFile => "💾  ",
253    }
254    .to_string()
255      + title)
256      .into();
257    self
258  }
259
260  /// Set the open button text.
261  pub fn open_button_text(mut self, text: Cow<'static, str>) -> Self {
262    self.open_button_text = text;
263    self
264  }
265
266  /// Set the save button text.
267  pub fn save_button_text(mut self, text: Cow<'static, str>) -> Self {
268    self.save_button_text = text;
269    self
270  }
271
272  /// Set the cancel button text.
273  pub fn cancel_button_text(mut self, text: Cow<'static, str>) -> Self {
274    self.cancel_button_text = text;
275    self
276  }
277
278  /// Set the new folder button text.
279  pub fn new_folder_button_text(mut self, text: Cow<'static, str>) -> Self {
280    self.new_folder_button_text = text;
281    self
282  }
283
284  /// Set the new folder name text.
285  pub fn new_folder_name_text(mut self, text: Cow<'static, str>) -> Self {
286    self.new_folder_name_text = text;
287    self
288  }
289
290  /// Set the refresh button hover text.
291  pub fn refresh_button_hover_text(mut self, text: Cow<'static, str>) -> Self {
292    self.refresh_button_hover_text = text;
293    self
294  }
295
296  /// Set the parent folder button hover text.
297  pub fn parent_folder_button_hover_text(mut self, text: Cow<'static, str>) -> Self {
298    self.parent_folder_button_hover_text = text;
299    self
300  }
301
302  /// Set the rename button text.
303  pub fn rename_button_text(mut self, text: Cow<'static, str>) -> Self {
304    self.rename_button_text = text;
305    self
306  }
307
308  /// Set the file label text.
309  pub fn file_label_text(mut self, text: Cow<'static, str>) -> Self {
310    self.file_label_text = text;
311    self
312  }
313
314  /// Set the show hidden checkbox text.
315  pub fn show_hidden_checkbox_text(mut self, text: Cow<'static, str>) -> Self {
316    self.show_hidden_checkbox_text = text;
317    self
318  }
319
320  /// Set the window ID. See [egui::Window::id].
321  pub fn id(mut self, id: impl Into<Id>) -> Self {
322    self.id = Some(id.into());
323    self
324  }
325
326  /// Set the window anchor. See [egui::Window::anchor].
327  pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
328    self.anchor = Some((align, offset.into()));
329    self
330  }
331
332  /// Set the window's current position. See [egui::Window::current_pos]
333  pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
334    self.current_pos = Some(current_pos.into());
335    self
336  }
337
338  /// Set the window's default position. See [egui::Window::default_pos].
339  pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
340    self.default_pos = Some(default_pos.into());
341    self
342  }
343
344  /// Set the window's default size. See [egui::Window::default_size].
345  pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
346    self.default_size = default_size.into();
347    self
348  }
349
350  /// Enable/disable resizing the window.  See [egui::Window::resizable].
351  pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
352    self.resizable = resizable.into();
353    self
354  }
355
356  /// Show the Rename button. Default is `true`.
357  pub fn show_rename(mut self, rename: bool) -> Self {
358    self.rename = rename;
359    self
360  }
361
362  /// Show the New Folder button. Default is `true`.
363  pub fn show_new_folder(mut self, new_folder: bool) -> Self {
364    self.new_folder = new_folder;
365    self
366  }
367
368  pub fn multi_select(mut self, multi_select: bool) -> Self {
369    self.multi_select_enabled = multi_select;
370    self
371  }
372
373  pub fn has_multi_select(&self) -> bool {
374    self.multi_select_enabled
375  }
376
377  /// Show the mapped drives on Windows. Default is `true`.
378  #[cfg(windows)]
379  pub fn show_drives(mut self, drives: bool) -> Self {
380    self.show_drives = drives;
381    self
382  }
383
384  /// Set a function to filter listed files.
385  pub fn show_files_filter(mut self, filter: Filter<PathBuf>) -> Self {
386    self.show_files_filter = filter;
387    self
388  }
389
390  /// Set a function to filter the selected filename.
391  pub fn filename_filter(mut self, filter: Filter<String>) -> Self {
392    self.filename_filter = filter;
393    self
394  }
395
396  /// Set to true in order to keep this window on top of other windows. Default is `false`.
397  pub fn keep_on_top(mut self, keep_on_top: bool) -> Self {
398    self.keep_on_top = keep_on_top;
399    self
400  }
401
402  pub fn with_fs(mut self, fs: Box<dyn Vfs>) -> Self {
403    self.fs = fs;
404    self
405  }
406
407  /// Set to true in order to show system files. Default is `false`.
408  pub fn show_system_files(mut self, show_system_files: bool) -> Self {
409    self.show_system_files = show_system_files;
410    self
411  }
412
413  /// Get the dialog type.
414  pub fn dialog_type(&self) -> DialogType {
415    self.dialog_type
416  }
417
418  /// Get the window's visibility.
419  pub fn visible(&self) -> bool {
420    self.state == State::Open
421  }
422
423  /// Opens the dialog.
424  pub fn open(&mut self) {
425    self.state = State::Open;
426    self.refresh();
427  }
428
429  /// Resulting file path.
430  pub fn path(&self) -> Option<&Path> {
431    self.selected_file.as_ref().map(|info| info.path())
432  }
433
434  /// Retrieves multi selection as a vector.
435  pub fn selection(&self) -> Vec<&Path> {
436    match self.files {
437      Ok(ref files) => files
438        .iter()
439        .filter_map(|info| if info.selected() { Some(info.path()) } else { None })
440        .collect(),
441      Err(_) => Vec::new(),
442    }
443  }
444
445  /// Currently mounted directory that is being shown in the dialog box
446  pub fn directory(&self) -> &Path {
447    self.path.as_path()
448  }
449
450  /// Set the dialog's current opened path
451  pub fn set_path(&mut self, path: impl Into<PathBuf>) {
452    self.path = path.into();
453    self.refresh();
454  }
455
456  /// Dialog state.
457  pub fn state(&self) -> State {
458    self.state
459  }
460
461  /// Returns true, if the file selection was confirmed.
462  pub fn selected(&self) -> bool {
463    self.state == State::Selected
464  }
465
466  fn open_selected(&mut self) {
467    if let Some(info) = &self.selected_file {
468      if info.is_dir() {
469        self.set_path(info.path().to_owned());
470      } else if self.dialog_type == DialogType::OpenFile {
471        self.confirm();
472      }
473    } else if self.multi_select_enabled && self.dialog_type == DialogType::OpenFile {
474      self.confirm();
475    }
476  }
477
478  fn confirm(&mut self) {
479    self.state = State::Selected;
480  }
481
482  fn refresh(&mut self) {
483    self.files = self.fs.read_folder(
484      &self.path,
485      self.show_system_files,
486      &self.show_files_filter,
487      #[cfg(unix)]
488      self.show_hidden,
489      #[cfg(windows)]
490      self.show_drives,
491    );
492    self.path_edit = String::from(self.path.to_str().unwrap_or_default());
493    self.select(None);
494    self.selected_file = None;
495  }
496
497  fn select(&mut self, file: Option<Box<dyn VfsFile>>) {
498    if let Some(info) = &file {
499      if !info.is_dir() {
500        info.get_file_name().clone_into(&mut self.filename_edit);
501      }
502    }
503    self.selected_file = file;
504  }
505
506  fn select_reset_multi(&mut self, idx: usize) {
507    if let Ok(files) = &mut self.files {
508      let selected_val = files[idx].selected();
509      for file in files.iter_mut() {
510        file.set_selected(false);
511      }
512      files[idx].set_selected(!selected_val);
513      self.range_start = Some(idx);
514    }
515  }
516
517  fn select_switch_multi(&mut self, idx: usize) {
518    if let Ok(files) = &mut self.files {
519      let old = !files[idx].selected();
520      files[idx].set_selected(old);
521      if files[idx].selected() {
522        self.range_start = Some(idx);
523      } else {
524        self.range_start = None;
525      }
526    } else {
527      self.range_start = None;
528    }
529  }
530
531  fn select_range(&mut self, idx: usize) {
532    if let Ok(files) = &mut self.files {
533      if let Some(range_start) = self.range_start {
534        let range = cmp::min(idx, range_start)..=cmp::max(idx, range_start);
535        for i in range {
536          files[i].set_selected(true);
537        }
538      }
539    }
540  }
541
542  fn can_save(&self) -> bool {
543    !self.filename_edit.is_empty() && (self.filename_filter)(self.filename_edit.as_str())
544  }
545
546  fn can_open(&self) -> bool {
547    if self.multi_select_enabled {
548      if let Ok(files) = &self.files {
549        for file in files {
550          if file.selected() && (self.filename_filter)(file.get_file_name()) {
551            return true;
552          }
553        }
554      }
555      false
556    } else {
557      !self.filename_edit.is_empty() && (self.filename_filter)(self.filename_edit.as_str())
558    }
559  }
560
561  fn can_rename(&self) -> bool {
562    if !self.filename_edit.is_empty() {
563      if let Some(file) = &self.selected_file {
564        return file.get_file_name() != self.filename_edit;
565      }
566    }
567    false
568  }
569
570  /// Shows the dialog if it is open. It is also responsible for state management.
571  /// Should be called every ui update.
572  pub fn show(&mut self, ctx: &Context) -> &Self {
573    self.state = match self.state {
574      State::Open => {
575        if ctx.input(|state| state.key_pressed(Key::Escape)) {
576          self.state = State::Cancelled;
577        }
578
579        let mut is_open = true;
580        self.ui(ctx, &mut is_open);
581        match is_open {
582          true => self.state,
583          false => State::Cancelled,
584        }
585      }
586      _ => State::Closed,
587    };
588
589    self
590  }
591
592  fn ui(&mut self, ctx: &Context, is_open: &mut bool) {
593    let mut window = Window::new(RichText::new(self.title.as_ref()).strong())
594      .open(is_open)
595      .default_size(self.default_size)
596      .resizable(self.resizable)
597      .collapsible(false);
598
599    if let Some(id) = self.id {
600      window = window.id(id);
601    }
602
603    if let Some((align, offset)) = self.anchor {
604      window = window.anchor(align, offset);
605    }
606
607    if let Some(current_pos) = self.current_pos {
608      window = window.current_pos(current_pos);
609    }
610
611    if let Some(default_pos) = self.default_pos {
612      window = window.default_pos(default_pos);
613    }
614
615    window.show(ctx, |ui| {
616      if self.keep_on_top {
617        ui.ctx().move_to_top(ui.layer_id());
618      }
619      self.ui_in_window(ui)
620    });
621  }
622
623  fn ui_in_window(&mut self, ui: &mut Ui) {
624    enum Command {
625      Cancel,
626      CreateDirectory,
627      Folder,
628      Open(Box<dyn VfsFile>),
629      OpenSelected,
630      BrowseDirectory(Box<dyn VfsFile>),
631      Refresh,
632      Rename(PathBuf, PathBuf),
633      Save(Box<dyn VfsFile>),
634      Select(Box<dyn VfsFile>),
635      MultiSelectRange(usize),
636      MultiSelect(usize),
637      MultiSelectSwitch(usize),
638      UpDirectory,
639    }
640    let mut command: Option<Command> = None;
641
642    // Top directory field with buttons.
643    egui::TopBottomPanel::top("egui_file_top").show_inside(ui, |ui| {
644      ui.horizontal(|ui| {
645        ui.add_enabled_ui(self.path.parent().is_some(), |ui| {
646          let response = ui
647            .add_sized([23.0, 20.0], Button::new("⬆"))
648            .on_hover_text(self.parent_folder_button_hover_text.as_ref());
649          if response.clicked() {
650            command = Some(Command::UpDirectory);
651          }
652        });
653        ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
654          let response = ui
655            .add_sized([23.0, 20.0], Button::new("⟲"))
656            .on_hover_text(self.refresh_button_hover_text.as_ref());
657          if response.clicked() {
658            command = Some(Command::Refresh);
659          }
660
661          let response = ui.add_sized(ui.available_size(), TextEdit::singleline(&mut self.path_edit));
662
663          if response.lost_focus() {
664            let path = PathBuf::from(&self.path_edit);
665            command = Some(Command::Open(Box::new(FileInfo::new(path))));
666          }
667        });
668      });
669      ui.add_space(ui.spacing().item_spacing.y);
670    });
671
672    // Bottom file field.
673    egui::TopBottomPanel::bottom("egui_file_bottom").show_inside(ui, |ui| {
674      ui.add_space(ui.spacing().item_spacing.y * 2.0);
675      ui.horizontal(|ui| {
676        ui.label(self.file_label_text.as_ref());
677        ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
678          if self.new_folder && ui.button(self.new_folder_button_text.as_ref()).clicked() {
679            command = Some(Command::CreateDirectory);
680          }
681
682          if self.rename {
683            ui.add_enabled_ui(self.can_rename(), |ui| {
684              if ui.button(self.rename_button_text.as_ref()).clicked() {
685                if let Some(from) = self.selected_file.clone() {
686                  let to = from.path().with_file_name(&self.filename_edit);
687                  command = Some(Command::Rename(from.path().to_owned(), to));
688                }
689              }
690            });
691          }
692
693          let response = ui.add_sized(ui.available_size(), TextEdit::singleline(&mut self.filename_edit));
694
695          if response.lost_focus() {
696            let ctx = response.ctx;
697            let enter_pressed = ctx.input(|state| state.key_pressed(Key::Enter));
698
699            if enter_pressed && (self.filename_filter)(self.filename_edit.as_str()) {
700              let path = self.path.join(&self.filename_edit);
701              match self.dialog_type {
702                DialogType::SelectFolder => command = Some(Command::Folder),
703                DialogType::OpenFile => {
704                  if path.exists() {
705                    command = Some(Command::Open(Box::new(FileInfo::new(path))));
706                  }
707                }
708                DialogType::SaveFile => {
709                  let file_info = Box::new(FileInfo::new(path));
710                  command = Some(match file_info.is_dir() {
711                    true => Command::Open(file_info),
712                    false => Command::Save(file_info),
713                  });
714                }
715              }
716            }
717          }
718        });
719      });
720
721      ui.add_space(ui.spacing().item_spacing.y);
722
723      // Confirm, Cancel buttons.
724      ui.horizontal(|ui| {
725        #[cfg(unix)]
726        if ui
727          .checkbox(&mut self.show_hidden, self.show_hidden_checkbox_text.as_ref())
728          .changed()
729        {
730          self.refresh();
731        }
732
733        ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
734          if ui.button(self.cancel_button_text.as_ref()).clicked() {
735            command = Some(Command::Cancel);
736          }
737
738          match self.dialog_type {
739            DialogType::SelectFolder => {
740              ui.horizontal(|ui| {
741                if ui.button(self.open_button_text.as_ref()).clicked() {
742                  command = Some(Command::Folder);
743                };
744              });
745            }
746            DialogType::OpenFile => {
747              ui.horizontal(|ui| {
748                if !self.can_open() {
749                  ui.disable();
750                }
751
752                if ui.button(self.open_button_text.as_ref()).clicked() {
753                  command = Some(Command::OpenSelected);
754                };
755              });
756            }
757            DialogType::SaveFile => {
758              let should_open_directory = match &self.selected_file {
759                Some(file) => file.is_dir(),
760                None => false,
761              };
762
763              if should_open_directory {
764                if ui.button(self.open_button_text.as_ref()).clicked() {
765                  command = Some(Command::OpenSelected);
766                };
767              } else {
768                ui.horizontal(|ui| {
769                  if !self.can_save() {
770                    ui.disable();
771                  }
772
773                  if ui.button(self.save_button_text.as_ref()).clicked() {
774                    let filename = &self.filename_edit;
775                    let path = self.path.join(filename);
776                    command = Some(Command::Save(Box::new(FileInfo::new(path))));
777                  };
778                });
779              }
780            }
781          }
782        });
783      });
784    });
785
786    // File list.
787    egui::CentralPanel::default().show_inside(ui, |ui| {
788      ScrollArea::vertical().show_rows(
789        ui,
790        ui.text_style_height(&egui::TextStyle::Body),
791        self.files.as_ref().map_or(0, |files| files.len()),
792        |ui, range| match self.files.as_ref() {
793          Ok(files) => {
794            ui.with_layout(ui.layout().with_cross_justify(true), |ui| {
795              let selected = self.selected_file.as_ref().map(|info| info.path());
796              let range_start = range.start;
797
798              for (n, info) in files[range].iter().enumerate() {
799                let idx = n + range_start;
800                let label = match info.is_dir() {
801                  true => "🗀 ",
802                  false => "🗋 ",
803                }
804                .to_string()
805                  + info.get_file_name();
806
807                let is_selected = if self.multi_select_enabled {
808                  files[idx].selected()
809                } else {
810                  Some(info.path()) == selected
811                };
812                let response = ui.selectable_label(is_selected, label);
813                if response.clicked() {
814                  if self.multi_select_enabled {
815                    if ui.input(|i| i.modifiers.shift) {
816                      command = Some(Command::MultiSelectRange(idx))
817                    } else if ui.input(|i| i.modifiers.ctrl) {
818                      command = Some(Command::MultiSelectSwitch(idx))
819                    } else {
820                      command = Some(Command::MultiSelect(idx))
821                    }
822                  } else {
823                    command = Some(Command::Select(dyn_clone::clone_box(info.as_ref())));
824                  }
825                }
826
827                if response.double_clicked() {
828                  match self.dialog_type {
829                    DialogType::SelectFolder => {
830                      // Always open folder on double click, otherwise SelectFolder cant enter sub-folders.
831                      command = Some(Command::OpenSelected);
832                    }
833                    // Open or save file only if name matches filter.
834                    DialogType::OpenFile => {
835                      if info.is_dir() {
836                        command = Some(Command::BrowseDirectory(clone_box(info.as_ref())));
837                      } else if (self.filename_filter)(self.filename_edit.as_str()) {
838                        command = Some(Command::Open(clone_box(info.as_ref())));
839                      }
840                    }
841                    DialogType::SaveFile => {
842                      if info.is_dir() {
843                        command = Some(Command::OpenSelected);
844                      } else if (self.filename_filter)(self.filename_edit.as_str()) {
845                        command = Some(Command::Save(info.clone()));
846                      }
847                    }
848                  }
849                }
850              }
851            })
852            .response
853          }
854          Err(e) => ui.label(e.to_string()),
855        },
856      );
857    });
858
859    if let Some(command) = command {
860      match command {
861        Command::Select(info) => self.select(Some(info)),
862        Command::MultiSelect(idx) => self.select_reset_multi(idx),
863        Command::MultiSelectRange(idx) => self.select_range(idx),
864        Command::MultiSelectSwitch(idx) => self.select_switch_multi(idx),
865        Command::Folder => {
866          let path = self.get_folder().to_owned();
867          self.selected_file = Some(Box::new(FileInfo::new(path)));
868          self.confirm();
869        }
870        Command::Open(path) => {
871          self.select(Some(path));
872          self.open_selected();
873        }
874        Command::OpenSelected => self.open_selected(),
875        Command::BrowseDirectory(dir) => {
876          self.selected_file = Some(dir);
877          self.open_selected();
878        }
879        Command::Save(file) => {
880          self.selected_file = Some(file);
881          self.confirm();
882        }
883        Command::Cancel => self.state = State::Cancelled,
884        Command::Refresh => self.refresh(),
885        Command::UpDirectory => {
886          if self.path.pop() {
887            self.refresh();
888          }
889        }
890        Command::CreateDirectory => {
891          let mut path = self.path.clone();
892          let name = match self.filename_edit.is_empty() {
893            true => self.new_folder_name_text.as_ref(),
894            false => self.filename_edit.as_ref(),
895          };
896          path.push(name);
897          match self.fs.create_dir(&path) {
898            Ok(_) => {
899              self.refresh();
900              self.select(Some(Box::new(FileInfo::new(path))));
901              // TODO: scroll to selected?
902            }
903            Err(err) => println!("Error while creating directory: {err}"),
904          }
905        }
906        Command::Rename(from, to) => match self.fs.rename(from.as_path(), to.as_path()) {
907          Ok(_) => {
908            self.refresh();
909            self.select(Some(Box::new(FileInfo::new(to))));
910          }
911          Err(err) => println!("Error while renaming: {err}"),
912        },
913      };
914    }
915  }
916
917  fn get_folder(&self) -> &Path {
918    if let Some(info) = &self.selected_file {
919      if info.is_dir() {
920        return info.path();
921      }
922    }
923
924    // No selected file or it's not a folder, so use the current path.
925    &self.path
926  }
927}