diff --git a/fyrox-ui/.gitignore b/fyrox-ui/.gitignore index 23c7104a9..ce51e54b4 100644 --- a/fyrox-ui/.gitignore +++ b/fyrox-ui/.gitignore @@ -1 +1,3 @@ -/test_dir_fetching \ No newline at end of file +/test_dir_fetching +/test_fs_tree_with_root +/test_fs_tree_without_root \ No newline at end of file diff --git a/fyrox-ui/src/file_browser/fs_tree.rs b/fyrox-ui/src/file_browser/fs_tree.rs index 33f37fc30..1c86be0a5 100644 --- a/fyrox-ui/src/file_browser/fs_tree.rs +++ b/fyrox-ui/src/file_browser/fs_tree.rs @@ -81,6 +81,7 @@ pub fn build_tree_item>( ctx: &mut BuildContext, root_title: Option<&str>, ) -> Handle { + dbg!(path.as_ref()); let content = GridBuilder::new( WidgetBuilder::new() .with_child(if path.as_ref().is_dir() { @@ -185,35 +186,32 @@ fn disk_letter(components: &[Component]) -> Option { None } -fn sanitize_path(in_path: &Path, root: Option<&PathBuf>) -> PathBuf { - let mut out_path = PathBuf::new(); - - if let Ok(canonical_in_path) = in_path.canonicalize() { - if let Some(canonical_root) = root.and_then(|r| r.canonicalize().ok()) { - if let Ok(stripped) = canonical_in_path.strip_prefix(canonical_root) { - stripped.clone_into(&mut out_path); - } - } else { - out_path = canonical_in_path; - } - } - - // There should be at least one component in the path. If the path is empty, this means that - // it "points" to the current directory. - if out_path.as_os_str().is_empty() { - out_path.push("."); - } - - // Relative paths must always start from CurDir component (./), otherwise the root dir will be ignored - // and the tree will be incorrect. - if !out_path.is_absolute() { - out_path = Path::new(".").join(out_path); - } - - out_path +struct SanitizedPath { + path: PathBuf, + root_components_to_skip: usize, } -fn read_dir_entries(dir: &Path, filter: &PathFilter) -> std::io::Result> { +impl SanitizedPath { + fn new(path: &Path, root: Option<&PathBuf>) -> std::io::Result { + let path = path.canonicalize()?; + if let Some(root) = root { + let canonical_root = root.canonicalize()?; + let root_components_to_skip = canonical_root.components().count().saturating_sub(1); + dbg!(&path, &canonical_root, root_components_to_skip); + Ok(Self { + path, + root_components_to_skip, + }) + } else { + Ok(Self { + path, + root_components_to_skip: 0, + }) + } + } +} + +pub(super) fn read_dir_entries(dir: &Path, filter: &PathFilter) -> std::io::Result> { #[allow(clippy::ptr_arg)] fn sort_dir_entries(a: &PathBuf, b: &PathBuf) -> Ordering { let a_is_dir = a.is_dir(); @@ -365,24 +363,38 @@ pub fn build_single_folder( } } +pub fn tree_path(tree_handle: Handle, ui: &UserInterface) -> Option { + ui.node(tree_handle).user_data_cloned::() +} + pub struct FsTree { pub root_items: Vec>, pub path_item: Handle, } impl FsTree { + fn empty() -> Self { + Self { + root_items: Default::default(), + path_item: Default::default(), + } + } + /// Builds entire file system tree to given final_path. pub fn new( root: Option<&PathBuf>, - final_path: &Path, + path: &Path, filter: &PathFilter, menu: RcUiNodeHandle, root_title: Option<&str>, ctx: &mut BuildContext, - ) -> Self { - let dest_path = sanitize_path(final_path, root); + ) -> std::io::Result { + let SanitizedPath { + path, + root_components_to_skip, + } = SanitizedPath::new(path, root)?; - let dest_path_components = dest_path.components().collect::>(); + let dest_path_components = path.components().collect::>(); let RootsCollection { items: mut root_items, @@ -397,6 +409,10 @@ impl FsTree { // Concat parts of path one by one. full_path = full_path.join(component.as_os_str()); + if i < root_components_to_skip { + continue; + } + let next_component = dest_path_components.get(i + 1); if let Some(next_component) = next_component { @@ -410,12 +426,12 @@ impl FsTree { let next = next_component.map(|p| full_path.join(p)); let mut new_parent = parent; - if let Ok(entries) = read_dir_entries(&full_path, filter) { - for path in entries { - let is_part_of_final_path = next.as_ref().is_some_and(|next| *next == path); + if let Ok(dir_entries) = read_dir_entries(&full_path, filter) { + for dir_path in dir_entries { + let is_part_of_final_path = next.as_ref().is_some_and(|next| *next == dir_path); let item = build_tree_item( - &path, + &dir_path, &full_path, menu.clone(), is_part_of_final_path, @@ -433,7 +449,7 @@ impl FsTree { new_parent = item; } - if path == dest_path { + if dir_path == path { path_item = item; } } @@ -441,68 +457,30 @@ impl FsTree { parent = new_parent; } - Self { + Ok(Self { root_items, path_item, + }) + } + + pub fn new_or_empty( + root: Option<&PathBuf>, + path: &Path, + filter: &PathFilter, + menu: RcUiNodeHandle, + root_title: Option<&str>, + ctx: &mut BuildContext, + ) -> Self { + match Self::new(root, path, filter, menu, root_title, ctx) { + Ok(fs_tree) => fs_tree, + Err(err) => { + err!( + "Unable to rebuild FS tree for {} path! Reason: {}", + path.display(), + err + ); + Self::empty() + } } } } - -#[cfg(test)] -mod tests { - use crate::file_browser::{fs_tree::read_dir_entries, PathFilter}; - use std::{ - fs::File, - path::{Path, PathBuf}, - }; - - fn create_dir(path: impl AsRef) { - std::fs::create_dir_all(path).unwrap() - } - - fn write_empty_file(path: impl AsRef) { - File::create(path).unwrap(); - } - - fn create_dirs(paths: &[PathBuf]) { - for path in paths { - create_dir(path) - } - } - - fn write_empty_files(paths: &[PathBuf]) { - for path in paths { - write_empty_file(path) - } - } - - fn clean_or_create(path: impl AsRef) { - if path.as_ref().exists() { - std::fs::remove_dir_all(path).unwrap(); - } else { - create_dir(path); - } - } - - #[test] - fn test_dir_fetching() { - let path = Path::new("./test_dir_fetching"); - clean_or_create(path); - let folders = [path.join("dir1"), path.join("dir2"), path.join("dir3")]; - let files = [path.join("file1"), path.join("file2"), path.join("file3")]; - create_dirs(&folders); - write_empty_files(&files); - let entries = read_dir_entries(path, &PathFilter::AllPass).unwrap(); - assert_eq!(entries[0..3], folders); - assert_eq!(entries[3..6], files); - let files_copy = files.clone(); - let folders_copy = folders.clone(); - let entries = read_dir_entries( - path, - &PathFilter::new(move |path| path != files_copy[0] && path != folders_copy[2]), - ) - .unwrap(); - assert_eq!(entries[0..2], folders[0..2]); - assert_eq!(entries[2..4], files[1..3]); - } -} diff --git a/fyrox-ui/src/file_browser/mod.rs b/fyrox-ui/src/file_browser/mod.rs index bd4c25a44..633c8652e 100644 --- a/fyrox-ui/src/file_browser/mod.rs +++ b/fyrox-ui/src/file_browser/mod.rs @@ -59,6 +59,9 @@ mod fs_tree; mod menu; mod selector; +#[cfg(test)] +mod test; + pub use filter::*; pub use selector::*; @@ -175,7 +178,7 @@ impl FileBrowser { } fn rebuild_fs_tree(&mut self, ui: &mut UserInterface) { - let fs_tree = fs_tree::FsTree::new( + let fs_tree = fs_tree::FsTree::new_or_empty( self.root.as_ref(), &self.path, &self.filter, @@ -362,19 +365,16 @@ impl Control for FileBrowser { } else if let Some(TreeMessage::Expand { expand, .. }) = message.data::() { if *expand { // Look into internals of directory and build tree items. - let parent_path = ui - .node(message.destination()) - .user_data_cloned::() - .unwrap() - .clone(); - fs_tree::build_single_folder( - &parent_path, - message.destination(), - self.item_context_menu.clone(), - self.root_title.as_deref(), - &self.filter, - ui, - ) + if let Some(parent_path) = fs_tree::tree_path(message.destination(), ui) { + fs_tree::build_single_folder( + &parent_path, + message.destination(), + self.item_context_menu.clone(), + self.root_title.as_deref(), + &self.filter, + ui, + ) + } } else { // Nuke everything in collapsed item. This also will free some resources // and will speed up layout pass. @@ -388,60 +388,48 @@ impl Control for FileBrowser { } } else if let Some(WidgetMessage::Drop(dropped)) = message.data() { if !message.handled() { - if let Some(path) = ui.node(message.destination()).user_data_cloned::() { + if let Some(path) = fs_tree::tree_path(message.destination(), ui) { ui.post( self.handle, FileBrowserMessage::Drop { dropped: *dropped, path_item: message.destination(), path: path.clone(), - dropped_path: ui - .node(*dropped) - .user_data_cloned::() - .unwrap_or_default(), + dropped_path: fs_tree::tree_path(*dropped, ui).unwrap_or_default(), }, ); message.set_handled(true); } } - } else if let Some(TreeRootMessage::Select(selection)) = message.data::() { - if message.destination() == self.tree_root - && message.direction() == MessageDirection::FromWidget - { - if let Some(&first_selected) = selection.first() { - if let Some(first_selected_ref) = ui.try_get_node(first_selected) { - let mut path = first_selected_ref - .user_data_cloned::() - .unwrap() - .clone(); - - if let FileBrowserMode::Save { .. } = self.mode { - if path.is_file() { - ui.send( - self.file_name, - TextMessage::Text( - path.file_name() - .map(|f| f.to_string_lossy().to_string()) - .unwrap_or_default(), - ), - ); - } else { - path = path.join(&self.file_name_value); - } - } - - if self.path != path { - self.path.clone_from(&path); - + } else if let Some(TreeRootMessage::Select(selection)) = message.data_from(self.tree_root) { + if let Some(&first_selected) = selection.first() { + if let Some(mut path) = fs_tree::tree_path(first_selected, ui) { + if let FileBrowserMode::Save { .. } = self.mode { + if path.is_file() { ui.send( - self.path_text, - TextMessage::Text(path.to_string_lossy().to_string()), + self.file_name, + TextMessage::Text( + path.file_name() + .map(|f| f.to_string_lossy().to_string()) + .unwrap_or_default(), + ), ); - - // Do response. - ui.post(self.handle, FileBrowserMessage::Path(path)); + } else { + path = path.join(&self.file_name_value); } } + + if self.path != path { + self.path.clone_from(&path); + + ui.send( + self.path_text, + TextMessage::Text(path.to_string_lossy().to_string()), + ); + + // Do response. + ui.post(self.handle, FileBrowserMessage::Path(path)); + } } } } else if let Some(ButtonMessage::Click) = message.data() { @@ -579,7 +567,7 @@ impl FileBrowserBuilder { let fs_tree::FsTree { root_items: items, .. - } = fs_tree::FsTree::new( + } = fs_tree::FsTree::new_or_empty( self.root.as_ref(), self.path.as_path(), &self.filter, @@ -791,54 +779,3 @@ fn setup_filebrowser_fs_watcher( Err(_) => None, } } - -#[cfg(test)] -mod test { - use crate::file_browser::{fs_tree, FileBrowserBuilder}; - use crate::test::test_widget_deletion; - use crate::{ - core::pool::Handle, tree::TreeRootBuilder, widget::WidgetBuilder, RcUiNodeHandle, - UserInterface, - }; - use fyrox_core::algebra::Vector2; - use fyrox_core::parking_lot::Mutex; - use std::path::PathBuf; - use std::sync::Arc; - - #[test] - fn test_deletion() { - test_widget_deletion(|ctx| FileBrowserBuilder::new(WidgetBuilder::new()).build(ctx)); - } - - #[test] - fn test_find_tree() { - let mut ui = UserInterface::new(Vector2::new(100.0, 100.0)); - - let root = TreeRootBuilder::new( - WidgetBuilder::new().with_user_data(Arc::new(Mutex::new(PathBuf::from("test")))), - ) - .build(&mut ui.build_ctx()); - - let path = fs_tree::build_tree( - root, - true, - "./test/path1", - "./test", - RcUiNodeHandle::new(Handle::new(0, 1), ui.sender()), - None, - &mut ui, - ); - - while ui.poll_message().is_some() {} - - // This passes. - assert_eq!(fs_tree::find_tree_item(root, &"./test/path1", &ui), path); - - // This expected to fail - // https://github.com/rust-lang/rust/issues/31374 - assert_eq!( - fs_tree::find_tree_item(root, &"test/path1", &ui), - Handle::NONE - ); - } -} diff --git a/fyrox-ui/src/file_browser/test.rs b/fyrox-ui/src/file_browser/test.rs new file mode 100644 index 000000000..febf0eb5b --- /dev/null +++ b/fyrox-ui/src/file_browser/test.rs @@ -0,0 +1,189 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use crate::test::UserInterfaceTestingExtension; +use crate::{ + core::{algebra::Vector2, parking_lot::Mutex, pool::Handle}, + file_browser::{fs_tree, fs_tree::read_dir_entries, FileBrowserBuilder, PathFilter}, + test::test_widget_deletion, + tree::TreeRootBuilder, + widget::WidgetBuilder, + RcUiNodeHandle, UiNode, UserInterface, +}; +use fyrox_graph::SceneGraph; +use std::{ + fs::File, + path::{Path, PathBuf}, + sync::Arc, +}; + +fn create_dir(path: impl AsRef) { + std::fs::create_dir_all(path).unwrap() +} + +fn write_empty_file(path: impl AsRef) { + if let Some(parent) = path.as_ref().parent() { + if !parent.exists() { + create_dir(parent) + } + } + File::create(path).unwrap(); +} + +fn create_dirs(paths: &[PathBuf]) { + for path in paths { + create_dir(path) + } +} + +fn write_empty_files(paths: &[PathBuf]) { + for path in paths { + write_empty_file(path) + } +} + +fn clean_or_create(path: impl AsRef) { + if path.as_ref().exists() { + std::fs::remove_dir_all(path).unwrap(); + } else { + create_dir(path); + } +} + +fn find_by_path(path: impl AsRef, ui: &UserInterface) -> Handle { + ui.find_from_root(&mut |n| { + n.user_data_cloned::() + .is_some_and(|p| p == path.as_ref()) + }) + .map(|(h, _)| h) + .unwrap_or_default() +} + +#[test] +fn test_deletion() { + test_widget_deletion(|ctx| FileBrowserBuilder::new(WidgetBuilder::new()).build(ctx)); +} + +#[test] +fn test_find_tree() { + let mut ui = UserInterface::new(Vector2::new(100.0, 100.0)); + + let root = TreeRootBuilder::new( + WidgetBuilder::new().with_user_data(Arc::new(Mutex::new(PathBuf::from("test")))), + ) + .build(&mut ui.build_ctx()); + + let path = fs_tree::build_tree( + root, + true, + "./test/path1", + "./test", + RcUiNodeHandle::new(Handle::new(0, 1), ui.sender()), + None, + &mut ui, + ); + + while ui.poll_message().is_some() {} + + // This passes. + assert_eq!(fs_tree::find_tree_item(root, &"./test/path1", &ui), path); + + // This expected to fail + // https://github.com/rust-lang/rust/issues/31374 + assert_eq!( + fs_tree::find_tree_item(root, &"test/path1", &ui), + Handle::NONE + ); +} + +#[test] +fn test_dir_fetching() { + let path = Path::new("./test_dir_fetching"); + clean_or_create(path); + let folders = [path.join("dir1"), path.join("dir2"), path.join("dir3")]; + let files = [path.join("file1"), path.join("file2"), path.join("file3")]; + create_dirs(&folders); + write_empty_files(&files); + let entries = read_dir_entries(path, &PathFilter::AllPass).unwrap(); + assert_eq!(entries[0..3], folders); + assert_eq!(entries[3..6], files); + let files_copy = files.clone(); + let folders_copy = folders.clone(); + let entries = read_dir_entries( + path, + &PathFilter::new(move |path| path != files_copy[0] && path != folders_copy[2]), + ) + .unwrap(); + assert_eq!(entries[0..2], folders[0..2]); + assert_eq!(entries[2..4], files[1..3]); +} + +#[test] +fn test_fs_tree_with_root() { + let root = PathBuf::from("./test_fs_tree_with_root"); + clean_or_create(&root); + let paths = [ + root.join("file1"), + root.join("file2"), + root.join("file3"), + root.join("subdir").join("file1"), + root.join("subdir").join("file2"), + root.join("subdir").join("file3"), + ]; + write_empty_files(&paths); + let screen_size = Vector2::repeat(1000.0); + let mut ui = UserInterface::new(screen_size); + let ctx = &mut ui.build_ctx(); + FileBrowserBuilder::new(WidgetBuilder::new()) + .with_root(root) + .with_path(paths.last().as_deref().unwrap()) + .build(ctx); + ui.poll_all_messages(); + for path in paths { + let path = path.canonicalize().unwrap(); + assert!(find_by_path(path, &ui).is_some()); + } +} + +#[test] +fn test_fs_tree_without_root() { + let root = PathBuf::from("./test_fs_tree_without_root"); + clean_or_create(&root); + let paths = [ + root.join("file1"), + root.join("file2"), + root.join("file3"), + root.join("subdir").join("file1"), + root.join("subdir").join("file2"), + root.join("subdir").join("file3"), + ]; + write_empty_files(&paths); + let screen_size = Vector2::repeat(1000.0); + let mut ui = UserInterface::new(screen_size); + let ctx = &mut ui.build_ctx(); + FileBrowserBuilder::new(WidgetBuilder::new()) + .with_path(paths.last().as_deref().unwrap()) + .build(ctx); + ui.poll_all_messages(); + for path in paths { + let path = path.canonicalize().unwrap(); + assert!(find_by_path(path, &ui).is_some()); + } +}