1use bytesize::ByteSize;
9use easy_imgui::{self as imgui, CustomRectIndex, id, lbl, lbl_id};
10pub use glob::{self, Pattern};
11use image::DynamicImage;
12use std::io::Result;
13use std::{
14 borrow::Cow,
15 ffi::{OsStr, OsString},
16 fs::DirEntry,
17 path::{Path, PathBuf},
18};
19use time::macros::format_description;
20
21#[cfg(feature = "tr")]
22include!(concat!(env!("OUT_DIR"), "/locale/translators.rs"));
23
24#[cfg(feature = "tr")]
26pub fn set_locale(locale: &str) {
27 translators::set_locale(locale);
28}
29
30#[cfg(not(feature = "tr"))]
32pub fn set_locale(_locale: &str) {}
33
34#[cfg(feature = "tr")]
35use tr::tr;
36
37#[cfg(not(feature = "tr"))]
38macro_rules! tr {
39 ($($args:tt)*) => { format!($($args)*) };
40}
41
42pub trait DirEnum {
44 fn roots(&self) -> impl Iterator<Item = FileEntry>;
53 fn read_dir<'s>(
55 &'s self,
56 path: &Path,
57 ) -> std::io::Result<impl Iterator<Item = FileEntry> + use<'s, Self>>;
58 fn absolute(&self, path: &Path) -> std::io::Result<PathBuf>;
62}
63
64pub trait DynDirEnum {
68 fn dyn_roots(&self) -> Box<dyn Iterator<Item = FileEntry> + '_>;
69 fn dyn_read_dir<'s>(
70 &'s self,
71 path: &Path,
72 ) -> std::io::Result<Box<dyn Iterator<Item = FileEntry> + 's>>;
73 fn dyn_absolute(&self, path: &Path) -> std::io::Result<PathBuf>;
74}
75
76impl<T> DirEnum for T
77where
78 T: AsRef<dyn DynDirEnum>,
79{
80 fn roots(&self) -> impl Iterator<Item = FileEntry> {
81 self.as_ref().dyn_roots()
82 }
83 fn read_dir<'s>(
84 &'s self,
85 path: &Path,
86 ) -> std::io::Result<impl Iterator<Item = FileEntry> + use<'s, T>> {
87 self.as_ref().dyn_read_dir(path)
88 }
89 fn absolute(&self, path: &Path) -> std::io::Result<PathBuf> {
90 self.as_ref().dyn_absolute(path)
91 }
92}
93
94impl<T: DirEnum> DynDirEnum for T {
95 fn dyn_roots(&self) -> Box<dyn Iterator<Item = FileEntry> + '_> {
96 Box::new(self.roots())
97 }
98 fn dyn_read_dir<'s>(
99 &'s self,
100 path: &Path,
101 ) -> std::io::Result<Box<dyn Iterator<Item = FileEntry> + 's>> {
102 Ok(Box::new(self.read_dir(path)?))
103 }
104 fn dyn_absolute(&self, path: &Path) -> std::io::Result<PathBuf> {
105 self.absolute(path)
106 }
107}
108
109pub fn box_dir_enum(t: impl DirEnum + 'static) -> Box<dyn DynDirEnum> {
113 Box::new(t)
114}
115
116#[cfg(feature = "zip")]
117mod zipdirenum;
118#[cfg(feature = "zip")]
119pub use zipdirenum::{FileSystemDirEnumWithZip, ZipAnalyzeResult, ZipDirEnum};
120
121#[derive(Default)]
125pub struct FileSystemDirEnum;
126
127impl DirEnum for FileSystemDirEnum {
128 #[cfg(not(target_os = "windows"))]
129 fn roots(&self) -> impl Iterator<Item = FileEntry> {
130 std::iter::empty()
131 }
132 #[cfg(target_os = "windows")]
133 fn roots(&self) -> impl Iterator<Item = FileEntry> {
134 struct Roots {
135 drives: u32,
136 letter: u8,
137 }
138 impl Iterator for Roots {
139 type Item = FileEntry;
140
141 fn next(&mut self) -> Option<FileEntry> {
142 while self.letter < 26 {
143 let bit = 1 << self.letter;
145 let drive = char::from(b'A' + self.letter);
146 self.letter += 1;
147 if (self.drives & bit) != 0 {
148 return Some(FileEntry {
149 name: format!("{}:\\", drive).into(),
150 kind: FileEntryKind::Root,
151 size: None,
152 modified: None,
153 hidden: false,
154 });
155 }
156 }
157 None
158 }
159 }
160 let drives = unsafe { windows::Win32::Storage::FileSystem::GetLogicalDrives() };
161 Roots { drives, letter: 0 }
162 }
163
164 fn read_dir<'s>(
165 &'s self,
166 path: &Path,
167 ) -> std::io::Result<impl Iterator<Item = FileEntry> + use<'s>> {
168 let rd = path.read_dir()?;
169 Ok(rd.filter_map(|e| {
170 let e = e.ok()?;
171 Some(FileEntry::new(&e))
172 }))
173 }
174 fn absolute(&self, path: &Path) -> std::io::Result<PathBuf> {
175 std::path::absolute(path)
176 }
177}
178
179pub type FileChooser = FileChooserD<FileSystemDirEnum>;
181
182pub struct FileChooserD<D: DirEnum> {
186 dir_enum: D,
187 path: PathBuf,
188 flags: Flags,
189 entries: Vec<FileEntry>,
190 selected: Option<usize>,
191 sort_dirty: bool,
192 visible_dirty: bool,
193 scroll_dirty: bool,
194 show_hidden: bool,
195 popup_dirs: Vec<(PathBuf, bool)>,
196 search_term: String,
197 file_name: OsString,
198 active_filter_idx: usize,
200 filters: Vec<Filter>,
201 read_only: bool,
202 visible_entries: Vec<usize>,
203 path_size_overflow: f32,
204}
205
206pub enum Output {
208 Continue,
212 Cancel,
216 Ok,
221}
222
223impl<D: DirEnum> std::fmt::Debug for FileChooserD<D> {
224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 f.debug_struct("FileChooser")
226 .field("path", &self.path())
227 .field("file_name", &self.file_name())
228 .field("active_filter", &self.active_filter())
229 .field("read_only", &self.read_only())
230 .finish()
231 }
232}
233
234#[allow(dead_code)]
238#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
239pub enum FileEntryKind {
240 Parent,
242 Directory,
244 File,
246 Root,
248}
249
250#[derive(Debug)]
252pub struct FileEntry {
253 pub name: OsString,
255 pub kind: FileEntryKind,
257 pub size: Option<u64>,
259 pub modified: Option<time::OffsetDateTime>,
261 pub hidden: bool,
263}
264
265#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
271pub struct FilterId(pub i32);
272
273#[derive(Default, Debug, Clone)]
275pub struct Filter {
276 pub id: FilterId,
280 pub text: String,
282 pub globs: Vec<glob::Pattern>,
288}
289
290impl Filter {
291 pub fn matches(&self, name: impl AsRef<OsStr>) -> bool {
293 let name = name.as_ref().to_string_lossy();
294 let opts = glob::MatchOptions {
295 case_sensitive: false,
296 ..Default::default()
297 };
298 self.globs.is_empty() || self.globs.iter().any(|glob| glob.matches_with(&name, opts))
300 }
301}
302
303bitflags::bitflags! {
304 pub struct Flags: u32 {
306 const SHOW_READ_ONLY = 1;
308 const MUST_EXIST = 2;
310 }
311}
312
313impl<D: DirEnum + Default> Default for FileChooserD<D> {
314 fn default() -> Self {
315 FileChooserD::with_dir_enum(D::default())
316 }
317}
318
319impl<D: DirEnum + Default> FileChooserD<D> {
320 pub fn new() -> Self {
322 Self::default()
323 }
324}
325
326#[derive(Debug, Copy, Clone, PartialEq, Eq)]
327enum ApplicablePathRes {
328 Directory,
329 ExistingFile,
330 NewEntry,
331 Forbidden,
332}
333
334impl<D: DirEnum> FileChooserD<D> {
335 pub fn with_dir_enum(dir_enum: D) -> FileChooserD<D> {
337 FileChooserD {
338 dir_enum,
339 path: PathBuf::default(),
340 flags: Flags::empty(),
341 entries: Vec::new(),
342 selected: None,
343 sort_dirty: false,
344 visible_dirty: false,
345 scroll_dirty: false,
346 show_hidden: false,
347 popup_dirs: Vec::new(),
348 search_term: String::new(),
349 file_name: OsString::new(),
350 active_filter_idx: 0,
351 filters: Vec::new(),
352 read_only: false,
353 visible_entries: Vec::new(),
354 path_size_overflow: 0.0,
355 }
356 }
357 pub fn add_flags(&mut self, flags: Flags) {
359 self.flags |= flags;
360 }
361 pub fn remove_flags(&mut self, flags: Flags) {
363 self.flags &= !flags;
364 }
365 pub fn set_path(&mut self, path: impl AsRef<Path>) -> Result<()> {
369 let path = self.dir_enum.absolute(path.as_ref())?;
370 self.selected = None;
371 let mut entries = std::mem::take(&mut self.entries);
373 entries.clear();
374 let mut add_entry = |entry: FileEntry| {
375 if self.file_name == entry.name {
376 self.selected = Some(entries.len());
377 }
378 entries.push(entry);
379 };
380
381 match path.parent() {
382 Some(t) if !t.as_os_str().is_empty() => {
383 add_entry(FileEntry::dot_dot());
384 }
385 _ => {}
386 }
387
388 for fe in self.dir_enum.read_dir(&path)? {
389 add_entry(fe);
390 }
391 self.path = path;
392 self.entries = entries;
393 self.sort_dirty = true;
394 self.visible_dirty = true;
395 self.scroll_dirty = true;
396 self.popup_dirs.clear();
397 self.search_term.clear();
398 self.path_size_overflow = 0.0;
399 Ok(())
400 }
401
402 pub fn set_file_name(&mut self, file_name: impl AsRef<OsStr>) {
406 self.file_name = file_name.as_ref().to_owned();
407
408 if !self.file_name.is_empty() {
409 for (i_f, f) in self.filters.iter().enumerate() {
411 if f.matches(&self.file_name) {
412 self.active_filter_idx = i_f;
413 break;
414 }
415 }
416 for (i_entry, entry) in self.entries.iter().enumerate() {
418 if entry.name == self.file_name {
419 self.selected = Some(i_entry);
420 break;
421 }
422 }
423 }
424 }
425
426 pub fn path(&self) -> &Path {
428 &self.path
429 }
430
431 pub fn selected_entry(&self) -> Option<&FileEntry> {
433 let i_sel = self.selected?;
434 Some(&self.entries[i_sel])
435 }
436
437 pub fn file_name(&self) -> &OsStr {
442 if let Some(entry) = self.selected_entry()
447 && entry.kind == FileEntryKind::File
448 && self.file_name == *entry.name.to_string_lossy()
449 {
450 return &entry.name;
451 }
452 &self.file_name
453 }
454
455 pub fn active_filter(&self) -> Option<FilterId> {
459 if self.filters.is_empty() {
460 None
461 } else {
462 Some(self.filters[self.active_filter_idx].id)
463 }
464 }
465 pub fn set_active_filter(&mut self, filter_id: FilterId) {
470 if let Some(p) = self.filters.iter().position(|f| f.id == filter_id) {
471 self.active_filter_idx = p;
472 }
473 }
474 pub fn read_only(&self) -> bool {
477 self.read_only
478 }
479 pub fn full_path(&self, default_extension: Option<&str>) -> PathBuf {
485 let (mut res, exists) = self.applicable_path();
486 if let (None, Some(new_ext), ApplicablePathRes::NewEntry) =
487 (res.extension(), default_extension, exists)
488 {
489 res.set_extension(new_ext);
490 }
491 res
492 }
493
494 pub fn add_filter(&mut self, filter: Filter) {
496 self.filters.push(filter);
497 if !self.file_name.is_empty()
498 && !self.filters[self.active_filter_idx].matches(&self.file_name)
499 {
500 if self.filters.last().unwrap().matches(&self.file_name) {
502 self.active_filter_idx = self.filters.len() - 1;
503 }
504 }
505 self.visible_dirty = true;
506 }
507 pub fn do_ui<'a, A, Params, Preview>(&mut self, ui: &'a imgui::Ui<A>, params: Params) -> Output
513 where
514 Params: Into<UiParameters<'a, Preview>>,
515 Preview: PreviewBuilder<A, D>,
516 {
517 if self.entries.is_empty() {
518 let res = self.set_path(".");
519 if res.is_err() {
520 return Output::Cancel;
521 }
522 }
523
524 let UiParameters { atlas, mut preview } = params.into();
525 let mut next_path = None;
526 let mut output = Output::Continue;
527
528 let mut my_path = PathBuf::new();
529
530 let style = ui.style();
531 ui.child_config(lbl("path"))
532 .size(imgui::Vector2::new(
534 0.0,
535 ui.get_frame_height_with_spacing()
536 + if self.path_size_overflow < 0.0 {
537 style.ScrollbarSize
538 } else {
539 0.0
540 },
541 ))
542 .window_flags(imgui::WindowFlags::HorizontalScrollbar)
543 .with(|| {
544 let mut roots = self.dir_enum.roots();
545 let r0 = roots.next();
546 if let Some(r0) = r0 {
547 let scale = ui.get_font_size() / 16.0;
548 if ui
549 .image_button_with_custom_rect_config(id("::super"), atlas.mypc_rr, scale)
550 .build()
551 {
552 self.entries.clear();
554 self.entries.push(r0);
555 self.entries.extend(roots);
556 self.path = PathBuf::default();
557 self.selected = None;
558 self.sort_dirty = true;
559 self.visible_dirty = true;
560 self.scroll_dirty = true;
561 self.popup_dirs.clear();
562 self.search_term.clear();
563 }
564 ui.same_line();
565 }
566
567 let mut my_disk = None;
568 'component: for component in self.path.components() {
569 let piece = 'piece: {
570 match component {
573 std::path::Component::Prefix(prefix) => match prefix.kind() {
574 std::path::Prefix::VerbatimDisk(disk)
575 | std::path::Prefix::Disk(disk) => {
576 my_disk = Some(char::from(disk));
577 continue 'component;
578 }
579 _ => (),
580 },
581 std::path::Component::RootDir => {
582 if let Some(my_disk) = my_disk.take() {
583 let drive_root = format!(
584 "{}:{}",
585 my_disk,
586 component.as_os_str().to_string_lossy()
587 );
588 let drive_root = OsString::from(drive_root);
589 break 'piece Cow::Owned(drive_root);
590 }
591 }
592 _ => (),
593 }
594 Cow::Borrowed(component.as_os_str())
595 };
596
597 my_path.push(&piece);
598 if ui
599 .button_config(lbl_id(piece.to_string_lossy(), my_path.to_string_lossy()))
600 .build()
601 {
602 next_path = Some(my_path.clone());
603 }
604 if ui.is_mouse_released(imgui::MouseButton::Right)
605 && ui.is_item_hovered_ex(imgui::HoveredFlags::AllowWhenBlockedByPopup)
606 {
607 let mut popup_dirs = Vec::new();
608 if let Some(parent) = my_path.parent() {
609 if let Ok(subdir) = self.dir_enum.read_dir(parent) {
610 for d in subdir {
611 if d.kind != FileEntryKind::Directory {
612 continue;
613 }
614 if d.hidden && !self.show_hidden {
615 continue;
616 }
617
618 let full_d = parent.join(d.name);
619 let sel = full_d == my_path;
620 popup_dirs.push((full_d, sel));
621 }
622 }
623 } else {
624 for root in self.dir_enum.roots() {
626 let root = PathBuf::from(root.name);
627 let sel = root == my_path;
628 popup_dirs.push((root, sel));
629 }
630 }
631 popup_dirs.sort_by(|a, b| a.0.cmp(&b.0));
632 self.popup_dirs = popup_dirs;
633 if !self.popup_dirs.is_empty() {
634 ui.open_popup(id(c"popup_dirs"));
635 }
636 }
637 ui.same_line();
638 }
639 let path_size_overflow = ui.get_content_region_avail().x;
642 if path_size_overflow != self.path_size_overflow {
643 ui.set_scroll_here_x(1.0);
644 self.path_size_overflow = path_size_overflow;
645 }
646
647 ui.popup_config(id(c"popup_dirs")).with(|| {
648 for (dir, sel) in &self.popup_dirs {
649 let name = dir.file_name().unwrap_or_else(|| dir.as_os_str());
650 if ui
651 .selectable_config(lbl_id(
652 name.to_string_lossy(),
653 dir.display().to_string(),
654 ))
655 .selected(*sel)
656 .build()
657 {
658 next_path = Some(dir.clone());
659 }
660 }
661 });
662 });
663
664 ui.text(&tr!("Search"));
665 ui.same_line();
666 ui.set_next_item_width(-ui.get_frame_height_with_spacing() - style.ItemSpacing.x);
667 if ui
668 .input_text_config(lbl_id(c"", c"Search"), &mut self.search_term)
669 .build()
670 {
671 self.visible_dirty = true;
672 }
673
674 ui.same_line();
675 ui.with_push(
676 self.show_hidden.then_some(((
677 imgui::ColorId::Button,
678 style.color(imgui::ColorId::ButtonActive),
679 ),)),
680 || {
681 let scale = ui.get_font_size() / 16.0;
682 if ui
683 .image_button_with_custom_rect_config(id("hidden"), atlas.hidden_rr, scale)
684 .build()
685 {
686 self.show_hidden ^= true;
687 self.visible_dirty = true;
688 }
689 },
690 );
691
692 let style = ui.style();
693 let reserve = 2.0 * ui.get_frame_height_with_spacing();
695 let preview_width = preview.width();
696 ui.table_config(lbl("FileChooser"), 4)
697 .flags(
698 imgui::TableFlags::RowBg
699 | imgui::TableFlags::ScrollY
700 | imgui::TableFlags::Resizable
701 | imgui::TableFlags::Sortable
702 | imgui::TableFlags::SizingFixedFit,
703 )
704 .outer_size(imgui::Vector2::new(-preview_width, -reserve))
705 .with(|| {
706 let pad = ui.style().FramePadding;
707 ui.table_setup_column("", imgui::TableColumnFlags::None, 0.00, 0);
708 ui.table_setup_column(
709 tr!("Name"),
710 imgui::TableColumnFlags::WidthStretch | imgui::TableColumnFlags::DefaultSort,
711 0.0,
712 0,
713 );
714 ui.table_setup_column(
715 tr!("Size"),
716 imgui::TableColumnFlags::WidthFixed,
717 ui.calc_text_size("999.9 GiB").x + 2.0 * pad.x,
718 0,
719 );
720 ui.table_setup_column(
721 tr!("Modified"),
722 imgui::TableColumnFlags::WidthFixed,
723 ui.calc_text_size("2024-12-31 23:59:59").x + 2.0 * pad.x,
724 0,
725 );
726 ui.table_setup_scroll_freeze(0, 1);
727 ui.table_headers_row();
728
729 ui.table_with_sort_specs_always(|dirty, specs| {
734 if dirty || self.sort_dirty {
735 self.sort_dirty = false;
736 self.resort_entries(specs);
737 }
738 false
739 });
740 if self.visible_dirty {
741 self.visible_dirty = false;
742 self.scroll_dirty = true;
743 self.recompute_visible_entries();
744 }
745
746 let mut clipper = ui.list_clipper(self.visible_entries.len());
747 if let (Some(i_sel), true) = (self.selected, self.scroll_dirty)
751 && let Some(idx) = self.visible_entries.iter().position(|i| *i == i_sel)
752 {
753 clipper.add_included_range(idx..idx + 1);
754 }
755 clipper.with(|i| {
756 let i_entry = self.visible_entries[i];
757 let entry = &self.entries[i_entry];
758
759 ui.table_next_row(imgui::TableRowFlags::None, 0.0);
760
761 ui.table_set_column_index(0);
763 let icon_rr = match entry.kind {
764 FileEntryKind::Parent => Some(atlas.parent_rr),
765 FileEntryKind::Directory => Some(atlas.folder_rr),
766 FileEntryKind::File => Some(atlas.file_rr),
767 FileEntryKind::Root => Some(atlas.mypc_rr),
768 };
769 if let Some(rr) = icon_rr {
770 let avail = ui.get_content_region_avail();
771 let scale = ui.get_font_size() / 16.0;
772 let img_w = ui.get_custom_rect(rr).unwrap().rect.w as f32;
773 ui.set_cursor_pos_x(
774 ui.get_cursor_pos_x() + (avail.x - scale * img_w) / 2.0,
775 );
776 ui.image_with_custom_rect_config(rr, scale).build();
777 }
778
779 ui.table_set_column_index(1);
781 let is_selected = Some(i_entry) == self.selected;
782 if ui
783 .selectable_config(entry.name.to_string_lossy().into())
784 .flags(
785 imgui::SelectableFlags::SpanAllColumns
786 | imgui::SelectableFlags::AllowOverlap
787 | imgui::SelectableFlags::AllowDoubleClick,
788 )
789 .selected(is_selected)
790 .build()
791 {
792 self.selected = Some(i_entry);
794 self.file_name = entry.name.clone();
796 if ui.is_mouse_double_clicked(easy_imgui::MouseButton::Left) {
798 match entry.kind {
799 FileEntryKind::Parent => {
800 next_path = self.path.parent().map(|p| p.to_owned());
801 }
802 FileEntryKind::Directory | FileEntryKind::Root => {
803 next_path = Some(self.path.join(&entry.name));
804 }
805 FileEntryKind::File => {
806 output = Output::Ok;
807 }
808 }
809 }
810 }
811
812 if is_selected && self.scroll_dirty {
813 self.scroll_dirty = false;
814 ui.set_scroll_here_y(0.5);
815 }
816
817 ui.table_set_column_index(2);
819 if let Some(size) = entry.size {
820 let text = format!("{}", ByteSize(size));
821 ui.text(&text);
822 }
823
824 ui.table_set_column_index(3);
826 if let Some(modified) = entry.modified {
827 let s = modified
828 .format(format_description!(
829 "[year]-[month]-[day] [hour]:[minute]:[second]"
830 ))
831 .unwrap_or_default();
832 ui.text(&s);
833 }
834 });
835 if self.scroll_dirty {
836 self.scroll_dirty = false;
837 ui.set_scroll_y(0.0);
838 }
839 });
840 if preview_width > 0.0 {
841 ui.same_line();
842 ui.child_config(lbl("preview"))
843 .size(imgui::Vector2::new(0.0, -reserve))
844 .with(|| preview.do_ui(ui, self));
845 }
846
847 ui.text(&tr!("File name"));
848 ui.same_line();
849
850 if self.filters.is_empty() {
851 ui.set_next_item_width(-f32::EPSILON);
852 } else {
853 let filter_width = style.ItemSpacing.x
854 + ui.calc_text_size(&self.filters[self.active_filter_idx].text)
855 .x
856 + ui.get_frame_height()
857 + style.ItemInnerSpacing.x;
858 let filter_width = filter_width.max(ui.get_font_size() * 10.0);
860 ui.set_next_item_width(-filter_width);
861 }
862
863 if ui.is_window_appearing() {
864 ui.set_keyboard_focus_here(0);
865 }
866 let press_enter = ui
867 .input_os_string_config(lbl_id(c"", c"input"), &mut self.file_name)
868 .flags(imgui::InputTextFlags::EnterReturnsTrue)
869 .build();
870
871 if !self.filters.is_empty() {
872 ui.same_line();
873 ui.set_next_item_width(-f32::EPSILON);
874 if ui.combo(
875 lbl_id(c"", c"Filter"),
876 0..self.filters.len(),
877 |i| &self.filters[i].text,
878 &mut self.active_filter_idx,
879 ) {
880 self.visible_dirty = true;
881 }
882 }
883
884 let font_sz = ui.get_font_size();
885
886 let (maybe_next_path, exists) = self.applicable_path();
887 let can_ok = exists != ApplicablePathRes::Forbidden;
888 ui.with_disabled(!can_ok, || {
889 if ui
890 .button_config(lbl_id(tr!("OK"), "ok"))
891 .size(imgui::Vector2::new(5.5 * font_sz, 0.0))
892 .build()
893 | ui.shortcut(imgui::Key::Enter)
894 | ui.shortcut(imgui::Key::KeypadEnter)
895 | (can_ok && press_enter)
896 {
897 match exists {
899 ApplicablePathRes::Directory => next_path = Some(maybe_next_path),
901 ApplicablePathRes::ExistingFile => output = Output::Ok,
903 ApplicablePathRes::NewEntry => output = Output::Ok,
905 ApplicablePathRes::Forbidden => (),
907 }
908 }
909 });
910 ui.same_line();
911 if ui
912 .button_config(lbl_id(tr!("Cancel"), "cancel"))
913 .size(imgui::Vector2::new(5.5 * font_sz, 0.0))
914 .build()
915 | ui.shortcut(imgui::Key::Escape)
916 {
917 output = Output::Cancel;
918 }
919 if self.flags.contains(Flags::SHOW_READ_ONLY) {
920 ui.same_line();
921 let text = tr!("Read only");
922 let check_width =
923 ui.get_frame_height() + style.ItemInnerSpacing.x + ui.calc_text_size(&text).x;
924 ui.set_cursor_pos_x(
925 ui.get_cursor_pos_x() + ui.get_content_region_avail().x - check_width,
926 );
927 ui.checkbox(text.into(), &mut self.read_only);
928 }
929
930 if let Some(next_path) = next_path {
931 let _ = self.set_path(next_path);
932 self.set_file_name("");
934 }
935
936 output
937 }
938
939 fn applicable_path(&self) -> (PathBuf, ApplicablePathRes) {
940 let mut file_name = self.file_name();
942
943 if file_name == "" {
944 (PathBuf::new(), ApplicablePathRes::Forbidden)
945 } else if file_name == "." {
946 (self.path.to_path_buf(), ApplicablePathRes::Directory)
947 } else if file_name == ".." {
948 let p = self
949 .path
950 .parent()
951 .map(|p| p.to_path_buf())
952 .unwrap_or(self.path.clone());
953 (p, ApplicablePathRes::Directory)
954 } else {
955 let exists = self
956 .visible_entries
957 .iter()
958 .map(|&i| &self.entries[i])
959 .find_map(|e| {
960 if e.name == file_name {
961 match e.kind {
962 FileEntryKind::File => {
963 file_name = &e.name;
964 Some(ApplicablePathRes::ExistingFile)
965 }
966 _ => Some(ApplicablePathRes::Directory),
967 }
968 } else {
969 None
970 }
971 })
972 .or_else(|| {
973 if Path::new(&file_name).extension().is_some() {
975 return None;
976 }
977 let candidate = self
978 .visible_entries
979 .iter()
980 .map(|&i| &self.entries[i])
981 .find_map(|e| {
982 if e.kind != FileEntryKind::File {
983 return None;
984 }
985 let p = Path::new(&e.name);
986 if p.file_stem() != Some(file_name) {
987 return None;
988 }
989 Some(&e.name)
990 })?;
991 file_name = candidate;
992 Some(ApplicablePathRes::ExistingFile)
993 })
994 .unwrap_or_else(|| {
995 if self.flags.contains(Flags::MUST_EXIST) {
996 ApplicablePathRes::Forbidden
997 } else {
998 ApplicablePathRes::NewEntry
999 }
1000 });
1001 (self.path.join(file_name), exists)
1002 }
1003 }
1004
1005 fn resort_entries(&mut self, specs: &[easy_imgui::TableColumnSortSpec]) {
1006 let sel = self.selected.map(|i| self.entries[i].name.clone());
1007
1008 self.entries.sort_by(|a, b| {
1009 use std::cmp::Ordering;
1010 use FileEntryKind::Parent;
1012 match (a.kind, b.kind) {
1013 (Parent, Parent) => return Ordering::Equal,
1014 (Parent, _) => return Ordering::Less,
1015 (_, Parent) => return Ordering::Greater,
1016 (_, _) => (),
1017 }
1018 for s in specs {
1019 let res = match s.index() {
1020 0 => a.kind.cmp(&b.kind),
1021 1 => a.name.cmp(&b.name),
1022 2 => a.size.cmp(&b.size),
1023 3 => a.modified.cmp(&b.modified),
1024 _ => continue,
1025 };
1026 let res = match s.sort_direction() {
1027 easy_imgui::SortDirection::Ascending => res,
1028 easy_imgui::SortDirection::Descending => res.reverse(),
1029 _ => continue,
1030 };
1031 if res.is_ne() {
1032 return res;
1033 }
1034 }
1035 Ordering::Equal
1036 });
1037 self.selected = sel.and_then(|n| self.entries.iter().position(|e| e.name == n));
1039 self.visible_dirty = true;
1041 }
1042 fn recompute_visible_entries(&mut self) {
1043 let search_term = self.search_term.to_lowercase();
1044 self.visible_entries.clear();
1045 self.visible_entries
1046 .extend(
1047 self.entries
1048 .iter()
1049 .enumerate()
1050 .filter_map(|(i_entry, entry)| {
1051 if !self.show_hidden && entry.hidden {
1052 return None;
1053 }
1054 if matches!(entry.kind, FileEntryKind::File | FileEntryKind::Directory)
1056 && !search_term.is_empty()
1057 && !entry
1058 .name
1059 .to_string_lossy()
1060 .to_lowercase()
1061 .contains(&search_term)
1062 {
1063 return None;
1064 }
1065 if entry.kind == FileEntryKind::File && !self.filters.is_empty() {
1067 let f = &self.filters[self.active_filter_idx];
1068 if !f.matches(&entry.name) {
1069 return None;
1070 }
1071 }
1072 Some(i_entry)
1073 }),
1074 );
1075 }
1076}
1077
1078pub struct UiParameters<'a, Preview> {
1080 atlas: &'a CustomAtlas,
1081 preview: Preview,
1082}
1083
1084pub trait PreviewBuilder<A, D: DirEnum = FileSystemDirEnum> {
1086 fn width(&self) -> f32;
1088 fn do_ui(&mut self, ui: &imgui::Ui<A>, chooser: &FileChooserD<D>);
1090}
1091
1092pub struct NoPreview;
1094
1095impl<A, D: DirEnum> PreviewBuilder<A, D> for NoPreview {
1096 fn width(&self) -> f32 {
1097 0.0
1098 }
1099 fn do_ui(&mut self, _ui: &easy_imgui::Ui<A>, _chooser: &FileChooserD<D>) {}
1100}
1101
1102impl<'a> UiParameters<'a, NoPreview> {
1103 pub fn new(atlas: &'a CustomAtlas) -> Self {
1105 UiParameters {
1106 atlas,
1107 preview: NoPreview,
1108 }
1109 }
1110 pub fn with_preview<A, D: DirEnum, P: PreviewBuilder<A, D>>(
1112 self,
1113 preview: P,
1114 ) -> UiParameters<'a, P> {
1115 UiParameters {
1116 atlas: self.atlas,
1117 preview,
1118 }
1119 }
1120}
1121
1122impl<'a> From<&'a CustomAtlas> for UiParameters<'a, NoPreview> {
1124 fn from(value: &'a CustomAtlas) -> Self {
1125 UiParameters::new(value)
1126 }
1127}
1128
1129impl FileEntry {
1130 fn new(rd: &DirEntry) -> FileEntry {
1131 let name = rd.file_name();
1132 let (kind, size, modified, hidden);
1133 match rd.path().metadata() {
1134 Ok(meta) => {
1135 if meta.is_dir() {
1136 kind = FileEntryKind::Directory;
1137 size = None;
1138 } else {
1139 kind = FileEntryKind::File;
1140 size = Some(meta.len());
1141 }
1142 modified = meta.modified().ok().map(time::OffsetDateTime::from);
1143 #[cfg(target_os = "windows")]
1144 {
1145 use std::os::windows::fs::MetadataExt;
1146 hidden = (meta.file_attributes() & 2) != 0; }
1148 }
1149 Err(_) => {
1150 kind = FileEntryKind::File;
1152 size = None;
1153 modified = None;
1154 #[cfg(target_os = "windows")]
1155 {
1156 hidden = false;
1157 }
1158 }
1159 }
1160
1161 #[cfg(not(target_os = "windows"))]
1162 {
1163 hidden = if matches!(kind, FileEntryKind::File | FileEntryKind::Directory) {
1164 name.as_encoded_bytes().first() == Some(&b'.')
1165 } else {
1166 false
1167 };
1168 }
1169 FileEntry {
1170 name,
1171 kind,
1172 size,
1173 modified,
1174 hidden,
1175 }
1176 }
1177
1178 fn dot_dot() -> FileEntry {
1179 FileEntry {
1180 name: "..".into(),
1181 kind: FileEntryKind::Parent,
1182 size: None,
1183 modified: None,
1184 hidden: false,
1185 }
1186 }
1187}
1188
1189macro_rules! image {
1190 ($name:ident = $file:literal) => {
1191 fn $name() -> &'static image::DynamicImage {
1192 static BYTES: &[u8] = include_bytes!($file);
1193 static IMG: std::sync::OnceLock<image::DynamicImage> = std::sync::OnceLock::new();
1194 IMG.get_or_init(|| {
1195 image::load_from_memory_with_format(BYTES, image::ImageFormat::Png).unwrap()
1196 })
1197 }
1198 };
1199}
1200
1201image! {file_img = "file.png"}
1202image! {folder_img = "folder.png"}
1203image! {parent_img = "parent.png"}
1204image! {hidden_img = "hidden.png"}
1205image! {mypc_img = "mypc.png"}
1206
1207#[derive(Default, Copy, Clone)]
1212pub struct CustomAtlas {
1213 file_rr: CustomRectIndex,
1214 folder_rr: CustomRectIndex,
1215 parent_rr: CustomRectIndex,
1216 hidden_rr: CustomRectIndex,
1217 mypc_rr: CustomRectIndex,
1218}
1219
1220pub fn build_custom_atlas(atlas: &mut easy_imgui::FontAtlas) -> CustomAtlas {
1225 use image::GenericImage;
1226
1227 let mut do_rr = |img: &'static DynamicImage| {
1228 atlas.add_custom_rect([img.width(), img.height()], {
1229 |pixels| pixels.copy_from(img, 0, 0).unwrap()
1230 })
1231 };
1232 let file_rr = do_rr(file_img());
1233 let folder_rr = do_rr(folder_img());
1234 let parent_rr = do_rr(parent_img());
1235 let hidden_rr = do_rr(hidden_img());
1236 let mypc_rr = do_rr(mypc_img());
1237
1238 CustomAtlas {
1239 file_rr,
1240 folder_rr,
1241 parent_rr,
1242 hidden_rr,
1243 mypc_rr,
1244 }
1245}