Skip to content

Commit

Permalink
Implement non blocking dir loading (fluxxcode#177)
Browse files Browse the repository at this point in the history
* Implement non blocking direcotry loading

* Update changelog
  • Loading branch information
fluxxcode authored Oct 26, 2024
1 parent 0c585c1 commit 89784ca
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 86 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Updated sysinfo to version `0.32` [#161](https://github.com/fluxxcode/egui-file-dialog/pull/161)
- Made default egui fonts an optional feature `default_fonts` [#163](https://github.com/fluxxcode/egui-file-dialog/pull/163) (thanks [@StarStarJ](https://github.com/StarStarJ)!)
- Filter directory when loading to improve performance [#169](https://github.com/fluxxcode/egui-file-dialog/pull/169)
- Implement non blocking directory loading [#177](https://github.com/fluxxcode/egui-file-dialog/pull/177)

### 📚 Documentation
- Updated `README.md` to include latest features [#176](https://github.com/fluxxcode/egui-file-dialog/pull/176)
Expand Down
5 changes: 5 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ pub struct FileDialogConfig {
pub directory_separator: String,
/// If the paths in the file dialog should be canonicalized before use.
pub canonicalize_paths: bool,
/// If the directory content should be loaded via a separate thread.
/// This prevents the application from blocking when loading large directories
/// or from slow hard drives.
pub load_via_thread: bool,

/// The icon that is used to display error messages.
pub err_icon: String,
Expand Down Expand Up @@ -213,6 +217,7 @@ impl Default for FileDialogConfig {
allow_path_edit_to_save_file_without_extension: false,
directory_separator: String::from(">"),
canonicalize_paths: true,
load_via_thread: true,

err_icon: String::from("⚠"),
warn_icon: String::from("⚠"),
Expand Down
195 changes: 178 additions & 17 deletions src/data/directory_content.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use std::path::{Path, PathBuf};
use std::{fs, io};
use std::sync::{mpsc, Arc};
use std::time::SystemTime;
use std::{fs, io, thread};

use egui::mutex::Mutex;

use crate::config::{FileDialogConfig, FileFilter};

Expand Down Expand Up @@ -110,18 +114,63 @@ impl DirectoryEntry {
}
}

/// Contains the state of the directory content.
#[derive(Debug, PartialEq, Eq)]
pub enum DirectoryContentState {
/// If we are currently waiting for the loading process on another thread.
/// The value is the timestamp when the loading process started.
Pending(SystemTime),
/// If loading the direcotry content finished since the last update call.
/// This is only returned once.
Finished,
/// If loading the directory content was successfull.
Success,
/// If there was an error loading the directory content.
/// The value contains the error message.
Errored(String),
}

type DirectoryContentReceiver =
Option<Arc<Mutex<mpsc::Receiver<Result<Vec<DirectoryEntry>, std::io::Error>>>>>;

/// Contains the content of a directory.
#[derive(Default, Debug)]
pub struct DirectoryContent {
/// Current state of the directory content.
state: DirectoryContentState,
/// The loaded directory contents.
content: Vec<DirectoryEntry>,
/// Receiver when the content is loaded on a different thread.
content_recv: DirectoryContentReceiver,
}

impl DirectoryContent {
/// Create a new object with empty content
pub const fn new() -> Self {
Self { content: vec![] }
impl Default for DirectoryContent {
fn default() -> Self {
Self {
state: DirectoryContentState::Success,
content: Vec::new(),
content_recv: None,
}
}
}

impl std::fmt::Debug for DirectoryContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DirectoryContent")
.field("state", &self.state)
.field("content", &self.content)
.field(
"content_recv",
if self.content_recv.is_some() {
&"<Receiver>"
} else {
&"None"
},
)
.finish()
}
}

impl DirectoryContent {
/// Create a new `DirectoryContent` object and loads the contents of the given path.
/// Use `include_files` to include or exclude files in the content list.
pub fn from_path(
Expand All @@ -131,17 +180,133 @@ impl DirectoryContent {
show_hidden: bool,
show_system_files: bool,
file_filter: Option<&FileFilter>,
) -> io::Result<Self> {
Ok(Self {
content: load_directory(
) -> Self {
if config.load_via_thread {
Self::with_thread(
config,
path,
include_files,
show_hidden,
show_system_files,
file_filter,
)?,
})
)
} else {
Self::without_thread(
config,
path,
include_files,
show_hidden,
show_system_files,
file_filter,
)
}
}

fn with_thread(
config: &FileDialogConfig,
path: &Path,
include_files: bool,
show_hidden: bool,
show_system_files: bool,
file_filter: Option<&FileFilter>,
) -> Self {
let (tx, rx) = mpsc::channel();

let c = config.clone();
let p = path.to_path_buf();
let f = file_filter.cloned();
thread::spawn(move || {
let _ = tx.send(load_directory(
&c,
&p,
include_files,
show_hidden,
show_system_files,
f.as_ref(),
));
});

Self {
state: DirectoryContentState::Pending(SystemTime::now()),
content: Vec::new(),
content_recv: Some(Arc::new(Mutex::new(rx))),
}
}

fn without_thread(
config: &FileDialogConfig,
path: &Path,
include_files: bool,
show_hidden: bool,
show_system_files: bool,
file_filter: Option<&FileFilter>,
) -> Self {
match load_directory(
config,
path,
include_files,
show_hidden,
show_system_files,
file_filter,
) {
Ok(c) => Self {
state: DirectoryContentState::Success,
content: c,
content_recv: None,
},
Err(err) => Self {
state: DirectoryContentState::Errored(err.to_string()),
content: Vec::new(),
content_recv: None,
},
}
}

pub fn update(&mut self) -> &DirectoryContentState {
if self.state == DirectoryContentState::Finished {
self.state = DirectoryContentState::Success;
}

if !matches!(self.state, DirectoryContentState::Pending(_)) {
return &self.state;
}

self.update_pending_state()
}

fn update_pending_state(&mut self) -> &DirectoryContentState {
let rx = std::mem::take(&mut self.content_recv);
let mut update_content_recv = true;

if let Some(recv) = &rx {
let value = recv.lock().try_recv();
match value {
Ok(result) => match result {
Ok(content) => {
self.state = DirectoryContentState::Finished;
self.content = content;
update_content_recv = false;
}
Err(err) => {
self.state = DirectoryContentState::Errored(err.to_string());
update_content_recv = false;
}
},
Err(err) => {
if mpsc::TryRecvError::Disconnected == err {
self.state =
DirectoryContentState::Errored("thread ended unexpectedly".to_owned());
update_content_recv = false;
}
}
}
}

if update_content_recv {
self.content_recv = rx;
}

&self.state
}

pub fn filtered_iter<'s>(
Expand Down Expand Up @@ -177,11 +342,6 @@ impl DirectoryContent {
pub fn push(&mut self, item: DirectoryEntry) {
self.content.push(item);
}

/// Clears the items inside the directory.
pub fn clear(&mut self) {
self.content.clear();
}
}

fn apply_search_value(entry: &DirectoryEntry, value: &str) -> bool {
Expand Down Expand Up @@ -266,7 +426,8 @@ fn is_path_hidden(item: &DirectoryEntry) -> bool {
}

/// Generates the icon for the specific path.
/// The default icon configuration is taken into account, as well as any configured file icon filters.
/// 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 {
for def in &config.file_icon_filters {
if (def.filter)(path) {
Expand Down
2 changes: 1 addition & 1 deletion src/data/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod directory_content;
pub use directory_content::{DirectoryContent, DirectoryEntry};
pub use directory_content::{DirectoryContent, DirectoryContentState, DirectoryEntry};

mod disks;
pub use disks::{Disk, Disks};
Expand Down
Loading

0 comments on commit 89784ca

Please sign in to comment.