1use bytesize::ByteSize;
8use easy_imgui::{self as imgui, CustomRectIndex, id, lbl, lbl_id};
9pub use glob::{self, Pattern};
10use image::DynamicImage;
11use std::io::Result;
12use std::{
13 borrow::Cow,
14 ffi::{OsStr, OsString},
15 fs::DirEntry,
16 path::{Path, PathBuf},
17 time::SystemTime,
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 struct FileChooser {
46 path: PathBuf,
47 flags: Flags,
48 entries: Vec<FileEntry>,
49 selected: Option<usize>,
50 sort_dirty: bool,
51 visible_dirty: bool,
52 scroll_dirty: bool,
53 show_hidden: bool,
54 popup_dirs: Vec<(PathBuf, bool)>,
55 search_term: String,
56 file_name: OsString,
57 active_filter_idx: usize,
59 filters: Vec<Filter>,
60 read_only: bool,
61 visible_entries: Vec<usize>,
62 path_size_overflow: f32,
63}
64
65pub enum Output {
67 Continue,
71 Cancel,
75 Ok,
80}
81
82impl std::fmt::Debug for FileChooser {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.debug_struct("FileChooser")
85 .field("path", &self.path())
86 .field("file_name", &self.file_name())
87 .field("active_filter", &self.active_filter())
88 .field("read_only", &self.read_only())
89 .finish()
90 }
91}
92
93#[allow(dead_code)]
94#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
95enum FileEntryKind {
96 Parent,
97 Directory,
98 File,
99 Root, }
101
102struct FileEntry {
103 name: OsString,
104 kind: FileEntryKind,
105 size: Option<u64>,
106 modified: Option<SystemTime>,
107 hidden: bool,
108}
109
110#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
116pub struct FilterId(pub i32);
117
118#[derive(Default, Debug, Clone)]
120pub struct Filter {
121 pub id: FilterId,
125 pub text: String,
127 pub globs: Vec<glob::Pattern>,
133}
134
135impl Filter {
136 pub fn matches(&self, name: impl AsRef<OsStr>) -> bool {
138 let name = name.as_ref().to_string_lossy();
139 let opts = glob::MatchOptions {
140 case_sensitive: false,
141 ..Default::default()
142 };
143 self.globs.is_empty() || self.globs.iter().any(|glob| glob.matches_with(&name, opts))
145 }
146}
147
148#[cfg(target_os = "windows")]
149mod os {
150 use std::path::PathBuf;
151
152 pub struct Roots {
153 drives: u32,
154 letter: u8,
155 }
156
157 impl Roots {
158 pub fn new() -> Roots {
159 let drives = unsafe { windows::Win32::Storage::FileSystem::GetLogicalDrives() };
160 Roots { drives, letter: 0 }
161 }
162 }
163 impl Iterator for Roots {
164 type Item = PathBuf;
165
166 fn next(&mut self) -> Option<PathBuf> {
167 while self.letter < 26 {
168 let bit = 1 << self.letter;
170 let drive = char::from(b'A' + self.letter);
171 self.letter += 1;
172 if (self.drives & bit) != 0 {
173 return Some(PathBuf::from(format!("{}:\\", drive)));
174 }
175 }
176 None
177 }
178 }
179}
180
181#[cfg(not(target_os = "windows"))]
182mod os {
183 use std::path::PathBuf;
184
185 pub struct Roots(());
186
187 impl Roots {
188 pub fn new() -> Roots {
189 Roots(())
190 }
191 }
192
193 impl Iterator for Roots {
194 type Item = PathBuf;
195
196 fn next(&mut self) -> Option<PathBuf> {
198 None
199 }
200 }
201}
202
203struct EnumSubdirs {
204 dir: std::fs::ReadDir,
205}
206
207impl EnumSubdirs {
208 fn new(path: impl Into<PathBuf>) -> Result<EnumSubdirs> {
209 let path = path.into();
210 let dir = std::fs::read_dir(path)?;
211 Ok(EnumSubdirs { dir })
212 }
213}
214
215impl Iterator for EnumSubdirs {
216 type Item = PathBuf;
217
218 fn next(&mut self) -> Option<Self::Item> {
219 loop {
220 let next = self.dir.next()?.ok()?;
221 let path = next.path();
222 let meta = path.metadata().ok()?;
223 let is_dir = meta.is_dir();
224 if !is_dir {
225 continue;
226 }
227 return Some(path);
228 }
229 }
230}
231
232bitflags::bitflags! {
233 pub struct Flags: u32 {
234 const SHOW_READ_ONLY = 1;
236 }
237}
238
239impl Default for FileChooser {
240 fn default() -> Self {
241 FileChooser::new()
242 }
243}
244
245impl FileChooser {
246 pub fn new() -> FileChooser {
248 FileChooser {
249 path: PathBuf::default(),
250 flags: Flags::empty(),
251 entries: Vec::new(),
252 selected: None,
253 sort_dirty: false,
254 visible_dirty: false,
255 scroll_dirty: false,
256 show_hidden: false,
257 popup_dirs: Vec::new(),
258 search_term: String::new(),
259 file_name: OsString::new(),
260 active_filter_idx: 0,
261 filters: Vec::new(),
262 read_only: false,
263 visible_entries: Vec::new(),
264 path_size_overflow: 0.0,
265 }
266 }
267 pub fn add_flags(&mut self, flags: Flags) {
269 self.flags |= flags;
270 }
271 pub fn remove_flags(&mut self, flags: Flags) {
273 self.flags &= !flags;
274 }
275 pub fn set_path(&mut self, path: impl AsRef<Path>) -> Result<()> {
279 let path = std::path::absolute(path)?;
280 self.selected = None;
281 let mut entries = std::mem::take(&mut self.entries);
283 entries.clear();
284 let mut add_entry = |entry: FileEntry| {
285 if self.file_name == entry.name {
286 self.selected = Some(entries.len());
287 }
288 entries.push(entry);
289 };
290 if path.parent().is_some() {
291 add_entry(FileEntry::dot_dot());
292 }
293 for rd in std::fs::read_dir(&path)? {
294 let Ok(rd) = rd else { continue };
295 add_entry(FileEntry::new(&rd));
296 }
297 self.path = path;
298 self.entries = entries;
299 self.sort_dirty = true;
300 self.visible_dirty = true;
301 self.scroll_dirty = true;
302 self.popup_dirs.clear();
303 self.search_term.clear();
304 self.path_size_overflow = 0.0;
305 Ok(())
306 }
307
308 pub fn set_file_name(&mut self, file_name: impl AsRef<OsStr>) {
312 self.file_name = file_name.as_ref().to_owned();
313
314 if !self.file_name.is_empty() {
315 for (i_f, f) in self.filters.iter().enumerate() {
317 if f.matches(&self.file_name) {
318 self.active_filter_idx = i_f;
319 break;
320 }
321 }
322 for (i_entry, entry) in self.entries.iter().enumerate() {
324 if entry.name == self.file_name {
325 self.selected = Some(i_entry);
326 break;
327 }
328 }
329 }
330 }
331
332 pub fn path(&self) -> &Path {
334 &self.path
335 }
336
337 pub fn file_name(&self) -> &OsStr {
342 if let Some(i_sel) = self.selected {
347 let entry = &self.entries[i_sel];
348 if entry.kind == FileEntryKind::File && self.file_name == *entry.name.to_string_lossy()
349 {
350 return &entry.name;
351 }
352 }
353 &self.file_name
354 }
355 pub fn active_filter(&self) -> Option<FilterId> {
359 if self.filters.is_empty() {
360 None
361 } else {
362 Some(self.filters[self.active_filter_idx].id)
363 }
364 }
365 pub fn set_active_filter(&mut self, filter_id: FilterId) {
370 if let Some(p) = self.filters.iter().position(|f| f.id == filter_id) {
371 self.active_filter_idx = p;
372 }
373 }
374 pub fn read_only(&self) -> bool {
377 self.read_only
378 }
379 pub fn full_path(&self, default_extension: Option<&str>) -> PathBuf {
385 let file_name = self.file_name();
386 let mut res = self.path.join(file_name);
387 if let (None, Some(new_ext)) = (res.extension(), default_extension)
388 && !res.exists()
389 {
390 res.set_extension(new_ext);
391 }
392 res
393 }
394
395 #[cfg(target_os = "windows")]
396 fn set_path_super_root(&mut self) {
397 self.path = PathBuf::default();
398 self.entries = os::Roots::new()
399 .map(|path| FileEntry {
400 name: path.into(),
401 kind: FileEntryKind::Root,
402 size: None,
403 modified: None,
404 hidden: false,
405 })
406 .collect();
407 self.selected = None;
408 self.sort_dirty = true;
409 self.visible_dirty = true;
410 self.scroll_dirty = true;
411 self.popup_dirs.clear();
412 self.search_term.clear();
413 }
414
415 pub fn add_filter(&mut self, filter: Filter) {
417 self.filters.push(filter);
418 if !self.file_name.is_empty()
419 && !self.filters[self.active_filter_idx].matches(&self.file_name)
420 {
421 if self.filters.last().unwrap().matches(&self.file_name) {
423 self.active_filter_idx = self.filters.len() - 1;
424 }
425 }
426 self.visible_dirty = true;
427 }
428 pub fn do_ui<'a, A, Params, Preview>(&mut self, ui: &'a imgui::Ui<A>, params: Params) -> Output
434 where
435 Params: Into<UiParameters<'a, Preview>>,
436 Preview: PreviewBuilder<A>,
437 {
438 if self.entries.is_empty() {
439 let res = self.set_path(".");
440 if res.is_err() {
441 return Output::Cancel;
442 }
443 }
444
445 let UiParameters { atlas, mut preview } = params.into();
446 let mut next_path = None;
447 let mut output = Output::Continue;
448
449 let mut my_path = PathBuf::new();
450
451 let style = ui.style();
452 ui.child_config(lbl("path"))
453 .size(imgui::Vector2::new(
455 0.0,
456 ui.get_frame_height_with_spacing()
457 + if self.path_size_overflow < 0.0 {
458 style.ScrollbarSize
459 } else {
460 0.0
461 },
462 ))
463 .window_flags(imgui::WindowFlags::HorizontalScrollbar)
464 .with(|| {
465 #[cfg(target_os = "windows")]
466 {
467 let scale = ui.get_font_size() / 16.0;
468 if ui
469 .image_button_with_custom_rect_config(id("::super"), atlas.mypc_rr, scale)
470 .build()
471 {
472 self.set_path_super_root();
473 }
474 ui.same_line();
475 }
476
477 let mut my_disk = None;
478 'component: for component in self.path.components() {
479 let piece = 'piece: {
480 match component {
483 std::path::Component::Prefix(prefix) => match prefix.kind() {
484 std::path::Prefix::VerbatimDisk(disk)
485 | std::path::Prefix::Disk(disk) => {
486 my_disk = Some(char::from(disk));
487 continue 'component;
488 }
489 _ => (),
490 },
491 std::path::Component::RootDir => {
492 if let Some(my_disk) = my_disk.take() {
493 let drive_root = format!(
494 "{}:{}",
495 my_disk,
496 component.as_os_str().to_string_lossy()
497 );
498 let drive_root = OsString::from(drive_root);
499 break 'piece Cow::Owned(drive_root);
500 }
501 }
502 _ => (),
503 }
504 Cow::Borrowed(component.as_os_str())
505 };
506
507 my_path.push(&piece);
508 if ui
509 .button_config(lbl_id(piece.to_string_lossy(), my_path.to_string_lossy()))
510 .build()
511 {
512 next_path = Some(my_path.clone());
513 }
514 if ui.is_mouse_released(imgui::MouseButton::Right)
515 && ui.is_item_hovered_ex(imgui::HoveredFlags::AllowWhenBlockedByPopup)
516 {
517 let mut popup_dirs = Vec::new();
518 if let Some(parent) = my_path.parent() {
519 if let Ok(subdir) = EnumSubdirs::new(parent) {
520 for d in subdir {
521 if d.file_name().is_some() {
522 let sel = d == my_path;
523 popup_dirs.push((d, sel));
524 }
525 }
526 }
527 } else {
528 for root in os::Roots::new() {
529 let sel = root == my_path;
530 popup_dirs.push((root, sel));
531 }
532 }
533 popup_dirs.sort_by(|a, b| a.0.cmp(&b.0));
534 self.popup_dirs = popup_dirs;
535 if !self.popup_dirs.is_empty() {
536 ui.open_popup(id(c"popup_dirs"));
537 }
538 }
539 ui.same_line();
540 }
541 let path_size_overflow = ui.get_content_region_avail().x;
544 if path_size_overflow != self.path_size_overflow {
545 ui.set_scroll_here_x(1.0);
546 self.path_size_overflow = path_size_overflow;
547 }
548
549 ui.popup_config(id(c"popup_dirs")).with(|| {
550 for (dir, sel) in &self.popup_dirs {
551 let name = dir.file_name().unwrap_or_else(|| dir.as_os_str());
552 if ui
553 .selectable_config(lbl_id(
554 name.to_string_lossy(),
555 dir.display().to_string(),
556 ))
557 .selected(*sel)
558 .build()
559 {
560 next_path = Some(dir.clone());
561 }
562 }
563 });
564 });
565
566 ui.text(&tr!("Search"));
567 ui.same_line();
568 ui.set_next_item_width(-ui.get_frame_height_with_spacing() - style.ItemSpacing.x);
569 if ui
570 .input_text_config(lbl_id(c"", c"Search"), &mut self.search_term)
571 .build()
572 {
573 self.visible_dirty = true;
574 }
575
576 ui.same_line();
577 ui.with_push(
578 self.show_hidden.then_some(((
579 imgui::ColorId::Button,
580 style.color(imgui::ColorId::ButtonActive),
581 ),)),
582 || {
583 let scale = ui.get_font_size() / 16.0;
584 if ui
585 .image_button_with_custom_rect_config(id("hidden"), atlas.hidden_rr, scale)
586 .build()
587 {
588 self.show_hidden ^= true;
589 self.visible_dirty = true;
590 }
591 },
592 );
593
594 let style = ui.style();
595 let reserve = 2.0 * ui.get_frame_height_with_spacing();
597 let preview_width = preview.width();
598 ui.table_config(lbl("FileChooser"), 4)
599 .flags(
600 imgui::TableFlags::RowBg
601 | imgui::TableFlags::ScrollY
602 | imgui::TableFlags::Resizable
603 | imgui::TableFlags::Sortable
604 | imgui::TableFlags::SizingFixedFit,
605 )
606 .outer_size(imgui::Vector2::new(-preview_width, -reserve))
607 .with(|| {
608 let pad = ui.style().FramePadding;
609 ui.table_setup_column("", imgui::TableColumnFlags::None, 0.00, 0);
610 ui.table_setup_column(
611 tr!("Name"),
612 imgui::TableColumnFlags::WidthStretch | imgui::TableColumnFlags::DefaultSort,
613 0.0,
614 0,
615 );
616 ui.table_setup_column(
617 tr!("Size"),
618 imgui::TableColumnFlags::WidthFixed,
619 ui.calc_text_size("999.9 GiB").x + 2.0 * pad.x,
620 0,
621 );
622 ui.table_setup_column(
623 tr!("Modified"),
624 imgui::TableColumnFlags::WidthFixed,
625 ui.calc_text_size("2024-12-31 23:59:59").x + 2.0 * pad.x,
626 0,
627 );
628 ui.table_setup_scroll_freeze(0, 1);
629 ui.table_headers_row();
630
631 ui.table_with_sort_specs_always(|dirty, specs| {
636 if dirty || self.sort_dirty {
637 self.sort_dirty = false;
638 self.resort_entries(specs);
639 }
640 false
641 });
642 if self.visible_dirty {
643 self.visible_dirty = false;
644 self.scroll_dirty = true;
645 self.recompute_visible_entries();
646 }
647
648 let mut clipper = ui.list_clipper(self.visible_entries.len());
649 if let (Some(i_sel), true) = (self.selected, self.scroll_dirty)
653 && let Some(idx) = self.visible_entries.iter().position(|i| *i == i_sel)
654 {
655 clipper.add_included_range(idx..idx + 1);
656 }
657 clipper.with(|i| {
658 let i_entry = self.visible_entries[i];
659 let entry = &self.entries[i_entry];
660
661 ui.table_next_row(imgui::TableRowFlags::None, 0.0);
662
663 ui.table_set_column_index(0);
665 let icon_rr = match entry.kind {
666 FileEntryKind::Parent => Some(atlas.parent_rr),
667 FileEntryKind::Directory => Some(atlas.folder_rr),
668 FileEntryKind::File => Some(atlas.file_rr),
669 FileEntryKind::Root => Some(atlas.mypc_rr),
670 };
671 if let Some(rr) = icon_rr {
672 let avail = ui.get_content_region_avail();
673 let scale = ui.get_font_size() / 16.0;
674 let img_w = ui.get_custom_rect(rr).unwrap().rect.w as f32;
675 ui.set_cursor_pos_x(
676 ui.get_cursor_pos_x() + (avail.x - scale * img_w) / 2.0,
677 );
678 ui.image_with_custom_rect_config(rr, scale).build();
679 }
680
681 ui.table_set_column_index(1);
683 let is_selected = Some(i_entry) == self.selected;
684 if ui
685 .selectable_config(entry.name.to_string_lossy().into())
686 .flags(
687 imgui::SelectableFlags::SpanAllColumns
688 | imgui::SelectableFlags::AllowOverlap
689 | imgui::SelectableFlags::AllowDoubleClick,
690 )
691 .selected(is_selected)
692 .build()
693 {
694 self.selected = Some(i_entry);
696 if entry.kind == FileEntryKind::File {
699 self.file_name = entry.name.clone();
700 }
701 if ui.is_mouse_double_clicked(easy_imgui::MouseButton::Left) {
703 match entry.kind {
704 FileEntryKind::Parent => {
705 next_path = self.path.parent().map(|p| p.to_owned());
706 }
707 FileEntryKind::Directory | FileEntryKind::Root => {
708 next_path = Some(self.path.join(&entry.name));
709 }
710 FileEntryKind::File => {
711 output = Output::Ok;
712 }
713 }
714 }
715 }
716
717 if is_selected && self.scroll_dirty {
718 self.scroll_dirty = false;
719 ui.set_scroll_here_y(0.5);
720 }
721
722 ui.table_set_column_index(2);
724 if let Some(size) = entry.size {
725 let text = format!("{}", ByteSize(size));
726 ui.text(&text);
727 }
728
729 ui.table_set_column_index(3);
731 if let Some(modified) = entry.modified {
732 let tm = time::OffsetDateTime::from(modified);
733 let s = tm
734 .format(format_description!(
735 "[year]-[month]-[day] [hour]:[minute]:[second]"
736 ))
737 .unwrap_or_default();
738 ui.text(&s);
739 }
740 });
741 if self.scroll_dirty {
742 self.scroll_dirty = false;
743 ui.set_scroll_y(0.0);
744 }
745 });
746 if preview_width > 0.0 {
747 ui.same_line();
748 ui.child_config(lbl("preview"))
749 .size(imgui::Vector2::new(0.0, -reserve))
750 .with(|| preview.do_ui(ui, self));
751 }
752
753 ui.text(&tr!("File name"));
754 ui.same_line();
755
756 if self.filters.is_empty() {
757 ui.set_next_item_width(-f32::EPSILON);
758 } else {
759 let filter_width = style.ItemSpacing.x
760 + ui.calc_text_size(&self.filters[self.active_filter_idx].text)
761 .x
762 + ui.get_frame_height()
763 + style.ItemInnerSpacing.x;
764 let filter_width = filter_width.max(ui.get_font_size() * 10.0);
766 ui.set_next_item_width(-filter_width);
767 }
768
769 if ui.is_window_appearing() {
770 ui.set_keyboard_focus_here(0);
771 }
772 ui.input_os_string_config(lbl_id(c"", c"input"), &mut self.file_name)
773 .build();
774
775 if !self.filters.is_empty() {
776 ui.same_line();
777 ui.set_next_item_width(-f32::EPSILON);
778 if ui.combo(
779 lbl_id(c"", c"Filter"),
780 0..self.filters.len(),
781 |i| &self.filters[i].text,
782 &mut self.active_filter_idx,
783 ) {
784 self.visible_dirty = true;
785 }
786 }
787
788 let font_sz = ui.get_font_size();
789 let can_ok = !self.file_name.is_empty() && !self.path.as_os_str().is_empty();
790 ui.with_disabled(!can_ok, || {
791 if ui
792 .button_config(lbl_id(tr!("OK"), "ok"))
793 .size(imgui::Vector2::new(5.5 * font_sz, 0.0))
794 .build()
795 | ui.shortcut(imgui::Key::Enter)
796 | ui.shortcut(imgui::Key::KeypadEnter)
797 {
798 output = Output::Ok;
799 }
800 });
801 ui.same_line();
802 if ui
803 .button_config(lbl_id(tr!("Cancel"), "cancel"))
804 .size(imgui::Vector2::new(5.5 * font_sz, 0.0))
805 .build()
806 | ui.shortcut(imgui::Key::Escape)
807 {
808 output = Output::Cancel;
809 }
810 if self.flags.contains(Flags::SHOW_READ_ONLY) {
811 ui.same_line();
812 let text = tr!("Read only");
813 let check_width =
814 ui.get_frame_height() + style.ItemInnerSpacing.x + ui.calc_text_size(&text).x;
815 ui.set_cursor_pos_x(
816 ui.get_cursor_pos_x() + ui.get_content_region_avail().x - check_width,
817 );
818 ui.checkbox(text.into(), &mut self.read_only);
819 }
820
821 if let Some(next_path) = next_path {
822 let _ = self.set_path(next_path);
823 }
824
825 output
826 }
827 fn resort_entries(&mut self, specs: &[easy_imgui::TableColumnSortSpec]) {
828 let sel = self.selected.map(|i| self.entries[i].name.clone());
829
830 self.entries.sort_by(|a, b| {
831 use std::cmp::Ordering;
832 use FileEntryKind::Parent;
834 match (a.kind, b.kind) {
835 (Parent, Parent) => return Ordering::Equal,
836 (Parent, _) => return Ordering::Less,
837 (_, Parent) => return Ordering::Greater,
838 (_, _) => (),
839 }
840 for s in specs {
841 let res = match s.index() {
842 0 => a.kind.cmp(&b.kind),
843 1 => a.name.cmp(&b.name),
844 2 => a.size.cmp(&b.size),
845 3 => a.modified.cmp(&b.modified),
846 _ => continue,
847 };
848 let res = match s.sort_direction() {
849 easy_imgui::SortDirection::Ascending => res,
850 easy_imgui::SortDirection::Descending => res.reverse(),
851 _ => continue,
852 };
853 if res.is_ne() {
854 return res;
855 }
856 }
857 Ordering::Equal
858 });
859 self.selected = sel.and_then(|n| self.entries.iter().position(|e| e.name == n));
861 self.visible_dirty = true;
863 }
864 fn recompute_visible_entries(&mut self) {
865 let search_term = self.search_term.to_lowercase();
866 self.visible_entries.clear();
867 self.visible_entries
868 .extend(
869 self.entries
870 .iter()
871 .enumerate()
872 .filter_map(|(i_entry, entry)| {
873 if !self.show_hidden && entry.hidden {
874 return None;
875 }
876 if matches!(entry.kind, FileEntryKind::File | FileEntryKind::Directory)
878 && !search_term.is_empty()
879 && !entry
880 .name
881 .to_string_lossy()
882 .to_lowercase()
883 .contains(&search_term)
884 {
885 return None;
886 }
887 if entry.kind == FileEntryKind::File && !self.filters.is_empty() {
889 let f = &self.filters[self.active_filter_idx];
890 if !f.matches(&entry.name) {
891 return None;
892 }
893 }
894 Some(i_entry)
895 }),
896 );
897 }
898}
899
900pub struct UiParameters<'a, Preview> {
902 atlas: &'a CustomAtlas,
903 preview: Preview,
904}
905
906pub trait PreviewBuilder<A> {
908 fn width(&self) -> f32;
910 fn do_ui(&mut self, ui: &imgui::Ui<A>, chooser: &FileChooser);
912}
913
914pub struct NoPreview;
916
917impl<A> PreviewBuilder<A> for NoPreview {
918 fn width(&self) -> f32 {
919 0.0
920 }
921 fn do_ui(&mut self, _ui: &easy_imgui::Ui<A>, _chooser: &FileChooser) {}
922}
923
924impl<'a> UiParameters<'a, NoPreview> {
925 pub fn new(atlas: &'a CustomAtlas) -> Self {
927 UiParameters {
928 atlas,
929 preview: NoPreview,
930 }
931 }
932 pub fn with_preview<A, P: PreviewBuilder<A>>(self, preview: P) -> UiParameters<'a, P> {
934 UiParameters {
935 atlas: self.atlas,
936 preview,
937 }
938 }
939}
940
941impl<'a> From<&'a CustomAtlas> for UiParameters<'a, NoPreview> {
943 fn from(value: &'a CustomAtlas) -> Self {
944 UiParameters::new(value)
945 }
946}
947
948impl FileEntry {
949 fn new(rd: &DirEntry) -> FileEntry {
950 let name = rd.file_name();
951 let (kind, size, modified, hidden);
952 match rd.path().metadata() {
953 Ok(meta) => {
954 if meta.is_dir() {
955 kind = FileEntryKind::Directory;
956 size = None;
957 } else {
958 kind = FileEntryKind::File;
959 size = Some(meta.len());
960 }
961 modified = meta.modified().ok();
962 #[cfg(target_os = "windows")]
963 {
964 use std::os::windows::fs::MetadataExt;
965 hidden = (meta.file_attributes() & 2) != 0; }
967 }
968 Err(_) => {
969 kind = FileEntryKind::File;
971 size = None;
972 modified = None;
973 #[cfg(target_os = "windows")]
974 {
975 hidden = false;
976 }
977 }
978 }
979
980 #[cfg(not(target_os = "windows"))]
981 {
982 hidden = if matches!(kind, FileEntryKind::File | FileEntryKind::Directory) {
983 name.as_encoded_bytes().first() == Some(&b'.')
984 } else {
985 false
986 };
987 }
988 FileEntry {
989 name,
990 kind,
991 size,
992 modified,
993 hidden,
994 }
995 }
996
997 fn dot_dot() -> FileEntry {
998 FileEntry {
999 name: "..".into(),
1000 kind: FileEntryKind::Parent,
1001 size: None,
1002 modified: None,
1003 hidden: false,
1004 }
1005 }
1006}
1007
1008macro_rules! image {
1009 ($name:ident = $file:literal) => {
1010 fn $name() -> &'static image::DynamicImage {
1011 static BYTES: &[u8] = include_bytes!($file);
1012 static IMG: std::sync::OnceLock<image::DynamicImage> = std::sync::OnceLock::new();
1013 IMG.get_or_init(|| {
1014 image::load_from_memory_with_format(BYTES, image::ImageFormat::Png).unwrap()
1015 })
1016 }
1017 };
1018}
1019
1020image! {file_img = "file.png"}
1021image! {folder_img = "folder.png"}
1022image! {parent_img = "parent.png"}
1023image! {hidden_img = "hidden.png"}
1024#[cfg(target_os = "windows")]
1025image! {mypc_img = "mypc.png"}
1026
1027#[derive(Default, Copy, Clone)]
1032pub struct CustomAtlas {
1033 file_rr: CustomRectIndex,
1034 folder_rr: CustomRectIndex,
1035 parent_rr: CustomRectIndex,
1036 hidden_rr: CustomRectIndex,
1037 mypc_rr: CustomRectIndex,
1038}
1039
1040pub fn build_custom_atlas(atlas: &mut easy_imgui::FontAtlas) -> CustomAtlas {
1045 use image::GenericImage;
1046
1047 let mut do_rr = |img: &'static DynamicImage| {
1048 atlas.add_custom_rect([img.width(), img.height()], {
1049 |pixels| pixels.copy_from(img, 0, 0).unwrap()
1050 })
1051 };
1052 let file_rr = do_rr(file_img());
1053 let folder_rr = do_rr(folder_img());
1054 let parent_rr = do_rr(parent_img());
1055 let hidden_rr = do_rr(hidden_img());
1056
1057 #[cfg(target_os = "windows")]
1059 let mypc_rr = do_rr(mypc_img());
1060 #[cfg(not(target_os = "windows"))]
1061 let mypc_rr = Default::default();
1062
1063 CustomAtlas {
1064 file_rr,
1065 folder_rr,
1066 parent_rr,
1067 hidden_rr,
1068 mypc_rr,
1069 }
1070}