Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filesystem abstraction #227

Open
wants to merge 29 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
94773a4
WIP
Masterchef365 Dec 21, 2024
816ce42
It compiles again!
Masterchef365 Dec 21, 2024
6711374
File preview
Masterchef365 Dec 21, 2024
d95823b
Disks and hidden paths
Masterchef365 Dec 21, 2024
2f32e81
Create dir dialog
Masterchef365 Dec 22, 2024
87d9574
User directories
Masterchef365 Dec 22, 2024
2d4c293
make more public
Masterchef365 Dec 22, 2024
e941b72
Don't try to use threading on wasm
Masterchef365 Dec 22, 2024
43bdb1a
Export disk
Masterchef365 Dec 22, 2024
14a63dc
Add a new constructor for file dialog
Masterchef365 Dec 22, 2024
1779ebc
Add current_dir to the filesystem abstraction
Masterchef365 Dec 22, 2024
34dc5b1
Icons use VFS
Masterchef365 Dec 22, 2024
0db3323
Find some more places to add vfs
Masterchef365 Dec 22, 2024
3b38026
Example using a custom filesystem
Masterchef365 Dec 22, 2024
2445a17
Extremely basic example of using the vfs
Masterchef365 Dec 22, 2024
ec946e2
Make Disk and Disks' members private
Masterchef365 Dec 24, 2024
b8721a2
Add new_native_disks instead of a random fn
Masterchef365 Dec 24, 2024
a6ba2a8
Constructor for UserDirectories
Masterchef365 Dec 24, 2024
765f93d
Construct for Metadata, rename vfs to file_system
Masterchef365 Dec 24, 2024
9729c20
Fix field accessors
Masterchef365 Dec 24, 2024
d379a65
Typo
Masterchef365 Dec 24, 2024
1e6f4e4
Default impl for load_text_file_preview
Masterchef365 Dec 27, 2024
c0cf868
Rename virtual_fs.rs to file_system.rs
Masterchef365 Jan 4, 2025
fc2c02e
Move filesystem to config
Masterchef365 Jan 4, 2025
735f250
Make some undocumented methods pub(crate) for now
Masterchef365 Jan 4, 2025
1c0c685
Adapt custom filesystem example code to use config API
Masterchef365 Jan 4, 2025
17fa6d2
Make canonicalize internal
Masterchef365 Jan 4, 2025
a377e79
Fix is_path_hidden on Windows
Jan 8, 2025
a569fe6
Add a with_file_system() builder for FileDialog & use it in example
Jan 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions examples/custom_filesystem.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use egui_file_dialog::{Disk, Disks, FileDialog, FileDialogConfig, FileSystem};
use std::{
path::{Component, Path, PathBuf},
sync::Arc,
};

use eframe::egui;

struct MyApp {
file_dialog: FileDialog,
picked_file: Option<PathBuf>,
}

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::new().with_file_system(
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<Vec<PathBuf>> {
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<egui_file_dialog::Metadata> {
Ok(Default::default())
}

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,
)])
}

fn user_dirs(&self, canonicalize_paths: bool) -> Option<egui_file_dialog::UserDirectories> {
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<PathBuf> {
Ok("folder_a".into())
}

fn is_path_hidden(&self, path: &Path) -> bool {
false
}

fn load_text_file_preview(&self, path: &Path, max_chars: usize) -> std::io::Result<String> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Unsupported".to_string(),
))
}
}
2 changes: 1 addition & 1 deletion examples/pick_file.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
20 changes: 18 additions & 2 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -65,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<dyn FileSystem + Send + Sync>,
/// Persistent data of the file dialog.
pub storage: FileDialogStorage,
/// The labels that the dialog uses.
Expand Down Expand Up @@ -206,22 +209,33 @@ pub struct FileDialogConfig {
}

impl Default for FileDialogConfig {
/// Creates a new configuration with default values
fn default() -> Self {
Self::default_from_filesystem(Arc::new(NativeFileSystem))
}
}

impl FileDialogConfig {
/// Creates a new configuration with default values
pub fn default_from_filesystem(file_system: Arc<dyn FileSystem + Send + Sync>) -> Self {
Self {
storage: FileDialogStorage::default(),
labels: FileDialogLabels::default(),
keybindings: FileDialogKeyBindings::default(),

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: 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,
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("⚠"),
Expand Down Expand Up @@ -269,6 +283,8 @@ impl Default for FileDialogConfig {
show_places: true,
show_devices: true,
show_removable_devices: true,

file_system,
}
}
}
Expand Down
11 changes: 7 additions & 4 deletions src/create_directory_dialog.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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.
Expand Down Expand Up @@ -47,11 +47,13 @@ pub struct CreateDirectoryDialog {
scroll_to_error: bool,
/// If the text input should request focus in the next frame
request_focus: bool,

file_system: Arc<dyn FileSystem + Send + Sync>,
}

impl CreateDirectoryDialog {
/// Creates a new dialog with default values
pub const fn new() -> Self {
pub fn from_filesystem(file_system: Arc<dyn FileSystem + Send + Sync>) -> Self {
Self {
open: false,
init: false,
Expand All @@ -61,6 +63,7 @@ impl CreateDirectoryDialog {
error: None,
scroll_to_error: false,
request_focus: true,
file_system,
}
}

Expand Down Expand Up @@ -177,7 +180,7 @@ impl CreateDirectoryDialog {
if let Some(mut dir) = self.directory.clone() {
dir.push(self.input.as_str());

match fs::create_dir(&dir) {
match self.file_system.create_dir(&dir) {
Ok(()) => {
self.close();
return CreateDirectoryResponse::new(dir.as_path());
Expand Down
Loading