From 94773a456cce11ad1816c0208c69ace303f18c82 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 15:02:33 -0800 Subject: [PATCH 01/29] WIP --- src/data/directory_content.rs | 21 +++++--------- src/data/mod.rs | 2 +- src/lib.rs | 3 ++ src/virtual_fs.rs | 52 +++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 15 deletions(-) create mode 100644 src/virtual_fs.rs diff --git a/src/data/directory_content.rs b/src/data/directory_content.rs index c39b286c..fa6b63b0 100644 --- a/src/data/directory_content.rs +++ b/src/data/directory_content.rs @@ -1,4 +1,5 @@ use crate::config::{FileDialogConfig, FileFilter}; +use crate::FileSystem; use egui::mutex::Mutex; use std::path::{Path, PathBuf}; use std::sync::{mpsc, Arc}; @@ -33,21 +34,12 @@ pub struct DirectoryEntry { impl DirectoryEntry { /// Creates a new directory entry from a path - pub fn from_path(config: &FileDialogConfig, path: &Path) -> Self { - let mut metadata = Metadata::default(); - - if let Ok(md) = fs::metadata(path) { - metadata.size = Some(md.len()); - metadata.last_modified = md.modified().ok(); - metadata.created = md.created().ok(); - metadata.file_type = Some(format!("{:?}", md.file_type())); - } - + pub fn from_path(config: &FileDialogConfig, path: &Path, vfs: &impl FileSystem) -> Self { Self { path: path.to_path_buf(), - metadata, - is_directory: path.is_dir(), - is_system_file: !path.is_dir() && !path.is_file(), + metadata: vfs.metadata(path).unwrap_or_default(), + is_directory: vfs.is_dir(path), + is_system_file: !vfs.is_dir(path) && !vfs.is_file(path), icon: gen_path_icon(config, path), selected: false, } @@ -358,6 +350,7 @@ fn load_directory( path: &Path, include_files: bool, file_filter: Option<&FileFilter>, + vfs: &impl FileSystem, ) -> io::Result> { let paths = fs::read_dir(path)?; @@ -365,7 +358,7 @@ fn load_directory( for path in paths { match path { Ok(entry) => { - let entry = DirectoryEntry::from_path(config, entry.path().as_path()); + let entry = DirectoryEntry::from_path(config, entry.path().as_path(), vfs); if !config.storage.show_system_files && entry.is_system_file() { continue; diff --git a/src/data/mod.rs b/src/data/mod.rs index cdae0f0d..78ae97b5 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,5 +1,5 @@ mod directory_content; -pub use directory_content::{DirectoryContent, DirectoryContentState, DirectoryEntry}; +pub use directory_content::{DirectoryContent, DirectoryContentState, DirectoryEntry, Metadata}; mod disks; pub use disks::{Disk, Disks}; diff --git a/src/lib.rs b/src/lib.rs index 0aa52518..9f83f820 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -201,6 +201,7 @@ mod file_dialog; /// Information panel showing the preview and metadata of the selected item pub mod information_panel; mod modals; +mod virtual_fs; pub use config::{ FileDialogConfig, FileDialogKeyBindings, FileDialogLabels, FileDialogStorage, IconFilter, @@ -208,3 +209,5 @@ pub use config::{ }; pub use data::DirectoryEntry; pub use file_dialog::{DialogMode, DialogState, FileDialog}; + +pub use virtual_fs::{FileSystem, NativeFileSystem}; diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs new file mode 100644 index 00000000..0fa78aab --- /dev/null +++ b/src/virtual_fs.rs @@ -0,0 +1,52 @@ +use std::path::{Path, PathBuf}; +use std::io; + +use crate::data::Metadata; + +/// File system abstraction +pub trait FileSystem { + /// Queries metadata for the given path + fn metadata(&self, path: &Path) -> io::Result; + + /// Returns true if the path exists and is a directory + fn is_dir(&self, path: &Path) -> bool; + + /// Returns true if the path exists and is a file + fn is_file(&self, path: &Path) -> bool; + + /// Reads + fn read_dir(&self, path: &Path) -> io::Result>; +} + +/// Implementation of FileSystem using the standard library +pub struct NativeFileSystem; + +impl FileSystem for NativeFileSystem { + fn metadata(&self, path: &Path) -> io::Result { + let mut metadata = Metadata::default(); + + let md = std::fs::metadata(path)?; + metadata.size = Some(md.len()); + metadata.last_modified = md.modified().ok(); + metadata.created = md.created().ok(); + metadata.file_type = Some(format!("{:?}", md.file_type())); + + Ok(metadata) + } + + fn is_dir(&self, path: &Path) -> bool { + path.is_dir() + } + + fn is_file(&self, path: &Path) -> bool { + path.is_file() + } + + fn read_dir(&self, path: &Path) -> io::Result> { + Ok(std::fs::read_dir(path)? + .into_iter() + .filter_map(|entry| entry.ok()) + .map(|entry| entry.path()) + .collect()) + } +} From 816ce42ecdbc5d831bb62eab9344450b47560c37 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 15:26:22 -0800 Subject: [PATCH 02/29] It compiles again! --- src/data/directory_content.rs | 56 ++++++++++++++++------------------- src/file_dialog.rs | 13 ++++++-- src/virtual_fs.rs | 6 ++++ 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/data/directory_content.rs b/src/data/directory_content.rs index fa6b63b0..3f53f4ff 100644 --- a/src/data/directory_content.rs +++ b/src/data/directory_content.rs @@ -34,7 +34,7 @@ pub struct DirectoryEntry { impl DirectoryEntry { /// Creates a new directory entry from a path - pub fn from_path(config: &FileDialogConfig, path: &Path, vfs: &impl FileSystem) -> Self { + pub fn from_path(config: &FileDialogConfig, path: &Path, vfs: &dyn FileSystem) -> Self { Self { path: path.to_path_buf(), metadata: vfs.metadata(path).unwrap_or_default(), @@ -194,11 +194,12 @@ impl DirectoryContent { path: &Path, include_files: bool, file_filter: Option<&FileFilter>, + vfs: Arc, ) -> Self { if config.load_via_thread { - Self::with_thread(config, path, include_files, file_filter) + Self::with_thread(config, path, include_files, file_filter, vfs) } else { - Self::without_thread(config, path, include_files, file_filter) + Self::without_thread(config, path, include_files, file_filter, &*vfs) } } @@ -207,6 +208,7 @@ impl DirectoryContent { path: &Path, include_files: bool, file_filter: Option<&FileFilter>, + vfs: Arc, ) -> Self { let (tx, rx) = mpsc::channel(); @@ -214,7 +216,7 @@ impl DirectoryContent { let p = path.to_path_buf(); let f = file_filter.cloned(); thread::spawn(move || { - let _ = tx.send(load_directory(&c, &p, include_files, f.as_ref())); + let _ = tx.send(load_directory(&c, &p, include_files, f.as_ref(), &*vfs)); }); Self { @@ -229,8 +231,9 @@ impl DirectoryContent { path: &Path, include_files: bool, file_filter: Option<&FileFilter>, + vfs: &dyn FileSystem, ) -> Self { - match load_directory(config, path, include_files, file_filter) { + match load_directory(config, path, include_files, file_filter, vfs) { Ok(c) => Self { state: DirectoryContentState::Success, content: c, @@ -350,38 +353,31 @@ fn load_directory( path: &Path, include_files: bool, file_filter: Option<&FileFilter>, - vfs: &impl FileSystem, + vfs: &dyn FileSystem, ) -> io::Result> { - let paths = fs::read_dir(path)?; - let mut result: Vec = Vec::new(); - for path in paths { - match path { - Ok(entry) => { - let entry = DirectoryEntry::from_path(config, entry.path().as_path(), vfs); - - if !config.storage.show_system_files && entry.is_system_file() { - continue; - } + for path in vfs.read_dir(path)? { + let entry = DirectoryEntry::from_path(config, &path, vfs); - if !include_files && entry.is_file() { - continue; - } + if !config.storage.show_system_files && entry.is_system_file() { + continue; + } - if !config.storage.show_hidden && entry.is_hidden() { - continue; - } + if !include_files && entry.is_file() { + continue; + } - if let Some(file_filter) = file_filter { - if entry.is_file() && !(file_filter.filter)(entry.as_path()) { - continue; - } - } + if !config.storage.show_hidden && entry.is_hidden() { + continue; + } - result.push(entry); + if let Some(file_filter) = file_filter { + if entry.is_file() && !(file_filter.filter)(entry.as_path()) { + continue; } - Err(_) => continue, - }; + } + + result.push(entry); } result.sort_by(|a, b| { diff --git a/src/file_dialog.rs b/src/file_dialog.rs index c5ca2d97..e6b535ce 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -7,10 +7,12 @@ use crate::data::{ DirectoryContent, DirectoryContentState, DirectoryEntry, Disk, Disks, UserDirectories, }; use crate::modals::{FileDialogModal, ModalAction, ModalState, OverwriteFileModal}; +use crate::{FileSystem, NativeFileSystem}; use egui::text::{CCursor, CCursorRange}; use std::fmt::Debug; use std::io; use std::path::{Path, PathBuf}; +use std::sync::Arc; /// Represents the mode the file dialog is currently in. #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -154,6 +156,8 @@ pub struct FileDialog { /// This is used to prevent the dialog from closing when pressing the escape key /// inside a text input. any_focused_last_frame: bool, + + vfs: Arc, } /// This tests if file dialog is send and sync. @@ -228,6 +232,8 @@ impl FileDialog { init_search: false, any_focused_last_frame: false, + + vfs: Arc::new(NativeFileSystem), } } @@ -1278,7 +1284,7 @@ impl FileDialog { } else if let Some(parent) = path.parent() { // Else, go to the parent directory self.load_directory(parent); - self.select_item(&mut DirectoryEntry::from_path(&self.config, path)); + self.select_item(&mut DirectoryEntry::from_path(&self.config, path, &*self.vfs)); self.scroll_to_selection = true; repaint = true; } @@ -2180,7 +2186,7 @@ impl FileDialog { DirectoryContentState::Finished => { if self.mode == DialogMode::SelectDirectory { if let Some(dir) = self.current_directory() { - let mut dir_entry = DirectoryEntry::from_path(&self.config, dir); + let mut dir_entry = DirectoryEntry::from_path(&self.config, dir, &*self.vfs); self.select_item(&mut dir_entry); } } @@ -2823,7 +2829,7 @@ impl FileDialog { /// Function that processes a newly created folder. fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry { - let mut entry = DirectoryEntry::from_path(&self.config, created_dir); + let mut entry = DirectoryEntry::from_path(&self.config, created_dir, &*self.vfs); self.directory_content.push(entry.clone()); @@ -3276,6 +3282,7 @@ impl FileDialog { path, self.show_files, self.get_selected_file_filter(), + self.vfs.clone(), ); self.create_directory_dialog.close(); diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index 0fa78aab..4b0d94df 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -18,6 +18,12 @@ pub trait FileSystem { fn read_dir(&self, path: &Path) -> io::Result>; } +impl std::fmt::Debug for dyn FileSystem + Send + Sync { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + /// Implementation of FileSystem using the standard library pub struct NativeFileSystem; From 671137434e174b8a23ae92b18dfda86bc6066181 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 15:39:32 -0800 Subject: [PATCH 03/29] File preview --- src/information_panel.rs | 32 ++++++-------------------------- src/virtual_fs.rs | 32 +++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/information_panel.rs b/src/information_panel.rs index bc3b1262..c8869e40 100644 --- a/src/information_panel.rs +++ b/src/information_panel.rs @@ -1,6 +1,6 @@ #![cfg(feature = "information_view")] -use crate::{DirectoryEntry, FileDialog}; +use crate::{DirectoryEntry, FileDialog, FileSystem, NativeFileSystem}; use chrono::{DateTime, Local}; use egui::ahash::{HashMap, HashMapExt}; use egui::{Direction, Layout, Ui, Vec2}; @@ -8,6 +8,7 @@ use indexmap::{IndexMap, IndexSet}; use std::fs::File; use std::io::{self, Read}; use std::path::PathBuf; +use std::sync::Arc; type SupportedPreviewFilesMap = HashMap>; type SupportedPreviewImagesMap = @@ -81,6 +82,8 @@ pub struct InformationPanel { other_meta_data: IndexMap, /// Stores the images already loaded by the egui loaders. stored_images: IndexSet, + + vfs: Arc, } impl Default for InformationPanel { @@ -170,6 +173,7 @@ impl Default for InformationPanel { additional_meta_files, other_meta_data: IndexMap::default(), stored_images: IndexSet::default(), + vfs: Arc::new(NativeFileSystem), } } } @@ -224,33 +228,9 @@ impl InformationPanel { self } - /// Reads a preview of the file if it is detected as a text file. - fn load_text_file_preview(path: PathBuf, max_chars: usize) -> io::Result { - let mut file = File::open(path)?; - let mut chunk = [0; 96]; // Temporary buffer - let mut buffer = String::new(); - - // Add the first chunk to the buffer as text - let mut total_read = 0; - - // Continue reading if needed - while total_read < max_chars { - let bytes_read = file.read(&mut chunk)?; - if bytes_read == 0 { - break; // End of file - } - let chars_read: String = String::from_utf8(chunk[..bytes_read].to_vec()) - .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?; - total_read += chars_read.len(); - buffer.push_str(&chars_read); - } - - Ok(buffer.to_string()) - } - fn load_content(&self, path: PathBuf) -> Option { if self.load_text_content { - Self::load_text_file_preview(path, self.text_content_max_chars).ok() + self.vfs.load_text_file_preview(&path, self.text_content_max_chars).ok() } else { None } diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index 4b0d94df..5a292a32 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -1,9 +1,9 @@ use std::path::{Path, PathBuf}; -use std::io; +use std::io::{self, Read}; use crate::data::Metadata; -/// File system abstraction +/// File system abstraction specific to the needs of egui-file-dialog pub trait FileSystem { /// Queries metadata for the given path fn metadata(&self, path: &Path) -> io::Result; @@ -14,8 +14,11 @@ pub trait FileSystem { /// Returns true if the path exists and is a file fn is_file(&self, path: &Path) -> bool; - /// Reads + /// Get the children of a directory fn read_dir(&self, path: &Path) -> io::Result>; + + /// Read a short preview of a text file + fn load_text_file_preview(&self, path: &Path, max_chars: usize) -> io::Result; } impl std::fmt::Debug for dyn FileSystem + Send + Sync { @@ -55,4 +58,27 @@ impl FileSystem for NativeFileSystem { .map(|entry| entry.path()) .collect()) } + + fn load_text_file_preview(&self, path: &Path, max_chars: usize) -> io::Result { + let mut file = std::fs::File::open(path)?; + let mut chunk = [0; 96]; // Temporary buffer + let mut buffer = String::new(); + + // Add the first chunk to the buffer as text + let mut total_read = 0; + + // Continue reading if needed + while total_read < max_chars { + let bytes_read = file.read(&mut chunk)?; + if bytes_read == 0 { + break; // End of file + } + let chars_read: String = String::from_utf8(chunk[..bytes_read].to_vec()) + .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?; + total_read += chars_read.len(); + buffer.push_str(&chars_read); + } + + Ok(buffer.to_string()) + } } From d95823b09d050f5d6c3230b458a13c09cec60987 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 15:57:28 -0800 Subject: [PATCH 04/29] Disks and hidden paths --- src/data/directory_content.rs | 21 ++++----------------- src/data/disks.rs | 7 +++++++ src/data/mod.rs | 1 + src/file_dialog.rs | 14 ++++++++++---- src/information_panel.rs | 2 -- src/virtual_fs.rs | 35 ++++++++++++++++++++++++++++++++++- 6 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/data/directory_content.rs b/src/data/directory_content.rs index 3f53f4ff..81ed98cf 100644 --- a/src/data/directory_content.rs +++ b/src/data/directory_content.rs @@ -4,7 +4,7 @@ use egui::mutex::Mutex; use std::path::{Path, PathBuf}; use std::sync::{mpsc, Arc}; use std::time::SystemTime; -use std::{fs, io, thread}; +use std::{io, thread}; /// Contains the metadata of a directory item. #[derive(Debug, Default, Clone)] @@ -27,6 +27,7 @@ pub struct DirectoryEntry { metadata: Metadata, is_directory: bool, is_system_file: bool, + is_hidden: bool, icon: String, /// If the item is marked as selected as part of a multi selection. pub selected: bool, @@ -41,6 +42,7 @@ impl DirectoryEntry { is_directory: vfs.is_dir(path), is_system_file: !vfs.is_dir(path) && !vfs.is_file(path), icon: gen_path_icon(config, path), + is_hidden: vfs.is_path_hidden(path), selected: false, } } @@ -126,7 +128,7 @@ impl DirectoryEntry { /// Returns whether the path this `DirectoryEntry` points to is considered hidden. pub fn is_hidden(&self) -> bool { - is_path_hidden(self) + self.is_hidden } } @@ -393,21 +395,6 @@ fn load_directory( Ok(result) } -#[cfg(windows)] -fn is_path_hidden(item: &DirectoryEntry) -> bool { - use std::os::windows::fs::MetadataExt; - - fs::metadata(item.as_path()).map_or(false, |metadata| metadata.file_attributes() & 0x2 > 0) -} - -#[cfg(not(windows))] -fn is_path_hidden(item: &DirectoryEntry) -> bool { - if item.file_name().bytes().next() == Some(b'.') { - return true; - } - - false -} /// Generates the icon for the specific path. /// The default icon configuration is taken into account, as well as any configured diff --git a/src/data/disks.rs b/src/data/disks.rs index 7b3d0f29..3a97916d 100644 --- a/src/data/disks.rs +++ b/src/data/disks.rs @@ -81,13 +81,20 @@ pub struct Disks { disks: Vec, } +pub fn native_load_disks(canonicalize_paths: bool) -> Disks { + Disks { disks: load_disks(canonicalize_paths) } +} + impl Disks { + /* /// Creates a new Disks object with a refreshed list of the system disks. pub fn new_with_refreshed_list(canonicalize_paths: bool) -> Self { Self { disks: load_disks(canonicalize_paths), } } + */ + /// Very simple wrapper method of the disks `.iter()` method. /// No trait is implemented since this is currently only used internal. pub fn iter(&self) -> std::slice::Iter<'_, Disk> { diff --git a/src/data/mod.rs b/src/data/mod.rs index 78ae97b5..45199fa6 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -3,6 +3,7 @@ pub use directory_content::{DirectoryContent, DirectoryContentState, DirectoryEn mod disks; pub use disks::{Disk, Disks}; +pub(crate) use disks::native_load_disks; mod user_directories; diff --git a/src/file_dialog.rs b/src/file_dialog.rs index e6b535ce..600ab2b4 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -195,6 +195,12 @@ impl FileDialog { /// Creates a new file dialog instance with default values. #[must_use] pub fn new() -> Self { + Self::from_filesystem(Arc::new(NativeFileSystem)) + } + + #[must_use] + /// Creates a new file dialog instance with the given file system abstraction + pub fn from_filesystem(vfs: Arc) -> Self { Self { config: FileDialogConfig::default(), @@ -208,7 +214,7 @@ impl FileDialog { window_id: egui::Id::new("file_dialog"), user_directories: UserDirectories::new(true), - system_disks: Disks::new_with_refreshed_list(true), + system_disks: vfs.get_disks(true), directory_stack: Vec::new(), directory_offset: 0, @@ -232,8 +238,8 @@ impl FileDialog { init_search: false, any_focused_last_frame: false, - - vfs: Arc::new(NativeFileSystem), + + vfs, } } @@ -2900,7 +2906,7 @@ impl FileDialog { /// Including the user directories, system disks and currently open directory. fn refresh(&mut self) { self.user_directories = UserDirectories::new(self.config.canonicalize_paths); - self.system_disks = Disks::new_with_refreshed_list(self.config.canonicalize_paths); + self.system_disks = self.vfs.get_disks(self.config.canonicalize_paths); self.reload_directory(); } diff --git a/src/information_panel.rs b/src/information_panel.rs index c8869e40..ea1f641c 100644 --- a/src/information_panel.rs +++ b/src/information_panel.rs @@ -5,8 +5,6 @@ use chrono::{DateTime, Local}; use egui::ahash::{HashMap, HashMapExt}; use egui::{Direction, Layout, Ui, Vec2}; use indexmap::{IndexMap, IndexSet}; -use std::fs::File; -use std::io::{self, Read}; use std::path::PathBuf; use std::sync::Arc; diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index 5a292a32..d6cda7f3 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use std::io::{self, Read}; -use crate::data::Metadata; +use crate::data::{native_load_disks, Disks, Metadata}; /// File system abstraction specific to the needs of egui-file-dialog pub trait FileSystem { @@ -19,6 +19,12 @@ pub trait FileSystem { /// Read a short preview of a text file fn load_text_file_preview(&self, path: &Path, max_chars: usize) -> io::Result; + + /// List out the disks in the system + fn get_disks(&self, canonicalize_paths: bool) -> Disks; + + /// Determine if a path is hidden + fn is_path_hidden(&self, path: &Path) -> bool; } impl std::fmt::Debug for dyn FileSystem + Send + Sync { @@ -81,4 +87,31 @@ impl FileSystem for NativeFileSystem { Ok(buffer.to_string()) } + + fn get_disks(&self, canonicalize_paths: bool) -> Disks { + native_load_disks(canonicalize_paths) + } + + fn is_path_hidden(&self, path: &Path) -> bool { + is_path_hidden(path) + } +} + +#[cfg(windows)] +fn is_path_hidden(item: &Path) -> bool { + use std::os::windows::fs::MetadataExt; + + std::fs::metadata(path).map_or(false, |metadata| metadata.file_attributes() & 0x2 > 0) +} + +#[cfg(not(windows))] +fn is_path_hidden(path: &Path) -> bool { + let Some(file_name) = path.file_name() else { return false }; + let Some(s) = file_name.to_str() else { return false }; + + if s.chars().next() == Some('.') { + return true; + } + + false } From 2f32e81d6b5df99c9e4dac3e5066a0946dc912fc Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 16:07:03 -0800 Subject: [PATCH 05/29] Create dir dialog --- src/create_directory_dialog.rs | 10 +++++++--- src/file_dialog.rs | 2 +- src/virtual_fs.rs | 7 +++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/create_directory_dialog.rs b/src/create_directory_dialog.rs index 19658f2e..369627e6 100644 --- a/src/create_directory_dialog.rs +++ b/src/create_directory_dialog.rs @@ -1,7 +1,8 @@ use std::fs; use std::path::{Path, PathBuf}; +use std::sync::Arc; -use crate::{FileDialogConfig, FileDialogLabels}; +use crate::{FileDialogConfig, FileDialogLabels, FileSystem}; pub struct CreateDirectoryResponse { /// Contains the path to the directory that was created. @@ -47,11 +48,13 @@ pub struct CreateDirectoryDialog { scroll_to_error: bool, /// If the text input should request focus in the next frame request_focus: bool, + + vfs: Arc, } impl CreateDirectoryDialog { /// Creates a new dialog with default values - pub const fn new() -> Self { + pub fn from_filestem(vfs: Arc) -> Self { Self { open: false, init: false, @@ -61,6 +64,7 @@ impl CreateDirectoryDialog { error: None, scroll_to_error: false, request_focus: true, + vfs, } } @@ -177,7 +181,7 @@ impl CreateDirectoryDialog { if let Some(mut dir) = self.directory.clone() { dir.push(self.input.as_str()); - match fs::create_dir(&dir) { + match self.vfs.create_dir(&dir) { Ok(()) => { self.close(); return CreateDirectoryResponse::new(dir.as_path()); diff --git a/src/file_dialog.rs b/src/file_dialog.rs index 600ab2b4..e5e6690e 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -220,7 +220,7 @@ impl FileDialog { directory_offset: 0, directory_content: DirectoryContent::default(), - create_directory_dialog: CreateDirectoryDialog::new(), + create_directory_dialog: CreateDirectoryDialog::from_filestem(vfs.clone()), path_edit_visible: false, path_edit_value: String::new(), diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index d6cda7f3..e43e83de 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -25,6 +25,9 @@ pub trait FileSystem { /// Determine if a path is hidden fn is_path_hidden(&self, path: &Path) -> bool; + + /// Creates a new directory + fn create_dir(&self, path: &Path) -> io::Result<()>; } impl std::fmt::Debug for dyn FileSystem + Send + Sync { @@ -95,6 +98,10 @@ impl FileSystem for NativeFileSystem { fn is_path_hidden(&self, path: &Path) -> bool { is_path_hidden(path) } + + fn create_dir(&self, path: &Path) -> io::Result<()> { + std::fs::create_dir(path) + } } #[cfg(windows)] From 87d957411961ae48269ed2a64a59fe3e7bc28560 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 16:13:16 -0800 Subject: [PATCH 06/29] User directories --- src/create_directory_dialog.rs | 1 - src/data/user_directories.rs | 37 +++++++++------------------------- src/file_dialog.rs | 4 ++-- src/virtual_fs.rs | 22 +++++++++++++++++++- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/create_directory_dialog.rs b/src/create_directory_dialog.rs index 369627e6..95e8213d 100644 --- a/src/create_directory_dialog.rs +++ b/src/create_directory_dialog.rs @@ -1,4 +1,3 @@ -use std::fs; use std::path::{Path, PathBuf}; use std::sync::Arc; diff --git a/src/data/user_directories.rs b/src/data/user_directories.rs index 496d838f..6bfc5720 100644 --- a/src/data/user_directories.rs +++ b/src/data/user_directories.rs @@ -4,36 +4,17 @@ use std::path::{Path, PathBuf}; /// Currently only used to canonicalize the paths. #[derive(Default, Clone, Debug)] pub struct UserDirectories { - home_dir: Option, - - audio_dir: Option, - desktop_dir: Option, - document_dir: Option, - download_dir: Option, - picture_dir: Option, - video_dir: Option, + pub home_dir: Option, + + pub audio_dir: Option, + pub desktop_dir: Option, + pub document_dir: Option, + pub download_dir: Option, + pub picture_dir: Option, + pub video_dir: Option, } impl UserDirectories { - /// Creates a new `UserDirectories` object. - /// Returns None if no valid home directory path could be retrieved from the operating system. - pub fn new(canonicalize_paths: bool) -> Option { - if let Some(dirs) = directories::UserDirs::new() { - return Some(Self { - home_dir: Self::canonicalize(Some(dirs.home_dir()), canonicalize_paths), - - audio_dir: Self::canonicalize(dirs.audio_dir(), canonicalize_paths), - desktop_dir: Self::canonicalize(dirs.desktop_dir(), canonicalize_paths), - document_dir: Self::canonicalize(dirs.document_dir(), canonicalize_paths), - download_dir: Self::canonicalize(dirs.download_dir(), canonicalize_paths), - picture_dir: Self::canonicalize(dirs.picture_dir(), canonicalize_paths), - video_dir: Self::canonicalize(dirs.video_dir(), canonicalize_paths), - }); - } - - None - } - pub fn home_dir(&self) -> Option<&Path> { self.home_dir.as_deref() } @@ -63,7 +44,7 @@ impl UserDirectories { } /// Canonicalizes the given paths. Returns None if an error occurred. - fn canonicalize(path: Option<&Path>, canonicalize: bool) -> Option { + pub fn canonicalize(path: Option<&Path>, canonicalize: bool) -> Option { if !canonicalize { return path.map(PathBuf::from); } diff --git a/src/file_dialog.rs b/src/file_dialog.rs index e5e6690e..d188fcc3 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -213,7 +213,7 @@ impl FileDialog { window_id: egui::Id::new("file_dialog"), - user_directories: UserDirectories::new(true), + user_directories: vfs.user_dirs(true), system_disks: vfs.get_disks(true), directory_stack: Vec::new(), @@ -2905,7 +2905,7 @@ impl FileDialog { /// Refreshes the dialog. /// Including the user directories, system disks and currently open directory. fn refresh(&mut self) { - self.user_directories = UserDirectories::new(self.config.canonicalize_paths); + self.user_directories = self.vfs.user_dirs(self.config.canonicalize_paths); self.system_disks = self.vfs.get_disks(self.config.canonicalize_paths); self.reload_directory(); diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index e43e83de..d1c48cc4 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use std::io::{self, Read}; -use crate::data::{native_load_disks, Disks, Metadata}; +use crate::data::{native_load_disks, Disks, Metadata, UserDirectories}; /// File system abstraction specific to the needs of egui-file-dialog pub trait FileSystem { @@ -28,6 +28,9 @@ pub trait FileSystem { /// Creates a new directory fn create_dir(&self, path: &Path) -> io::Result<()>; + + /// Returns the user directories + fn user_dirs(&self, canonicalize_paths: bool) -> Option; } impl std::fmt::Debug for dyn FileSystem + Send + Sync { @@ -102,6 +105,23 @@ impl FileSystem for NativeFileSystem { fn create_dir(&self, path: &Path) -> io::Result<()> { std::fs::create_dir(path) } + + fn user_dirs(&self, canonicalize_paths: bool) -> Option { + if let Some(dirs) = directories::UserDirs::new() { + return Some(UserDirectories { + home_dir: UserDirectories::canonicalize(Some(dirs.home_dir()), canonicalize_paths), + + audio_dir: UserDirectories::canonicalize(dirs.audio_dir(), canonicalize_paths), + desktop_dir: UserDirectories::canonicalize(dirs.desktop_dir(), canonicalize_paths), + document_dir: UserDirectories::canonicalize(dirs.document_dir(), canonicalize_paths), + download_dir: UserDirectories::canonicalize(dirs.download_dir(), canonicalize_paths), + picture_dir: UserDirectories::canonicalize(dirs.picture_dir(), canonicalize_paths), + video_dir: UserDirectories::canonicalize(dirs.video_dir(), canonicalize_paths), + }); + } + + None + } } #[cfg(windows)] From 2d4c29308565bf0b57b74761ee946e7bdd9cdd5b Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 16:56:06 -0800 Subject: [PATCH 07/29] make more public --- examples/pick_file.rs | 2 +- src/data/disks.rs | 8 ++++---- src/lib.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/pick_file.rs b/examples/pick_file.rs index 0ffb6ccc..1b0b2b62 100644 --- a/examples/pick_file.rs +++ b/examples/pick_file.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; +use egui_file_dialog::FileDialog; use eframe::egui; -use egui_file_dialog::FileDialog; struct MyApp { file_dialog: FileDialog, diff --git a/src/data/disks.rs b/src/data/disks.rs index 3a97916d..66b78b5b 100644 --- a/src/data/disks.rs +++ b/src/data/disks.rs @@ -7,9 +7,9 @@ use std::path::{Path, PathBuf}; /// the names of the disks are generated dynamically. #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct Disk { - mount_point: PathBuf, - display_name: String, - is_removable: bool, + pub mount_point: PathBuf, + pub display_name: String, + pub is_removable: bool, } impl Disk { @@ -78,7 +78,7 @@ impl Disk { /// Wrapper above the `sysinfo::Disks` struct #[derive(Default, Debug)] pub struct Disks { - disks: Vec, + pub disks: Vec, } pub fn native_load_disks(canonicalize_paths: bool) -> Disks { diff --git a/src/lib.rs b/src/lib.rs index 9f83f820..346c98ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -207,7 +207,7 @@ pub use config::{ FileDialogConfig, FileDialogKeyBindings, FileDialogLabels, FileDialogStorage, IconFilter, KeyBinding, QuickAccess, QuickAccessPath, }; -pub use data::DirectoryEntry; +pub use data::{DirectoryEntry, Metadata, UserDirectories, Disks}; pub use file_dialog::{DialogMode, DialogState, FileDialog}; pub use virtual_fs::{FileSystem, NativeFileSystem}; From e941b72892e49a240bf1941ba9995022a3b2e319 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 17:15:46 -0800 Subject: [PATCH 08/29] Don't try to use threading on wasm --- src/config/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/config/mod.rs b/src/config/mod.rs index be076ace..7ac8f2c9 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -221,7 +221,12 @@ impl Default for FileDialogConfig { allow_path_edit_to_save_file_without_extension: false, directory_separator: String::from(">"), canonicalize_paths: true, + + #[cfg(target_arch = "wasm32")] + load_via_thread: false, + #[cfg(not(target_arch = "wasm32"))] load_via_thread: true, + truncate_filenames: true, err_icon: String::from("⚠"), From 43bdb1a0814e015a09f2fb4860dd3e84b259977a Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 17:42:48 -0800 Subject: [PATCH 09/29] Export disk --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 346c98ff..08af8766 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -207,7 +207,7 @@ pub use config::{ FileDialogConfig, FileDialogKeyBindings, FileDialogLabels, FileDialogStorage, IconFilter, KeyBinding, QuickAccess, QuickAccessPath, }; -pub use data::{DirectoryEntry, Metadata, UserDirectories, Disks}; +pub use data::{DirectoryEntry, Metadata, UserDirectories, Disks, Disk}; pub use file_dialog::{DialogMode, DialogState, FileDialog}; pub use virtual_fs::{FileSystem, NativeFileSystem}; From 14a63dcfeb1ded0b0808846e2b89b917c0353a2c Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 17:45:11 -0800 Subject: [PATCH 10/29] Add a new constructor for file dialog --- src/file_dialog.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/file_dialog.rs b/src/file_dialog.rs index d188fcc3..75333362 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -243,6 +243,16 @@ impl FileDialog { } } + /// Creates a new file dialog object and initializes it with the specified configuration. + pub fn with_config_and_vfs(config: FileDialogConfig, vfs: Arc) -> Self { + let mut obj = Self::from_filesystem(vfs); + *obj.config_mut() = config; + + obj.refresh(); + + obj + } + /// Creates a new file dialog object and initializes it with the specified configuration. pub fn with_config(config: FileDialogConfig) -> Self { let mut obj = Self::new(); @@ -2899,7 +2909,7 @@ impl FileDialog { /// Configuration variables are retained. fn reset(&mut self) { let config = self.config.clone(); - *self = Self::with_config(config); + *self = Self::with_config_and_vfs(config, self.vfs.clone()); } /// Refreshes the dialog. From 1779ebc62dbce6ccd471e6a1ede3fd7b17dcc614 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 23:04:36 -0800 Subject: [PATCH 11/29] Add current_dir to the filesystem abstraction --- src/config/mod.rs | 11 +++++++++-- src/file_dialog.rs | 2 +- src/virtual_fs.rs | 7 +++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 7ac8f2c9..827e23e6 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -8,6 +8,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use crate::data::DirectoryEntry; +use crate::{FileSystem, NativeFileSystem}; /// Contains data of the `FileDialog` that should be stored persistently. #[derive(Debug, Clone)] @@ -206,8 +207,14 @@ pub struct FileDialogConfig { } impl Default for FileDialogConfig { - /// Creates a new configuration with default values fn default() -> Self { + Self::default_from_filesystem(&NativeFileSystem) + } +} + +impl FileDialogConfig { + /// Creates a new configuration with default values + pub fn default_from_filesystem(fs: &dyn FileSystem) -> Self { Self { storage: FileDialogStorage::default(), labels: FileDialogLabels::default(), @@ -215,7 +222,7 @@ impl Default for FileDialogConfig { as_modal: true, modal_overlay_color: egui::Color32::from_rgba_premultiplied(0, 0, 0, 120), - initial_directory: std::env::current_dir().unwrap_or_default(), + initial_directory: fs.current_dir().unwrap_or_default(), default_file_name: String::new(), allow_file_overwrite: true, allow_path_edit_to_save_file_without_extension: false, diff --git a/src/file_dialog.rs b/src/file_dialog.rs index 75333362..d8062f9f 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -202,7 +202,7 @@ impl FileDialog { /// Creates a new file dialog instance with the given file system abstraction pub fn from_filesystem(vfs: Arc) -> Self { Self { - config: FileDialogConfig::default(), + config: FileDialogConfig::default_from_filesystem(&*vfs), modals: Vec::new(), diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index d1c48cc4..3e895361 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -31,6 +31,9 @@ pub trait FileSystem { /// Returns the user directories fn user_dirs(&self, canonicalize_paths: bool) -> Option; + + /// Get the current working directory + fn current_dir(&self) -> io::Result; } impl std::fmt::Debug for dyn FileSystem + Send + Sync { @@ -122,6 +125,10 @@ impl FileSystem for NativeFileSystem { None } + + fn current_dir(&self) -> io::Result { + std::env::current_dir() + } } #[cfg(windows)] From 34dc5b1bf386b6062bbfcbd3a983abae4d5c3389 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sat, 21 Dec 2024 23:58:15 -0800 Subject: [PATCH 12/29] Icons use VFS --- src/data/directory_content.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/directory_content.rs b/src/data/directory_content.rs index 81ed98cf..6c507dcb 100644 --- a/src/data/directory_content.rs +++ b/src/data/directory_content.rs @@ -41,7 +41,7 @@ impl DirectoryEntry { metadata: vfs.metadata(path).unwrap_or_default(), is_directory: vfs.is_dir(path), is_system_file: !vfs.is_dir(path) && !vfs.is_file(path), - icon: gen_path_icon(config, path), + icon: gen_path_icon(config, path, vfs), is_hidden: vfs.is_path_hidden(path), selected: false, } @@ -399,14 +399,14 @@ fn load_directory( /// Generates the icon for the specific path. /// The default icon configuration is taken into account, as well as any configured /// file icon filters. -fn gen_path_icon(config: &FileDialogConfig, path: &Path) -> String { +fn gen_path_icon(config: &FileDialogConfig, path: &Path, vfs: &dyn FileSystem) -> String { for def in &config.file_icon_filters { if (def.filter)(path) { return def.icon.clone(); } } - if path.is_dir() { + if vfs.is_dir(path) { config.default_folder_icon.clone() } else { config.default_file_icon.clone() From 0db3323f5d1e3490ec56008beff12d6b9fa1c3ab Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sun, 22 Dec 2024 00:00:20 -0800 Subject: [PATCH 13/29] Find some more places to add vfs --- src/file_dialog.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/file_dialog.rs b/src/file_dialog.rs index d8062f9f..5cd57186 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -1293,7 +1293,7 @@ impl FileDialog { // Check if files were dropped if let Some(dropped_file) = i.raw.dropped_files.last() { if let Some(path) = &dropped_file.path { - if path.is_dir() { + if self.vfs.is_dir(path) { // If we dropped a directory, go there self.load_directory(path.as_path()); repaint = true; @@ -2582,13 +2582,13 @@ impl FileDialog { let path = item.as_path(); - let file_stem = if path.is_file() { + let file_stem = if item.is_file() { path.file_stem().and_then(|f| f.to_str()).unwrap_or("") } else { item.file_name() }; - let extension = if path.is_file() { + let extension = if item.is_file() { path.extension().map_or(String::new(), |ext| { format!(".{}", ext.to_str().unwrap_or("")) }) @@ -2979,7 +2979,7 @@ impl FileDialog { fn gen_initial_directory(&self, path: &Path) -> PathBuf { let mut path = self.canonicalize_path(path); - if path.is_file() { + if self.vfs.is_file(&path) { if let Some(parent) = path.parent() { path = parent.to_path_buf(); } @@ -3026,11 +3026,11 @@ impl FileDialog { let mut full_path = x.to_path_buf(); full_path.push(self.file_name_input.as_str()); - if full_path.is_dir() { + if self.vfs.is_dir(&full_path) { return Some(self.config.labels.err_directory_exists.clone()); } - if !self.config.allow_file_overwrite && full_path.is_file() { + if !self.config.allow_file_overwrite && self.vfs.is_file(&full_path) { return Some(self.config.labels.err_file_exists.clone()); } } else { @@ -3174,7 +3174,7 @@ impl FileDialog { let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value)); - if self.mode == DialogMode::SelectFile && path.is_file() { + if self.mode == DialogMode::SelectFile && self.vfs.is_file(&path) { self.state = DialogState::Selected(path); return; } @@ -3188,7 +3188,7 @@ impl FileDialog { if self.mode == DialogMode::SaveFile && (path.extension().is_some() || self.config.allow_path_edit_to_save_file_without_extension) - && !path.is_dir() + && !self.vfs.is_dir(&path) && path.parent().is_some_and(std::path::Path::exists) { self.submit_save_file(path); From 3b3802696cc475ff70079afc077bb512d23eb897 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sun, 22 Dec 2024 15:48:46 -0800 Subject: [PATCH 14/29] Example using a custom filesystem --- examples/custom_filesystem.rs | 165 ++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 examples/custom_filesystem.rs diff --git a/examples/custom_filesystem.rs b/examples/custom_filesystem.rs new file mode 100644 index 00000000..d2200a9f --- /dev/null +++ b/examples/custom_filesystem.rs @@ -0,0 +1,165 @@ +use egui_file_dialog::{FileDialog, FileSystem}; +use std::{ + path::{Component, Path, PathBuf}, + sync::Arc, +}; + +use eframe::egui; + +struct MyApp { + file_dialog: FileDialog, + picked_file: Option, +} + +impl MyApp { + pub fn new(_cc: &eframe::CreationContext) -> Self { + let root = vec![ + ("im_a_file.txt".to_string(), Node::File), + ( + "folder_a".to_string(), + Node::Directory(vec![ + ("hello.txt".to_string(), Node::File), + ("we are files.md".to_string(), Node::File), + ( + "nesting".to_string(), + Node::Directory(vec![( + "Nesting for beginners.pdf".to_string(), + Node::File, + )]), + ), + ]), + ), + ( + "folder_b".to_string(), + Node::Directory(vec![( + "Yeah this is also a directory.tar".to_string(), + Node::File, + )]), + ), + ]; + Self { + file_dialog: FileDialog::from_filesystem(Arc::new(MyFileSystem(root))), + picked_file: None, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + if ui.button("Picked file").clicked() { + self.file_dialog.pick_file(); + } + + ui.label(format!("Picked file: {:?}", self.picked_file)); + + if let Some(path) = self.file_dialog.update(ctx).picked() { + self.picked_file = Some(path.to_path_buf()); + } + }); + } +} + +fn main() -> eframe::Result<()> { + eframe::run_native( + "File dialog example", + eframe::NativeOptions::default(), + Box::new(|ctx| Ok(Box::new(MyApp::new(ctx)))), + ) +} + +type Directory = Vec<(String, Node)>; + +#[derive(Clone, PartialEq)] +enum Node { + Directory(Directory), + File, +} + +struct MyFileSystem(Directory); + +impl MyFileSystem { + fn browse(&self, path: &Path) -> std::io::Result<&Directory> { + let mut dir = &self.0; + for component in path.components() { + let Component::Normal(part) = component else { + continue; + }; + let part = part.to_str().unwrap(); + + let subdir = dir + .iter() + .find_map(|(name, node)| match node { + Node::File => None, + Node::Directory(subdir) => (name == part).then(|| subdir), + }) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::NotFound, + "Directory not found".to_string(), + ) + })?; + + dir = subdir; + } + + Ok(dir) + } +} + +impl FileSystem for MyFileSystem { + fn read_dir(&self, path: &Path) -> std::io::Result> { + let dir = self.browse(path)?; + Ok(dir.iter().map(|(name, _)| path.join(name)).collect()) + } + + fn is_dir(&self, path: &Path) -> bool { + self.browse(path).is_ok() + } + + fn is_file(&self, path: &Path) -> bool { + let Some(parent) = path.parent() else { + return false; + }; + let Ok(dir) = self.browse(parent) else { + return false; + }; + dir.iter().any(|(name, node)| { + node == &Node::File && Some(name.as_str()) == path.file_name().and_then(|s| s.to_str()) + }) + } + + fn metadata(&self, path: &Path) -> std::io::Result { + Ok(Default::default()) + } + + fn get_disks(&self, canonicalize_paths: bool) -> egui_file_dialog::Disks { + Default::default() + } + + fn user_dirs(&self, canonicalize_paths: bool) -> Option { + None + } + + fn create_dir(&self, path: &Path) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Unsupported".to_string(), + )) + } + + fn current_dir(&self) -> std::io::Result { + Ok("".into()) + } + + fn is_path_hidden(&self, path: &Path) -> bool { + false + } + + fn load_text_file_preview(&self, path: &Path, max_chars: usize) -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Unsupported".to_string(), + )) + } +} From 2445a1727cea109a39024e3fd4b380174362f1b6 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Sun, 22 Dec 2024 15:58:07 -0800 Subject: [PATCH 15/29] Extremely basic example of using the vfs --- src/virtual_fs.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index 3e895361..ff4b8a30 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -3,7 +3,22 @@ use std::io::{self, Read}; use crate::data::{native_load_disks, Disks, Metadata, UserDirectories}; -/// File system abstraction specific to the needs of egui-file-dialog +/// An abstraction over the host system, allowing the file dialog to be used to browse e.g. in +/// memory filesystems. +/// +/// # Examples +/// +/// ``` +/// use egui_file_dialog::{FileDialog, FileSystem}; +/// +/// struct MyFileSystem; +/// +/// impl FileSystem for MyFileSystem { /* ... */ } +/// +/// let dialog = FileDialog::from_filesystem(Arc::new(MyFileSystem)); +/// +/// /* Use the file dialog as usual */ +/// ``` pub trait FileSystem { /// Queries metadata for the given path fn metadata(&self, path: &Path) -> io::Result; From ec946e2ca255056bcab173993e5433ca2d2b1f29 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Tue, 24 Dec 2024 14:11:17 -0800 Subject: [PATCH 16/29] Make Disk and Disks' members private --- examples/custom_filesystem.rs | 6 ++++-- src/data/disks.rs | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/examples/custom_filesystem.rs b/examples/custom_filesystem.rs index d2200a9f..1ad30a00 100644 --- a/examples/custom_filesystem.rs +++ b/examples/custom_filesystem.rs @@ -1,4 +1,4 @@ -use egui_file_dialog::{FileDialog, FileSystem}; +use egui_file_dialog::{Disk, Disks, FileDialog, FileSystem}; use std::{ path::{Component, Path, PathBuf}, sync::Arc, @@ -134,7 +134,9 @@ impl FileSystem for MyFileSystem { } fn get_disks(&self, canonicalize_paths: bool) -> egui_file_dialog::Disks { - Default::default() + Disks::new(vec![ + Disk::new(Some("I'm a fake disk"), &PathBuf::from("/disk"), false, true), + ]) } fn user_dirs(&self, canonicalize_paths: bool) -> Option { diff --git a/src/data/disks.rs b/src/data/disks.rs index 66b78b5b..2b08b9fb 100644 --- a/src/data/disks.rs +++ b/src/data/disks.rs @@ -7,12 +7,13 @@ use std::path::{Path, PathBuf}; /// the names of the disks are generated dynamically. #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct Disk { - pub mount_point: PathBuf, - pub display_name: String, - pub is_removable: bool, + mount_point: PathBuf, + display_name: String, + is_removable: bool, } impl Disk { + /// Creates a new disk with the given name and mount point pub fn new( name: Option<&str>, mount_point: &Path, @@ -78,7 +79,7 @@ impl Disk { /// Wrapper above the `sysinfo::Disks` struct #[derive(Default, Debug)] pub struct Disks { - pub disks: Vec, + disks: Vec, } pub fn native_load_disks(canonicalize_paths: bool) -> Disks { @@ -86,6 +87,13 @@ pub fn native_load_disks(canonicalize_paths: bool) -> Disks { } impl Disks { + /// Create a new set of disks + pub fn new(disks: Vec) -> Self { + Self { + disks, + } + } + /* /// Creates a new Disks object with a refreshed list of the system disks. pub fn new_with_refreshed_list(canonicalize_paths: bool) -> Self { From b8721a20b3c047a2e3b78adf08e8ab355f0b7fc9 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Tue, 24 Dec 2024 14:13:46 -0800 Subject: [PATCH 17/29] Add new_native_disks instead of a random fn --- src/data/disks.rs | 14 +++----------- src/data/mod.rs | 1 - src/virtual_fs.rs | 4 ++-- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/data/disks.rs b/src/data/disks.rs index 2b08b9fb..06100bde 100644 --- a/src/data/disks.rs +++ b/src/data/disks.rs @@ -82,10 +82,6 @@ pub struct Disks { disks: Vec, } -pub fn native_load_disks(canonicalize_paths: bool) -> Disks { - Disks { disks: load_disks(canonicalize_paths) } -} - impl Disks { /// Create a new set of disks pub fn new(disks: Vec) -> Self { @@ -94,14 +90,10 @@ impl Disks { } } - /* - /// Creates a new Disks object with a refreshed list of the system disks. - pub fn new_with_refreshed_list(canonicalize_paths: bool) -> Self { - Self { - disks: load_disks(canonicalize_paths), - } + /// Queries the operating system for disks + pub fn new_native_disks(canonicalize_paths: bool) -> Disks { + Disks { disks: load_disks(canonicalize_paths) } } - */ /// Very simple wrapper method of the disks `.iter()` method. /// No trait is implemented since this is currently only used internal. diff --git a/src/data/mod.rs b/src/data/mod.rs index 45199fa6..78ae97b5 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -3,7 +3,6 @@ pub use directory_content::{DirectoryContent, DirectoryContentState, DirectoryEn mod disks; pub use disks::{Disk, Disks}; -pub(crate) use disks::native_load_disks; mod user_directories; diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index ff4b8a30..9e4dc82f 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use std::io::{self, Read}; -use crate::data::{native_load_disks, Disks, Metadata, UserDirectories}; +use crate::data::{Disks, Metadata, UserDirectories}; /// An abstraction over the host system, allowing the file dialog to be used to browse e.g. in /// memory filesystems. @@ -113,7 +113,7 @@ impl FileSystem for NativeFileSystem { } fn get_disks(&self, canonicalize_paths: bool) -> Disks { - native_load_disks(canonicalize_paths) + Disks::new_native_disks(canonicalize_paths) } fn is_path_hidden(&self, path: &Path) -> bool { From a6ba2a8ff77479c2606a92f1556f4e0103320b68 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Tue, 24 Dec 2024 14:17:30 -0800 Subject: [PATCH 18/29] Constructor for UserDirectories --- src/data/user_directories.rs | 37 ++++++++++++++++++++++++++++-------- src/virtual_fs.rs | 19 +++++++++--------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/data/user_directories.rs b/src/data/user_directories.rs index 6bfc5720..4d424c89 100644 --- a/src/data/user_directories.rs +++ b/src/data/user_directories.rs @@ -4,17 +4,38 @@ use std::path::{Path, PathBuf}; /// Currently only used to canonicalize the paths. #[derive(Default, Clone, Debug)] pub struct UserDirectories { - pub home_dir: Option, - - pub audio_dir: Option, - pub desktop_dir: Option, - pub document_dir: Option, - pub download_dir: Option, - pub picture_dir: Option, - pub video_dir: Option, + home_dir: Option, + + audio_dir: Option, + desktop_dir: Option, + document_dir: Option, + download_dir: Option, + picture_dir: Option, + video_dir: Option, } impl UserDirectories { + /// Creates a new custom UserDirectories object + pub fn new( + home_dir: Option, + audio_dir: Option, + desktop_dir: Option, + document_dir: Option, + download_dir: Option, + picture_dir: Option, + video_dir: Option, + ) -> Self { + Self { + home_dir, + audio_dir, + desktop_dir, + document_dir, + download_dir, + picture_dir, + video_dir, + } + } + pub fn home_dir(&self) -> Option<&Path> { self.home_dir.as_deref() } diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index 9e4dc82f..ff6cfce4 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -126,16 +126,15 @@ impl FileSystem for NativeFileSystem { fn user_dirs(&self, canonicalize_paths: bool) -> Option { if let Some(dirs) = directories::UserDirs::new() { - return Some(UserDirectories { - home_dir: UserDirectories::canonicalize(Some(dirs.home_dir()), canonicalize_paths), - - audio_dir: UserDirectories::canonicalize(dirs.audio_dir(), canonicalize_paths), - desktop_dir: UserDirectories::canonicalize(dirs.desktop_dir(), canonicalize_paths), - document_dir: UserDirectories::canonicalize(dirs.document_dir(), canonicalize_paths), - download_dir: UserDirectories::canonicalize(dirs.download_dir(), canonicalize_paths), - picture_dir: UserDirectories::canonicalize(dirs.picture_dir(), canonicalize_paths), - video_dir: UserDirectories::canonicalize(dirs.video_dir(), canonicalize_paths), - }); + return Some(UserDirectories::new( + UserDirectories::canonicalize(Some(dirs.home_dir()), canonicalize_paths), + UserDirectories::canonicalize(dirs.audio_dir(), canonicalize_paths), + UserDirectories::canonicalize(dirs.desktop_dir(), canonicalize_paths), + UserDirectories::canonicalize(dirs.document_dir(), canonicalize_paths), + UserDirectories::canonicalize(dirs.download_dir(), canonicalize_paths), + UserDirectories::canonicalize(dirs.picture_dir(), canonicalize_paths), + UserDirectories::canonicalize(dirs.video_dir(), canonicalize_paths), + )); } None From 765f93d9e331ac407d4bd48f5d0a999d238dc090 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Tue, 24 Dec 2024 14:22:03 -0800 Subject: [PATCH 19/29] Construct for Metadata, rename vfs to file_system --- src/create_directory_dialog.rs | 8 ++--- src/data/directory_content.rs | 61 ++++++++++++++++++++++------------ src/file_dialog.rs | 44 ++++++++++++------------ src/information_panel.rs | 6 ++-- 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/src/create_directory_dialog.rs b/src/create_directory_dialog.rs index 95e8213d..7ad22342 100644 --- a/src/create_directory_dialog.rs +++ b/src/create_directory_dialog.rs @@ -48,12 +48,12 @@ pub struct CreateDirectoryDialog { /// If the text input should request focus in the next frame request_focus: bool, - vfs: Arc, + file_system: Arc, } impl CreateDirectoryDialog { /// Creates a new dialog with default values - pub fn from_filestem(vfs: Arc) -> Self { + pub fn from_filesystem(file_system: Arc) -> Self { Self { open: false, init: false, @@ -63,7 +63,7 @@ impl CreateDirectoryDialog { error: None, scroll_to_error: false, request_focus: true, - vfs, + file_system, } } @@ -180,7 +180,7 @@ impl CreateDirectoryDialog { if let Some(mut dir) = self.directory.clone() { dir.push(self.input.as_str()); - match self.vfs.create_dir(&dir) { + match self.file_system.create_dir(&dir) { Ok(()) => { self.close(); return CreateDirectoryResponse::new(dir.as_path()); diff --git a/src/data/directory_content.rs b/src/data/directory_content.rs index 6c507dcb..2b0e5993 100644 --- a/src/data/directory_content.rs +++ b/src/data/directory_content.rs @@ -10,10 +10,27 @@ use std::{io, thread}; #[derive(Debug, Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Metadata { - pub size: Option, - pub last_modified: Option, - pub created: Option, - pub file_type: Option, + size: Option, + last_modified: Option, + created: Option, + file_type: Option, +} + +impl Metadata { + /// Create a new custom metadata + pub fn new( + size: Option, + last_modified: Option, + created: Option, + file_type: Option, + ) -> Self { + Self { + size, + last_modified, + created, + file_type, + } + } } /// Contains the information of a directory item. @@ -35,14 +52,14 @@ pub struct DirectoryEntry { impl DirectoryEntry { /// Creates a new directory entry from a path - pub fn from_path(config: &FileDialogConfig, path: &Path, vfs: &dyn FileSystem) -> Self { + pub fn from_path(config: &FileDialogConfig, path: &Path, file_system: &dyn FileSystem) -> Self { Self { path: path.to_path_buf(), - metadata: vfs.metadata(path).unwrap_or_default(), - is_directory: vfs.is_dir(path), - is_system_file: !vfs.is_dir(path) && !vfs.is_file(path), - icon: gen_path_icon(config, path, vfs), - is_hidden: vfs.is_path_hidden(path), + metadata: file_system.metadata(path).unwrap_or_default(), + is_directory: file_system.is_dir(path), + is_system_file: !file_system.is_dir(path) && !file_system.is_file(path), + icon: gen_path_icon(config, path, file_system), + is_hidden: file_system.is_path_hidden(path), selected: false, } } @@ -196,12 +213,12 @@ impl DirectoryContent { path: &Path, include_files: bool, file_filter: Option<&FileFilter>, - vfs: Arc, + file_system: Arc, ) -> Self { if config.load_via_thread { - Self::with_thread(config, path, include_files, file_filter, vfs) + Self::with_thread(config, path, include_files, file_filter, file_system) } else { - Self::without_thread(config, path, include_files, file_filter, &*vfs) + Self::without_thread(config, path, include_files, file_filter, &*file_system) } } @@ -210,7 +227,7 @@ impl DirectoryContent { path: &Path, include_files: bool, file_filter: Option<&FileFilter>, - vfs: Arc, + file_system: Arc, ) -> Self { let (tx, rx) = mpsc::channel(); @@ -218,7 +235,7 @@ impl DirectoryContent { let p = path.to_path_buf(); let f = file_filter.cloned(); thread::spawn(move || { - let _ = tx.send(load_directory(&c, &p, include_files, f.as_ref(), &*vfs)); + let _ = tx.send(load_directory(&c, &p, include_files, f.as_ref(), &*file_system)); }); Self { @@ -233,9 +250,9 @@ impl DirectoryContent { path: &Path, include_files: bool, file_filter: Option<&FileFilter>, - vfs: &dyn FileSystem, + file_system: &dyn FileSystem, ) -> Self { - match load_directory(config, path, include_files, file_filter, vfs) { + match load_directory(config, path, include_files, file_filter, file_system) { Ok(c) => Self { state: DirectoryContentState::Success, content: c, @@ -355,11 +372,11 @@ fn load_directory( path: &Path, include_files: bool, file_filter: Option<&FileFilter>, - vfs: &dyn FileSystem, + file_system: &dyn FileSystem, ) -> io::Result> { let mut result: Vec = Vec::new(); - for path in vfs.read_dir(path)? { - let entry = DirectoryEntry::from_path(config, &path, vfs); + for path in file_system.read_dir(path)? { + let entry = DirectoryEntry::from_path(config, &path, file_system); if !config.storage.show_system_files && entry.is_system_file() { continue; @@ -399,14 +416,14 @@ fn load_directory( /// Generates the icon for the specific path. /// The default icon configuration is taken into account, as well as any configured /// file icon filters. -fn gen_path_icon(config: &FileDialogConfig, path: &Path, vfs: &dyn FileSystem) -> String { +fn gen_path_icon(config: &FileDialogConfig, path: &Path, file_system: &dyn FileSystem) -> String { for def in &config.file_icon_filters { if (def.filter)(path) { return def.icon.clone(); } } - if vfs.is_dir(path) { + if file_system.is_dir(path) { config.default_folder_icon.clone() } else { config.default_file_icon.clone() diff --git a/src/file_dialog.rs b/src/file_dialog.rs index 5cd57186..c76c56a3 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -157,7 +157,7 @@ pub struct FileDialog { /// inside a text input. any_focused_last_frame: bool, - vfs: Arc, + file_system: Arc, } /// This tests if file dialog is send and sync. @@ -200,9 +200,9 @@ impl FileDialog { #[must_use] /// Creates a new file dialog instance with the given file system abstraction - pub fn from_filesystem(vfs: Arc) -> Self { + pub fn from_filesystem(file_system: Arc) -> Self { Self { - config: FileDialogConfig::default_from_filesystem(&*vfs), + config: FileDialogConfig::default_from_filesystem(&*file_system), modals: Vec::new(), @@ -213,14 +213,14 @@ impl FileDialog { window_id: egui::Id::new("file_dialog"), - user_directories: vfs.user_dirs(true), - system_disks: vfs.get_disks(true), + user_directories: file_system.user_dirs(true), + system_disks: file_system.get_disks(true), directory_stack: Vec::new(), directory_offset: 0, directory_content: DirectoryContent::default(), - create_directory_dialog: CreateDirectoryDialog::from_filestem(vfs.clone()), + create_directory_dialog: CreateDirectoryDialog::from_filesystem(file_system.clone()), path_edit_visible: false, path_edit_value: String::new(), @@ -239,13 +239,13 @@ impl FileDialog { any_focused_last_frame: false, - vfs, + file_system, } } /// Creates a new file dialog object and initializes it with the specified configuration. - pub fn with_config_and_vfs(config: FileDialogConfig, vfs: Arc) -> Self { - let mut obj = Self::from_filesystem(vfs); + pub fn with_config_and_filesystem(config: FileDialogConfig, file_system: Arc) -> Self { + let mut obj = Self::from_filesystem(file_system); *obj.config_mut() = config; obj.refresh(); @@ -1293,14 +1293,14 @@ impl FileDialog { // Check if files were dropped if let Some(dropped_file) = i.raw.dropped_files.last() { if let Some(path) = &dropped_file.path { - if self.vfs.is_dir(path) { + if self.file_system.is_dir(path) { // If we dropped a directory, go there self.load_directory(path.as_path()); repaint = true; } else if let Some(parent) = path.parent() { // Else, go to the parent directory self.load_directory(parent); - self.select_item(&mut DirectoryEntry::from_path(&self.config, path, &*self.vfs)); + self.select_item(&mut DirectoryEntry::from_path(&self.config, path, &*self.file_system)); self.scroll_to_selection = true; repaint = true; } @@ -2202,7 +2202,7 @@ impl FileDialog { DirectoryContentState::Finished => { if self.mode == DialogMode::SelectDirectory { if let Some(dir) = self.current_directory() { - let mut dir_entry = DirectoryEntry::from_path(&self.config, dir, &*self.vfs); + let mut dir_entry = DirectoryEntry::from_path(&self.config, dir, &*self.file_system); self.select_item(&mut dir_entry); } } @@ -2845,7 +2845,7 @@ impl FileDialog { /// Function that processes a newly created folder. fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry { - let mut entry = DirectoryEntry::from_path(&self.config, created_dir, &*self.vfs); + let mut entry = DirectoryEntry::from_path(&self.config, created_dir, &*self.file_system); self.directory_content.push(entry.clone()); @@ -2909,14 +2909,14 @@ impl FileDialog { /// Configuration variables are retained. fn reset(&mut self) { let config = self.config.clone(); - *self = Self::with_config_and_vfs(config, self.vfs.clone()); + *self = Self::with_config_and_filesystem(config, self.file_system.clone()); } /// Refreshes the dialog. /// Including the user directories, system disks and currently open directory. fn refresh(&mut self) { - self.user_directories = self.vfs.user_dirs(self.config.canonicalize_paths); - self.system_disks = self.vfs.get_disks(self.config.canonicalize_paths); + self.user_directories = self.file_system.user_dirs(self.config.canonicalize_paths); + self.system_disks = self.file_system.get_disks(self.config.canonicalize_paths); self.reload_directory(); } @@ -2979,7 +2979,7 @@ impl FileDialog { fn gen_initial_directory(&self, path: &Path) -> PathBuf { let mut path = self.canonicalize_path(path); - if self.vfs.is_file(&path) { + if self.file_system.is_file(&path) { if let Some(parent) = path.parent() { path = parent.to_path_buf(); } @@ -3026,11 +3026,11 @@ impl FileDialog { let mut full_path = x.to_path_buf(); full_path.push(self.file_name_input.as_str()); - if self.vfs.is_dir(&full_path) { + if self.file_system.is_dir(&full_path) { return Some(self.config.labels.err_directory_exists.clone()); } - if !self.config.allow_file_overwrite && self.vfs.is_file(&full_path) { + if !self.config.allow_file_overwrite && self.file_system.is_file(&full_path) { return Some(self.config.labels.err_file_exists.clone()); } } else { @@ -3174,7 +3174,7 @@ impl FileDialog { let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value)); - if self.mode == DialogMode::SelectFile && self.vfs.is_file(&path) { + if self.mode == DialogMode::SelectFile && self.file_system.is_file(&path) { self.state = DialogState::Selected(path); return; } @@ -3188,7 +3188,7 @@ impl FileDialog { if self.mode == DialogMode::SaveFile && (path.extension().is_some() || self.config.allow_path_edit_to_save_file_without_extension) - && !self.vfs.is_dir(&path) + && !self.file_system.is_dir(&path) && path.parent().is_some_and(std::path::Path::exists) { self.submit_save_file(path); @@ -3298,7 +3298,7 @@ impl FileDialog { path, self.show_files, self.get_selected_file_filter(), - self.vfs.clone(), + self.file_system.clone(), ); self.create_directory_dialog.close(); diff --git a/src/information_panel.rs b/src/information_panel.rs index ea1f641c..dc78bc71 100644 --- a/src/information_panel.rs +++ b/src/information_panel.rs @@ -81,7 +81,7 @@ pub struct InformationPanel { /// Stores the images already loaded by the egui loaders. stored_images: IndexSet, - vfs: Arc, + file_system: Arc, } impl Default for InformationPanel { @@ -171,7 +171,7 @@ impl Default for InformationPanel { additional_meta_files, other_meta_data: IndexMap::default(), stored_images: IndexSet::default(), - vfs: Arc::new(NativeFileSystem), + file_system: Arc::new(NativeFileSystem), } } } @@ -228,7 +228,7 @@ impl InformationPanel { fn load_content(&self, path: PathBuf) -> Option { if self.load_text_content { - self.vfs.load_text_file_preview(&path, self.text_content_max_chars).ok() + self.file_system.load_text_file_preview(&path, self.text_content_max_chars).ok() } else { None } From 9729c20765e29bd084870adda3fbb4ea67f0c0c7 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Tue, 24 Dec 2024 14:26:00 -0800 Subject: [PATCH 20/29] Fix field accessors --- src/data/directory_content.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/directory_content.rs b/src/data/directory_content.rs index 2b0e5993..c8f94156 100644 --- a/src/data/directory_content.rs +++ b/src/data/directory_content.rs @@ -10,10 +10,10 @@ use std::{io, thread}; #[derive(Debug, Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Metadata { - size: Option, - last_modified: Option, - created: Option, - file_type: Option, + pub(crate) size: Option, + pub(crate) last_modified: Option, + pub(crate) created: Option, + pub(crate) file_type: Option, } impl Metadata { From d379a652ee036df0aa6cbc0f776d4d13c30ee1c4 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Tue, 24 Dec 2024 14:29:32 -0800 Subject: [PATCH 21/29] Typo --- src/virtual_fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index ff6cfce4..077ba580 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -29,7 +29,7 @@ pub trait FileSystem { /// Returns true if the path exists and is a file fn is_file(&self, path: &Path) -> bool; - /// Get the children of a directory + /// Gets the children of a directory fn read_dir(&self, path: &Path) -> io::Result>; /// Read a short preview of a text file From 1e6f4e43a1ef61fbc2c693381a0f0a8f1f4279a3 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Fri, 27 Dec 2024 13:40:41 -0800 Subject: [PATCH 22/29] Default impl for load_text_file_preview --- src/virtual_fs.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index 077ba580..a25b3119 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -1,5 +1,5 @@ -use std::path::{Path, PathBuf}; use std::io::{self, Read}; +use std::path::{Path, PathBuf}; use crate::data::{Disks, Metadata, UserDirectories}; @@ -32,9 +32,6 @@ pub trait FileSystem { /// Gets the children of a directory fn read_dir(&self, path: &Path) -> io::Result>; - /// Read a short preview of a text file - fn load_text_file_preview(&self, path: &Path, max_chars: usize) -> io::Result; - /// List out the disks in the system fn get_disks(&self, canonicalize_paths: bool) -> Disks; @@ -49,6 +46,14 @@ pub trait FileSystem { /// Get the current working directory fn current_dir(&self) -> io::Result; + + /// Read a short preview of a text file + fn load_text_file_preview(&self, _path: &Path, _max_chars: usize) -> io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "load_text_file_preview not implemented.".to_string(), + )) + } } impl std::fmt::Debug for dyn FileSystem + Send + Sync { @@ -154,8 +159,12 @@ fn is_path_hidden(item: &Path) -> bool { #[cfg(not(windows))] fn is_path_hidden(path: &Path) -> bool { - let Some(file_name) = path.file_name() else { return false }; - let Some(s) = file_name.to_str() else { return false }; + let Some(file_name) = path.file_name() else { + return false; + }; + let Some(s) = file_name.to_str() else { + return false; + }; if s.chars().next() == Some('.') { return true; From c0cf86805cef745531d6596cdfda415aac7f81ea Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Fri, 3 Jan 2025 19:58:40 -0800 Subject: [PATCH 23/29] Rename virtual_fs.rs to file_system.rs --- src/{virtual_fs.rs => file_system.rs} | 0 src/lib.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/{virtual_fs.rs => file_system.rs} (100%) diff --git a/src/virtual_fs.rs b/src/file_system.rs similarity index 100% rename from src/virtual_fs.rs rename to src/file_system.rs diff --git a/src/lib.rs b/src/lib.rs index 08af8766..ab2f0c00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -201,7 +201,7 @@ mod file_dialog; /// Information panel showing the preview and metadata of the selected item pub mod information_panel; mod modals; -mod virtual_fs; +mod file_system; pub use config::{ FileDialogConfig, FileDialogKeyBindings, FileDialogLabels, FileDialogStorage, IconFilter, @@ -210,4 +210,4 @@ pub use config::{ pub use data::{DirectoryEntry, Metadata, UserDirectories, Disks, Disk}; pub use file_dialog::{DialogMode, DialogState, FileDialog}; -pub use virtual_fs::{FileSystem, NativeFileSystem}; +pub use file_system::{FileSystem, NativeFileSystem}; From fc2c02e387a55fab2416fd43514050f2fbffb6e4 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Fri, 3 Jan 2025 20:07:31 -0800 Subject: [PATCH 24/29] Move filesystem to config --- src/config/mod.rs | 10 ++++++--- src/file_dialog.rs | 51 +++++++++++++++------------------------------- 2 files changed, 23 insertions(+), 38 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 827e23e6..3955a016 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -66,6 +66,8 @@ impl Default for FileDialogStorage { pub struct FileDialogConfig { // ------------------------------------------------------------------------ // Core: + /// File system browsed by the file dialog; may be native or virtual. + pub file_system: Arc, /// Persistent data of the file dialog. pub storage: FileDialogStorage, /// The labels that the dialog uses. @@ -208,13 +210,13 @@ pub struct FileDialogConfig { impl Default for FileDialogConfig { fn default() -> Self { - Self::default_from_filesystem(&NativeFileSystem) + Self::default_from_filesystem(Arc::new(NativeFileSystem)) } } impl FileDialogConfig { /// Creates a new configuration with default values - pub fn default_from_filesystem(fs: &dyn FileSystem) -> Self { + pub fn default_from_filesystem(file_system: Arc) -> Self { Self { storage: FileDialogStorage::default(), labels: FileDialogLabels::default(), @@ -222,7 +224,7 @@ impl FileDialogConfig { as_modal: true, modal_overlay_color: egui::Color32::from_rgba_premultiplied(0, 0, 0, 120), - initial_directory: fs.current_dir().unwrap_or_default(), + initial_directory: file_system.current_dir().unwrap_or_default(), default_file_name: String::new(), allow_file_overwrite: true, allow_path_edit_to_save_file_without_extension: false, @@ -281,6 +283,8 @@ impl FileDialogConfig { show_places: true, show_devices: true, show_removable_devices: true, + + file_system, } } } diff --git a/src/file_dialog.rs b/src/file_dialog.rs index c76c56a3..d1f3489f 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -156,8 +156,6 @@ pub struct FileDialog { /// This is used to prevent the dialog from closing when pressing the escape key /// inside a text input. any_focused_last_frame: bool, - - file_system: Arc, } /// This tests if file dialog is send and sync. @@ -195,15 +193,8 @@ impl FileDialog { /// Creates a new file dialog instance with default values. #[must_use] pub fn new() -> Self { - Self::from_filesystem(Arc::new(NativeFileSystem)) - } - - #[must_use] - /// Creates a new file dialog instance with the given file system abstraction - pub fn from_filesystem(file_system: Arc) -> Self { + let file_system = Arc::new(NativeFileSystem); Self { - config: FileDialogConfig::default_from_filesystem(&*file_system), - modals: Vec::new(), mode: DialogMode::SelectDirectory, @@ -238,19 +229,9 @@ impl FileDialog { init_search: false, any_focused_last_frame: false, - - file_system, - } - } - /// Creates a new file dialog object and initializes it with the specified configuration. - pub fn with_config_and_filesystem(config: FileDialogConfig, file_system: Arc) -> Self { - let mut obj = Self::from_filesystem(file_system); - *obj.config_mut() = config; - - obj.refresh(); - - obj + config: FileDialogConfig::default_from_filesystem(file_system), + } } /// Creates a new file dialog object and initializes it with the specified configuration. @@ -1293,14 +1274,14 @@ impl FileDialog { // Check if files were dropped if let Some(dropped_file) = i.raw.dropped_files.last() { if let Some(path) = &dropped_file.path { - if self.file_system.is_dir(path) { + if self.config.file_system.is_dir(path) { // If we dropped a directory, go there self.load_directory(path.as_path()); repaint = true; } else if let Some(parent) = path.parent() { // Else, go to the parent directory self.load_directory(parent); - self.select_item(&mut DirectoryEntry::from_path(&self.config, path, &*self.file_system)); + self.select_item(&mut DirectoryEntry::from_path(&self.config, path, &*self.config.file_system)); self.scroll_to_selection = true; repaint = true; } @@ -2202,7 +2183,7 @@ impl FileDialog { DirectoryContentState::Finished => { if self.mode == DialogMode::SelectDirectory { if let Some(dir) = self.current_directory() { - let mut dir_entry = DirectoryEntry::from_path(&self.config, dir, &*self.file_system); + let mut dir_entry = DirectoryEntry::from_path(&self.config, dir, &*self.config.file_system); self.select_item(&mut dir_entry); } } @@ -2845,7 +2826,7 @@ impl FileDialog { /// Function that processes a newly created folder. fn process_new_folder(&mut self, created_dir: &Path) -> DirectoryEntry { - let mut entry = DirectoryEntry::from_path(&self.config, created_dir, &*self.file_system); + let mut entry = DirectoryEntry::from_path(&self.config, created_dir, &*self.config.file_system); self.directory_content.push(entry.clone()); @@ -2909,14 +2890,14 @@ impl FileDialog { /// Configuration variables are retained. fn reset(&mut self) { let config = self.config.clone(); - *self = Self::with_config_and_filesystem(config, self.file_system.clone()); + *self = Self::with_config(config); } /// Refreshes the dialog. /// Including the user directories, system disks and currently open directory. fn refresh(&mut self) { - self.user_directories = self.file_system.user_dirs(self.config.canonicalize_paths); - self.system_disks = self.file_system.get_disks(self.config.canonicalize_paths); + self.user_directories = self.config.file_system.user_dirs(self.config.canonicalize_paths); + self.system_disks = self.config.file_system.get_disks(self.config.canonicalize_paths); self.reload_directory(); } @@ -2979,7 +2960,7 @@ impl FileDialog { fn gen_initial_directory(&self, path: &Path) -> PathBuf { let mut path = self.canonicalize_path(path); - if self.file_system.is_file(&path) { + if self.config.file_system.is_file(&path) { if let Some(parent) = path.parent() { path = parent.to_path_buf(); } @@ -3026,11 +3007,11 @@ impl FileDialog { let mut full_path = x.to_path_buf(); full_path.push(self.file_name_input.as_str()); - if self.file_system.is_dir(&full_path) { + if self.config.file_system.is_dir(&full_path) { return Some(self.config.labels.err_directory_exists.clone()); } - if !self.config.allow_file_overwrite && self.file_system.is_file(&full_path) { + if !self.config.allow_file_overwrite && self.config.file_system.is_file(&full_path) { return Some(self.config.labels.err_file_exists.clone()); } } else { @@ -3174,7 +3155,7 @@ impl FileDialog { let path = self.canonicalize_path(&PathBuf::from(&self.path_edit_value)); - if self.mode == DialogMode::SelectFile && self.file_system.is_file(&path) { + if self.mode == DialogMode::SelectFile && self.config.file_system.is_file(&path) { self.state = DialogState::Selected(path); return; } @@ -3188,7 +3169,7 @@ impl FileDialog { if self.mode == DialogMode::SaveFile && (path.extension().is_some() || self.config.allow_path_edit_to_save_file_without_extension) - && !self.file_system.is_dir(&path) + && !self.config.file_system.is_dir(&path) && path.parent().is_some_and(std::path::Path::exists) { self.submit_save_file(path); @@ -3298,7 +3279,7 @@ impl FileDialog { path, self.show_files, self.get_selected_file_filter(), - self.file_system.clone(), + self.config.file_system.clone(), ); self.create_directory_dialog.close(); From 735f25090dc8368cab8c5f2091f057afb41f5e9f Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Fri, 3 Jan 2025 20:12:34 -0800 Subject: [PATCH 25/29] Make some undocumented methods pub(crate) for now --- src/data/disks.rs | 1 + src/data/user_directories.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/data/disks.rs b/src/data/disks.rs index 06100bde..23d8efd3 100644 --- a/src/data/disks.rs +++ b/src/data/disks.rs @@ -71,6 +71,7 @@ impl Disk { &self.display_name } + /// Returns true if the disk is removable pub const fn is_removable(&self) -> bool { self.is_removable } diff --git a/src/data/user_directories.rs b/src/data/user_directories.rs index 4d424c89..c3da63e6 100644 --- a/src/data/user_directories.rs +++ b/src/data/user_directories.rs @@ -36,31 +36,31 @@ impl UserDirectories { } } - pub fn home_dir(&self) -> Option<&Path> { + pub(crate) fn home_dir(&self) -> Option<&Path> { self.home_dir.as_deref() } - pub fn audio_dir(&self) -> Option<&Path> { + pub(crate) fn audio_dir(&self) -> Option<&Path> { self.audio_dir.as_deref() } - pub fn desktop_dir(&self) -> Option<&Path> { + pub(crate) fn desktop_dir(&self) -> Option<&Path> { self.desktop_dir.as_deref() } - pub fn document_dir(&self) -> Option<&Path> { + pub(crate) fn document_dir(&self) -> Option<&Path> { self.document_dir.as_deref() } - pub fn download_dir(&self) -> Option<&Path> { + pub(crate) fn download_dir(&self) -> Option<&Path> { self.download_dir.as_deref() } - pub fn picture_dir(&self) -> Option<&Path> { + pub(crate) fn picture_dir(&self) -> Option<&Path> { self.picture_dir.as_deref() } - pub fn video_dir(&self) -> Option<&Path> { + pub(crate) fn video_dir(&self) -> Option<&Path> { self.video_dir.as_deref() } From 1c0c685dfaa5cce4d5d736d1a3c109bb99d7c646 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Fri, 3 Jan 2025 20:14:33 -0800 Subject: [PATCH 26/29] Adapt custom filesystem example code to use config API --- examples/custom_filesystem.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/custom_filesystem.rs b/examples/custom_filesystem.rs index 1ad30a00..e1870dfd 100644 --- a/examples/custom_filesystem.rs +++ b/examples/custom_filesystem.rs @@ -1,4 +1,4 @@ -use egui_file_dialog::{Disk, Disks, FileDialog, FileSystem}; +use egui_file_dialog::{Disk, Disks, FileDialog, FileDialogConfig, FileSystem}; use std::{ path::{Component, Path, PathBuf}, sync::Arc, @@ -37,8 +37,11 @@ impl MyApp { )]), ), ]; + Self { - file_dialog: FileDialog::from_filesystem(Arc::new(MyFileSystem(root))), + file_dialog: FileDialog::with_config(FileDialogConfig::default_from_filesystem( + Arc::new(MyFileSystem(root)), + )), picked_file: None, } } @@ -134,9 +137,12 @@ impl FileSystem for MyFileSystem { } fn get_disks(&self, canonicalize_paths: bool) -> egui_file_dialog::Disks { - Disks::new(vec![ - Disk::new(Some("I'm a fake disk"), &PathBuf::from("/disk"), false, true), - ]) + Disks::new(vec![Disk::new( + Some("I'm a fake disk"), + &PathBuf::from("/disk"), + false, + true, + )]) } fn user_dirs(&self, canonicalize_paths: bool) -> Option { From 17fa6d231bad2194eb2878683a85752c2e158959 Mon Sep 17 00:00:00 2001 From: Masterchef365 Date: Fri, 3 Jan 2025 20:17:55 -0800 Subject: [PATCH 27/29] Make canonicalize internal --- src/data/user_directories.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/user_directories.rs b/src/data/user_directories.rs index c3da63e6..7dfa1ada 100644 --- a/src/data/user_directories.rs +++ b/src/data/user_directories.rs @@ -65,7 +65,7 @@ impl UserDirectories { } /// Canonicalizes the given paths. Returns None if an error occurred. - pub fn canonicalize(path: Option<&Path>, canonicalize: bool) -> Option { + pub(crate) fn canonicalize(path: Option<&Path>, canonicalize: bool) -> Option { if !canonicalize { return path.map(PathBuf::from); } From a377e7998221116a97efc152b570820996f52ff4 Mon Sep 17 00:00:00 2001 From: Duncan Freeman Date: Wed, 8 Jan 2025 12:58:23 -0800 Subject: [PATCH 28/29] Fix is_path_hidden on Windows --- src/file_system.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/file_system.rs b/src/file_system.rs index a25b3119..3f87404c 100644 --- a/src/file_system.rs +++ b/src/file_system.rs @@ -151,7 +151,7 @@ impl FileSystem for NativeFileSystem { } #[cfg(windows)] -fn is_path_hidden(item: &Path) -> bool { +fn is_path_hidden(path: &Path) -> bool { use std::os::windows::fs::MetadataExt; std::fs::metadata(path).map_or(false, |metadata| metadata.file_attributes() & 0x2 > 0) From a569fe63c623d48063d866fa314148b84d51e3c7 Mon Sep 17 00:00:00 2001 From: Duncan Freeman Date: Wed, 8 Jan 2025 13:07:46 -0800 Subject: [PATCH 29/29] Add a with_file_system() builder for FileDialog & use it in example --- examples/custom_filesystem.rs | 6 +++--- src/file_dialog.rs | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/examples/custom_filesystem.rs b/examples/custom_filesystem.rs index e1870dfd..1a429551 100644 --- a/examples/custom_filesystem.rs +++ b/examples/custom_filesystem.rs @@ -39,9 +39,9 @@ impl MyApp { ]; Self { - file_dialog: FileDialog::with_config(FileDialogConfig::default_from_filesystem( + file_dialog: FileDialog::new().with_file_system( Arc::new(MyFileSystem(root)), - )), + ), picked_file: None, } } @@ -157,7 +157,7 @@ impl FileSystem for MyFileSystem { } fn current_dir(&self) -> std::io::Result { - Ok("".into()) + Ok("folder_a".into()) } fn is_path_hidden(&self, path: &Path) -> bool { diff --git a/src/file_dialog.rs b/src/file_dialog.rs index d1f3489f..17ad4046 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -234,6 +234,14 @@ impl FileDialog { } } + /// Uses the given file system instead of the native file system. + #[must_use] + pub fn with_file_system(mut self, file_system: Arc) -> Self { + self.config.initial_directory = file_system.current_dir().unwrap_or_default(); + self.config.file_system = file_system; + self + } + /// Creates a new file dialog object and initializes it with the specified configuration. pub fn with_config(config: FileDialogConfig) -> Self { let mut obj = Self::new();