1use crate::{
27 core::{
28 parking_lot::Mutex, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
29 uuid_provider, visitor::prelude::*,
30 },
31 define_constructor,
32 file_browser::menu::ItemContextMenu,
33 grid::{Column, GridBuilder, Row},
34 message::{MessageDirection, UiMessage},
35 scroll_viewer::{ScrollViewerBuilder, ScrollViewerMessage},
36 text::{TextBuilder, TextMessage},
37 text_box::{TextBoxBuilder, TextCommitMode},
38 tree::{Tree, TreeBuilder, TreeMessage, TreeRoot, TreeRootBuilder, TreeRootMessage},
39 widget::{Widget, WidgetBuilder, WidgetMessage},
40 BuildContext, Control, RcUiNodeHandle, Thickness, UiNode, UserInterface, VerticalAlignment,
41};
42use core::time;
43use fyrox_graph::{
44 constructor::{ConstructorProvider, GraphNodeConstructor},
45 BaseSceneGraph,
46};
47use notify::Watcher;
48use std::{
49 borrow::BorrowMut,
50 cmp::Ordering,
51 fmt::{Debug, Formatter},
52 fs::DirEntry,
53 ops::{Deref, DerefMut},
54 path::{Component, Path, PathBuf, Prefix},
55 sync::{
56 mpsc::{self, Receiver},
57 Arc,
58 },
59 thread,
60};
61#[cfg(not(target_arch = "wasm32"))]
62use sysinfo::{DiskExt, RefreshKind, SystemExt};
63
64mod menu;
65mod selector;
66
67use crate::button::{ButtonBuilder, ButtonMessage};
68use crate::utils::make_simple_tooltip;
69pub use selector::*;
70
71#[derive(Debug, Clone, PartialEq)]
72pub enum FileBrowserMessage {
73 Root(Option<PathBuf>),
74 Path(PathBuf),
75 Filter(Option<Filter>),
76 Add(PathBuf),
77 Remove(PathBuf),
78 FocusCurrentPath,
79 Rescan,
80 Drop {
81 dropped: Handle<UiNode>,
82 path_item: Handle<UiNode>,
83 path: PathBuf,
84 dropped_path: PathBuf,
86 },
87}
88
89impl FileBrowserMessage {
90 define_constructor!(FileBrowserMessage:Root => fn root(Option<PathBuf>), layout: false);
91 define_constructor!(FileBrowserMessage:Path => fn path(PathBuf), layout: false);
92 define_constructor!(FileBrowserMessage:Filter => fn filter(Option<Filter>), layout: false);
93 define_constructor!(FileBrowserMessage:Add => fn add(PathBuf), layout: false);
94 define_constructor!(FileBrowserMessage:Remove => fn remove(PathBuf), layout: false);
95 define_constructor!(FileBrowserMessage:Rescan => fn rescan(), layout: false);
96 define_constructor!(FileBrowserMessage:FocusCurrentPath => fn focus_current_path(), layout: false);
97 define_constructor!(FileBrowserMessage:Drop => fn drop(
98 dropped: Handle<UiNode>,
99 path_item: Handle<UiNode>,
100 path: PathBuf,
101 dropped_path: PathBuf),
102 layout: false
103 );
104}
105
106#[derive(Clone)]
107#[allow(clippy::type_complexity)]
108pub struct Filter(pub Arc<Mutex<dyn FnMut(&Path) -> bool + Send>>);
109
110impl Filter {
111 pub fn new<F: FnMut(&Path) -> bool + 'static + Send>(filter: F) -> Self {
112 Self(Arc::new(Mutex::new(filter)))
113 }
114}
115
116impl PartialEq for Filter {
117 fn eq(&self, other: &Self) -> bool {
118 std::ptr::eq(&*self.0, &*other.0)
119 }
120}
121
122impl Debug for Filter {
123 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
124 write!(f, "Filter")
125 }
126}
127
128#[derive(Default, Clone, PartialEq, Eq, Hash, Debug, Visit, Reflect)]
129pub enum FileBrowserMode {
130 #[default]
131 Open,
132 Save {
133 default_file_name: PathBuf,
134 },
135}
136
137#[derive(Default, Visit, Reflect, ComponentProvider)]
138pub struct FileBrowser {
139 pub widget: Widget,
140 pub tree_root: Handle<UiNode>,
141 pub home_dir: Handle<UiNode>,
142 pub desktop_dir: Handle<UiNode>,
143 pub path_text: Handle<UiNode>,
144 pub scroll_viewer: Handle<UiNode>,
145 pub path: PathBuf,
146 pub root: Option<PathBuf>,
147 #[visit(skip)]
148 #[reflect(hidden)]
149 pub filter: Option<Filter>,
150 pub mode: FileBrowserMode,
151 pub file_name: Handle<UiNode>,
152 pub file_name_value: PathBuf,
153 #[visit(skip)]
154 #[reflect(hidden)]
155 pub fs_receiver: Option<Receiver<notify::Event>>,
156 #[visit(skip)]
157 #[reflect(hidden)]
158 pub item_context_menu: RcUiNodeHandle,
159 #[allow(clippy::type_complexity)]
160 #[visit(skip)]
161 #[reflect(hidden)]
162 pub watcher: Option<(notify::RecommendedWatcher, thread::JoinHandle<()>)>,
163}
164
165impl ConstructorProvider<UiNode, UserInterface> for FileBrowser {
166 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
167 GraphNodeConstructor::new::<Self>()
168 .with_variant("File Browser", |ui| {
169 FileBrowserBuilder::new(WidgetBuilder::new().with_name("File Browser"))
170 .build(&mut ui.build_ctx())
171 .into()
172 })
173 .with_group("File System")
174 }
175}
176
177impl Clone for FileBrowser {
178 fn clone(&self) -> Self {
179 Self {
180 widget: self.widget.clone(),
181 tree_root: self.tree_root,
182 home_dir: self.home_dir,
183 desktop_dir: self.desktop_dir,
184 path_text: self.path_text,
185 scroll_viewer: self.scroll_viewer,
186 path: self.path.clone(),
187 root: self.root.clone(),
188 filter: self.filter.clone(),
189 mode: self.mode.clone(),
190 file_name: self.file_name,
191 file_name_value: self.file_name_value.clone(),
192 fs_receiver: None,
193 item_context_menu: self.item_context_menu.clone(),
194 watcher: None,
195 }
196 }
197}
198
199impl Debug for FileBrowser {
200 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
201 writeln!(f, "FileBrowser")
202 }
203}
204
205crate::define_widget_deref!(FileBrowser);
206
207impl FileBrowser {
208 fn rebuild_from_root(&mut self, ui: &mut UserInterface) {
209 let result = build_all(
211 self.root.as_ref(),
212 &self.path,
213 self.filter.clone(),
214 self.item_context_menu.clone(),
215 &mut ui.build_ctx(),
216 );
217
218 ui.send_message(TreeRootMessage::items(
220 self.tree_root,
221 MessageDirection::ToWidget,
222 result.root_items,
223 ));
224
225 if result.path_item.is_some() {
226 ui.send_message(TreeRootMessage::select(
228 self.tree_root,
229 MessageDirection::ToWidget,
230 vec![result.path_item],
231 ));
232 ui.send_message(ScrollViewerMessage::bring_into_view(
234 self.scroll_viewer,
235 MessageDirection::ToWidget,
236 result.path_item,
237 ));
238 } else {
239 ui.send_message(TextMessage::text(
241 self.path_text,
242 MessageDirection::ToWidget,
243 String::new(),
244 ));
245 }
246 }
247}
248
249uuid_provider!(FileBrowser = "b7f4610e-4b0c-4671-9b4a-60bb45268928");
250
251impl Control for FileBrowser {
252 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
253 self.widget.handle_routed_message(ui, message);
254
255 if let Some(msg) = message.data::<FileBrowserMessage>() {
256 if message.destination() == self.handle() {
257 match msg {
258 FileBrowserMessage::Path(path) => {
259 if message.direction() == MessageDirection::ToWidget && &self.path != path {
260 let existing_path = ignore_nonexistent_sub_dirs(path);
261
262 let mut item = find_tree(self.tree_root, &existing_path, ui);
263
264 if item.is_none() {
265 let result = build_all(
267 self.root.as_ref(),
268 &existing_path,
269 self.filter.clone(),
270 self.item_context_menu.clone(),
271 &mut ui.build_ctx(),
272 );
273
274 ui.send_message(TreeRootMessage::items(
276 self.tree_root,
277 MessageDirection::ToWidget,
278 result.root_items,
279 ));
280
281 item = result.path_item;
282 }
283
284 self.path.clone_from(path);
285
286 ui.send_message(TextMessage::text(
288 self.path_text,
289 MessageDirection::ToWidget,
290 path.to_string_lossy().to_string(),
291 ));
292
293 if item.is_some() {
295 ui.send_message(TreeRootMessage::select(
297 self.tree_root,
298 MessageDirection::ToWidget,
299 vec![item],
300 ));
301
302 ui.send_message(ScrollViewerMessage::bring_into_view(
304 self.scroll_viewer,
305 MessageDirection::ToWidget,
306 item,
307 ));
308 }
309
310 ui.send_message(message.reverse());
311 }
312 }
313 FileBrowserMessage::Root(root) => {
314 if &self.root != root {
315 let watcher_replacement = match self.watcher.take() {
316 Some((mut watcher, converter)) => {
317 let current_root = match &self.root {
318 Some(path) => path.clone(),
319 None => self.path.clone(),
320 };
321 if current_root.exists() {
322 let _ = watcher.unwatch(¤t_root);
323 }
324 let new_root = match &root {
325 Some(path) => path.clone(),
326 None => self.path.clone(),
327 };
328 let _ =
329 watcher.watch(&new_root, notify::RecursiveMode::Recursive);
330 Some((watcher, converter))
331 }
332 None => None,
333 };
334 self.root.clone_from(root);
335 self.path = root.clone().unwrap_or_default();
336 self.rebuild_from_root(ui);
337 self.watcher = watcher_replacement;
338 }
339 }
340 FileBrowserMessage::Filter(filter) => {
341 let equal = match (&self.filter, filter) {
342 (Some(current), Some(new)) => std::ptr::eq(new, current),
343 _ => false,
344 };
345 if !equal {
346 self.filter.clone_from(filter);
347 self.rebuild_from_root(ui);
348 }
349 }
350 FileBrowserMessage::Add(path) => {
351 let path =
352 make_fs_watcher_event_path_relative_to_tree_root(&self.root, path);
353 if filtered_out(&mut self.filter, &path) {
354 return;
355 }
356 let parent_path = parent_path(&path);
357 let existing_parent_node = find_tree(self.tree_root, &parent_path, ui);
358 if existing_parent_node.is_some() {
359 if let Some(tree) = ui.node(existing_parent_node).cast::<Tree>() {
360 if tree.is_expanded {
361 build_tree(
362 existing_parent_node,
363 existing_parent_node == self.tree_root,
364 path,
365 parent_path,
366 self.item_context_menu.clone(),
367 ui,
368 );
369 } else if !tree.always_show_expander {
370 ui.send_message(TreeMessage::set_expander_shown(
371 tree.handle(),
372 MessageDirection::ToWidget,
373 true,
374 ))
375 }
376 }
377 }
378 }
379 FileBrowserMessage::Remove(path) => {
380 let path =
381 make_fs_watcher_event_path_relative_to_tree_root(&self.root, path);
382 let node = find_tree(self.tree_root, &path, ui);
383 if node.is_some() {
384 let parent_path = parent_path(&path);
385 let parent_node = find_tree(self.tree_root, &parent_path, ui);
386 ui.send_message(TreeMessage::remove_item(
387 parent_node,
388 MessageDirection::ToWidget,
389 node,
390 ))
391 }
392 }
393 FileBrowserMessage::Rescan | FileBrowserMessage::Drop { .. } => (),
394 FileBrowserMessage::FocusCurrentPath => {
395 if let Ok(canonical_path) = self.path.canonicalize() {
396 let item = find_tree(self.tree_root, &canonical_path, ui);
397 if item.is_some() {
398 ui.send_message(TreeRootMessage::select(
400 self.tree_root,
401 MessageDirection::ToWidget,
402 vec![item],
403 ));
404 ui.send_message(ScrollViewerMessage::bring_into_view(
405 self.scroll_viewer,
406 MessageDirection::ToWidget,
407 item,
408 ));
409 }
410 }
411 }
412 }
413 }
414 } else if let Some(TextMessage::Text(txt)) = message.data::<TextMessage>() {
415 if message.direction() == MessageDirection::FromWidget {
416 if message.destination() == self.path_text {
417 self.path = txt.into();
418 } else if message.destination() == self.file_name {
419 self.file_name_value = txt.into();
420 ui.send_message(FileBrowserMessage::path(
421 self.handle,
422 MessageDirection::ToWidget,
423 {
424 let mut combined = self.path.clone();
425 combined.set_file_name(PathBuf::from(txt));
426 combined
427 },
428 ));
429 }
430 }
431 } else if let Some(TreeMessage::Expand { expand, .. }) = message.data::<TreeMessage>() {
432 if *expand {
433 let parent_path = ui
435 .node(message.destination())
436 .user_data_cloned::<PathBuf>()
437 .unwrap()
438 .clone();
439 if let Ok(dir_iter) = std::fs::read_dir(&parent_path) {
440 let mut entries: Vec<_> = dir_iter.flatten().collect();
441 entries.sort_unstable_by(sort_dir_entries);
442 for entry in entries {
443 let path = entry.path();
444 let build = if let Some(filter) = self.filter.as_mut() {
445 filter.0.borrow_mut().deref_mut().lock()(&path)
446 } else {
447 true
448 };
449 if build {
450 build_tree(
451 message.destination(),
452 false,
453 &path,
454 &parent_path,
455 self.item_context_menu.clone(),
456 ui,
457 );
458 }
459 }
460 }
461 } else {
462 ui.send_message(TreeMessage::set_items(
465 message.destination(),
466 MessageDirection::ToWidget,
467 vec![],
468 true,
469 ));
470 }
471 } else if let Some(WidgetMessage::Drop(dropped)) = message.data() {
472 if !message.handled() {
473 if let Some(path) = ui.node(message.destination()).user_data_cloned::<PathBuf>() {
474 ui.send_message(FileBrowserMessage::drop(
475 self.handle,
476 MessageDirection::FromWidget,
477 *dropped,
478 message.destination(),
479 path.clone(),
480 ui.node(*dropped)
481 .user_data_cloned::<PathBuf>()
482 .unwrap_or_default(),
483 ));
484
485 message.set_handled(true);
486 }
487 }
488 } else if let Some(TreeRootMessage::Selected(selection)) = message.data::<TreeRootMessage>()
489 {
490 if message.destination() == self.tree_root
491 && message.direction() == MessageDirection::FromWidget
492 {
493 if let Some(&first_selected) = selection.first() {
494 if let Some(first_selected_ref) = ui.try_get(first_selected) {
495 let mut path = first_selected_ref
496 .user_data_cloned::<PathBuf>()
497 .unwrap()
498 .clone();
499
500 if let FileBrowserMode::Save { .. } = self.mode {
501 if path.is_file() {
502 ui.send_message(TextMessage::text(
503 self.file_name,
504 MessageDirection::ToWidget,
505 path.file_name()
506 .map(|f| f.to_string_lossy().to_string())
507 .unwrap_or_default(),
508 ));
509 } else {
510 path = path.join(&self.file_name_value);
511 }
512 }
513
514 if self.path != path {
515 self.path.clone_from(&path);
516
517 ui.send_message(TextMessage::text(
518 self.path_text,
519 MessageDirection::ToWidget,
520 path.to_string_lossy().to_string(),
521 ));
522
523 ui.send_message(FileBrowserMessage::path(
525 self.handle,
526 MessageDirection::FromWidget,
527 path,
528 ));
529 }
530 }
531 }
532 }
533 } else if let Some(ButtonMessage::Click) = message.data() {
534 if message.direction() == MessageDirection::FromWidget {
535 #[cfg(not(target_arch = "wasm32"))]
536 if message.destination() == self.desktop_dir {
537 let user_dirs = directories::UserDirs::new();
538 if let Some(desktop_dir) =
539 user_dirs.as_ref().and_then(|dirs| dirs.desktop_dir())
540 {
541 ui.send_message(FileBrowserMessage::path(
542 self.handle,
543 MessageDirection::ToWidget,
544 desktop_dir.to_path_buf(),
545 ));
546 }
547 }
548
549 #[cfg(not(target_arch = "wasm32"))]
550 if message.destination() == self.home_dir {
551 let user_dirs = directories::UserDirs::new();
552 if let Some(home_dir) = user_dirs.as_ref().map(|dirs| dirs.home_dir()) {
553 ui.send_message(FileBrowserMessage::path(
554 self.handle,
555 MessageDirection::ToWidget,
556 home_dir.to_path_buf(),
557 ));
558 }
559 }
560 }
561 }
562 }
563
564 fn update(&mut self, _dt: f32, ui: &mut UserInterface) {
565 if let Ok(event) = self.fs_receiver.as_ref().unwrap().try_recv() {
566 if event.need_rescan() {
567 ui.send_message(FileBrowserMessage::rescan(
568 self.handle,
569 MessageDirection::ToWidget,
570 ));
571 } else {
572 for path in event.paths.iter() {
573 match event.kind {
574 notify::EventKind::Remove(_) => {
575 ui.send_message(FileBrowserMessage::remove(
576 self.handle,
577 MessageDirection::ToWidget,
578 path.clone(),
579 ));
580 }
581 notify::EventKind::Create(_) => {
582 ui.send_message(FileBrowserMessage::add(
583 self.handle,
584 MessageDirection::ToWidget,
585 path.clone(),
586 ));
587 }
588 _ => (),
589 }
590 }
591 }
592 }
593 }
594}
595
596fn parent_path(path: &Path) -> PathBuf {
597 let mut parent_path = path.to_owned();
598 parent_path.pop();
599 parent_path
600}
601
602fn filtered_out(filter: &mut Option<Filter>, path: &Path) -> bool {
603 match filter.as_mut() {
604 Some(filter) => !filter.0.borrow_mut().deref_mut().lock()(path),
605 None => false,
606 }
607}
608
609fn ignore_nonexistent_sub_dirs(path: &Path) -> PathBuf {
610 let mut existing_path = path.to_owned();
611 while !existing_path.exists() {
612 if !existing_path.pop() {
613 break;
614 }
615 }
616 existing_path
617}
618
619fn sort_dir_entries(a: &DirEntry, b: &DirEntry) -> Ordering {
620 let a_is_dir = a.path().is_dir();
621 let b_is_dir = b.path().is_dir();
622
623 if a_is_dir && !b_is_dir {
624 Ordering::Less
625 } else if !a_is_dir && b_is_dir {
626 Ordering::Greater
627 } else {
628 a.file_name()
629 .to_ascii_lowercase()
630 .cmp(&b.file_name().to_ascii_lowercase())
631 }
632}
633
634fn make_fs_watcher_event_path_relative_to_tree_root(
635 root: &Option<PathBuf>,
636 path: &Path,
637) -> PathBuf {
638 match root {
639 Some(ref root) => {
640 let remove_prefix = if *root == PathBuf::from(".") {
641 std::env::current_dir().unwrap()
642 } else {
643 root.clone()
644 };
645 PathBuf::from("./").join(path.strip_prefix(remove_prefix).unwrap_or(path))
646 }
647 None => path.to_owned(),
648 }
649}
650
651fn find_tree<P: AsRef<Path>>(node: Handle<UiNode>, path: &P, ui: &UserInterface) -> Handle<UiNode> {
652 let mut tree_handle = Handle::NONE;
653 let node_ref = ui.node(node);
654
655 if let Some(tree) = node_ref.cast::<Tree>() {
656 let tree_path = tree.user_data_cloned::<PathBuf>().unwrap();
657 if tree_path == path.as_ref() {
658 tree_handle = node;
659 } else {
660 for &item in &tree.items {
661 let tree = find_tree(item, path, ui);
662 if tree.is_some() {
663 tree_handle = tree;
664 break;
665 }
666 }
667 }
668 } else if let Some(root) = node_ref.cast::<TreeRoot>() {
669 for &item in &root.items {
670 let tree = find_tree(item, path, ui);
671 if tree.is_some() {
672 tree_handle = tree;
673 break;
674 }
675 }
676 } else {
677 unreachable!()
678 }
679 tree_handle
680}
681
682fn build_tree_item<P: AsRef<Path>>(
683 path: P,
684 parent_path: P,
685 menu: RcUiNodeHandle,
686 expanded: bool,
687 ctx: &mut BuildContext,
688) -> Handle<UiNode> {
689 let is_dir_empty = path
690 .as_ref()
691 .read_dir()
692 .map_or(true, |mut f| f.next().is_none());
693 TreeBuilder::new(
694 WidgetBuilder::new()
695 .with_user_data(Arc::new(Mutex::new(path.as_ref().to_owned())))
696 .with_context_menu(menu),
697 )
698 .with_expanded(expanded)
699 .with_always_show_expander(!is_dir_empty)
700 .with_content(
701 TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::left(4.0)))
702 .with_text(
703 path.as_ref()
704 .to_string_lossy()
705 .replace(&parent_path.as_ref().to_string_lossy().to_string(), "")
706 .replace('\\', ""),
707 )
708 .with_vertical_text_alignment(VerticalAlignment::Center)
709 .build(ctx),
710 )
711 .build(ctx)
712}
713
714fn build_tree<P: AsRef<Path>>(
715 parent: Handle<UiNode>,
716 is_parent_root: bool,
717 path: P,
718 parent_path: P,
719 menu: RcUiNodeHandle,
720 ui: &mut UserInterface,
721) -> Handle<UiNode> {
722 let subtree = build_tree_item(path, parent_path, menu, false, &mut ui.build_ctx());
723 insert_subtree_in_parent(ui, parent, is_parent_root, subtree);
724 subtree
725}
726
727fn insert_subtree_in_parent(
728 ui: &mut UserInterface,
729 parent: Handle<UiNode>,
730 is_parent_root: bool,
731 tree: Handle<UiNode>,
732) {
733 if is_parent_root {
734 ui.send_message(TreeRootMessage::add_item(
735 parent,
736 MessageDirection::ToWidget,
737 tree,
738 ));
739 } else {
740 ui.send_message(TreeMessage::add_item(
741 parent,
742 MessageDirection::ToWidget,
743 tree,
744 ));
745 }
746}
747
748struct BuildResult {
749 root_items: Vec<Handle<UiNode>>,
750 path_item: Handle<UiNode>,
751}
752
753fn build_all(
755 root: Option<&PathBuf>,
756 final_path: &Path,
757 mut filter: Option<Filter>,
758 menu: RcUiNodeHandle,
759 ctx: &mut BuildContext,
760) -> BuildResult {
761 let mut dest_path = PathBuf::new();
762 if let Ok(canonical_final_path) = final_path.canonicalize() {
763 if let Some(canonical_root) = root.and_then(|r| r.canonicalize().ok()) {
764 if let Ok(stripped) = canonical_final_path.strip_prefix(canonical_root) {
765 stripped.clone_into(&mut dest_path);
766 }
767 } else {
768 dest_path = canonical_final_path;
769 }
770 }
771
772 if dest_path.as_os_str().is_empty() {
775 dest_path.push(".");
776 }
777
778 if !dest_path.is_absolute() {
781 dest_path = Path::new(".").join(dest_path);
782 }
783
784 let dest_path_components = dest_path.components().collect::<Vec<Component>>();
785 #[allow(unused_variables)]
786 let dest_disk = dest_path_components.first().and_then(|c| {
787 if let Component::Prefix(prefix) = c {
788 if let Prefix::Disk(disk_letter) | Prefix::VerbatimDisk(disk_letter) = prefix.kind() {
789 Some(disk_letter)
790 } else {
791 None
792 }
793 } else {
794 None
795 }
796 });
797
798 let mut root_items = Vec::new();
799 let mut parent = if let Some(root) = root {
800 let path = if std::env::current_dir().is_ok_and(|dir| &dir == root) {
801 Path::new(".")
802 } else {
803 root.as_path()
804 };
805 let item = build_tree_item(path, Path::new(""), menu.clone(), true, ctx);
806 root_items.push(item);
807 item
808 } else {
809 #[cfg(not(target_arch = "wasm32"))]
810 {
811 let mut parent = Handle::NONE;
812
813 for disk in sysinfo::System::new_with_specifics(RefreshKind::new().with_disks_list())
815 .disks()
816 .iter()
817 .map(|i| i.mount_point().to_string_lossy())
818 {
819 let disk_letter = disk.chars().next().unwrap() as u8;
820 let is_disk_part_of_path = dest_disk == Some(disk_letter);
821
822 let item =
823 build_tree_item(disk.as_ref(), "", menu.clone(), is_disk_part_of_path, ctx);
824
825 if is_disk_part_of_path {
826 parent = item;
827 }
828
829 root_items.push(item);
830 }
831
832 parent
833 }
834
835 #[cfg(target_arch = "wasm32")]
836 {
837 Handle::NONE
838 }
839 };
840
841 let mut path_item = Handle::NONE;
842
843 let mut full_path = PathBuf::new();
845 for (i, component) in dest_path_components.iter().enumerate() {
846 full_path = full_path.join(component.as_os_str());
848
849 let next_component = dest_path_components.get(i + 1);
850
851 if let Some(next_component) = next_component {
852 if matches!(component, Component::Prefix(_))
853 && matches!(next_component, Component::RootDir)
854 {
855 continue;
856 }
857 }
858
859 let next = next_component.map(|p| full_path.join(p));
860
861 let mut new_parent = parent;
862 if let Ok(dir_iter) = std::fs::read_dir(&full_path) {
863 let mut entries: Vec<_> = dir_iter.flatten().collect();
864 entries.sort_unstable_by(sort_dir_entries);
865 for entry in entries {
866 let path = entry.path();
867 #[allow(clippy::blocks_in_conditions)]
868 if filter
869 .as_mut()
870 .map_or(true, |f| f.0.borrow_mut().deref_mut().lock()(&path))
871 {
872 let is_part_of_final_path = next.as_ref().is_some_and(|next| *next == path);
873
874 let item = build_tree_item(
875 &path,
876 &full_path,
877 menu.clone(),
878 is_part_of_final_path,
879 ctx,
880 );
881
882 if parent.is_some() {
883 Tree::add_item(parent, item, ctx);
884 } else {
885 root_items.push(item);
886 }
887
888 if is_part_of_final_path {
889 new_parent = item;
890 }
891
892 if path == dest_path {
893 path_item = item;
894 }
895 }
896 }
897 }
898 parent = new_parent;
899 }
900
901 BuildResult {
902 root_items,
903 path_item,
904 }
905}
906
907pub struct FileBrowserBuilder {
908 widget_builder: WidgetBuilder,
909 path: PathBuf,
910 filter: Option<Filter>,
911 root: Option<PathBuf>,
912 mode: FileBrowserMode,
913 show_path: bool,
914}
915
916impl FileBrowserBuilder {
917 pub fn new(widget_builder: WidgetBuilder) -> Self {
918 Self {
919 widget_builder,
920 path: "./".into(),
921 filter: None,
922 root: None,
923 mode: FileBrowserMode::Open,
924 show_path: true,
925 }
926 }
927
928 pub fn with_filter(mut self, filter: Filter) -> Self {
929 self.filter = Some(filter);
930 self
931 }
932
933 pub fn with_opt_filter(mut self, filter: Option<Filter>) -> Self {
934 self.filter = filter;
935 self
936 }
937
938 pub fn with_mode(mut self, mode: FileBrowserMode) -> Self {
939 self.mode = mode;
940 self
941 }
942
943 pub fn with_show_path(mut self, show_path: bool) -> Self {
944 self.show_path = show_path;
945 self
946 }
947
948 pub fn with_path<P: AsRef<Path>>(mut self, path: P) -> Self {
958 path.as_ref().clone_into(&mut self.path);
959 self
960 }
961
962 pub fn with_root(mut self, root: PathBuf) -> Self {
963 self.root = Some(root);
964 self
965 }
966
967 pub fn with_opt_root(mut self, root: Option<PathBuf>) -> Self {
968 self.root = root;
969 self
970 }
971
972 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
973 let item_context_menu = RcUiNodeHandle::new(ItemContextMenu::build(ctx), ctx.sender());
974
975 let BuildResult {
976 root_items: items, ..
977 } = build_all(
978 self.root.as_ref(),
979 self.path.as_path(),
980 self.filter.clone(),
981 item_context_menu.clone(),
982 ctx,
983 );
984
985 let path_text;
986 let tree_root;
987 let scroll_viewer = ScrollViewerBuilder::new(
988 WidgetBuilder::new()
989 .on_row(match self.mode {
990 FileBrowserMode::Open => 1,
991 FileBrowserMode::Save { .. } => 2,
992 })
993 .on_column(0),
994 )
995 .with_content({
996 tree_root = TreeRootBuilder::new(WidgetBuilder::new())
997 .with_items(items)
998 .build(ctx);
999 tree_root
1000 })
1001 .build(ctx);
1002
1003 let home_dir;
1004 let desktop_dir;
1005 let grid = GridBuilder::new(
1006 WidgetBuilder::new()
1007 .with_child(
1008 GridBuilder::new(
1009 WidgetBuilder::new()
1010 .with_visibility(self.show_path)
1011 .with_height(24.0)
1012 .with_child({
1013 home_dir = ButtonBuilder::new(
1014 WidgetBuilder::new()
1015 .on_column(0)
1016 .with_width(24.0)
1017 .with_tooltip(make_simple_tooltip(ctx, "Home Folder"))
1018 .with_margin(Thickness::uniform(1.0)),
1019 )
1020 .with_text("H")
1021 .build(ctx);
1022 home_dir
1023 })
1024 .with_child({
1025 desktop_dir = ButtonBuilder::new(
1026 WidgetBuilder::new()
1027 .on_column(1)
1028 .with_width(24.0)
1029 .with_tooltip(make_simple_tooltip(ctx, "Desktop Folder"))
1030 .with_margin(Thickness::uniform(1.0)),
1031 )
1032 .with_text("D")
1033 .build(ctx);
1034 desktop_dir
1035 })
1036 .with_child({
1037 path_text = TextBoxBuilder::new(
1038 WidgetBuilder::new()
1039 .with_enabled(matches!(self.mode, FileBrowserMode::Open))
1041 .on_row(0)
1042 .on_column(2)
1043 .with_margin(Thickness::uniform(2.0)),
1044 )
1045 .with_text_commit_mode(TextCommitMode::Immediate)
1046 .with_vertical_text_alignment(VerticalAlignment::Center)
1047 .with_text(self.path.to_string_lossy().as_ref())
1048 .build(ctx);
1049 path_text
1050 }),
1051 )
1052 .add_row(Row::stretch())
1053 .add_column(Column::auto())
1054 .add_column(Column::auto())
1055 .add_column(Column::stretch())
1056 .build(ctx),
1057 )
1058 .with_child(scroll_viewer),
1059 )
1060 .add_column(Column::stretch())
1061 .add_rows(match self.mode {
1062 FileBrowserMode::Open => {
1063 vec![Row::auto(), Row::stretch()]
1064 }
1065 FileBrowserMode::Save { .. } => {
1066 vec![Row::auto(), Row::strict(24.0), Row::stretch()]
1067 }
1068 })
1069 .build(ctx);
1070
1071 let file_name = match self.mode {
1072 FileBrowserMode::Save {
1073 ref default_file_name,
1074 } => {
1075 let file_name;
1076 let name_grid = GridBuilder::new(
1077 WidgetBuilder::new()
1078 .on_row(1)
1079 .on_column(0)
1080 .with_child(
1081 TextBuilder::new(
1082 WidgetBuilder::new()
1083 .on_row(0)
1084 .on_column(0)
1085 .with_vertical_alignment(VerticalAlignment::Center),
1086 )
1087 .with_text("File Name:")
1088 .build(ctx),
1089 )
1090 .with_child({
1091 file_name = TextBoxBuilder::new(
1092 WidgetBuilder::new()
1093 .on_row(0)
1094 .on_column(1)
1095 .with_margin(Thickness::uniform(2.0)),
1096 )
1097 .with_text_commit_mode(TextCommitMode::Immediate)
1098 .with_vertical_text_alignment(VerticalAlignment::Center)
1099 .with_text(default_file_name.to_string_lossy())
1100 .build(ctx);
1101 file_name
1102 }),
1103 )
1104 .add_row(Row::stretch())
1105 .add_column(Column::strict(80.0))
1106 .add_column(Column::stretch())
1107 .build(ctx);
1108
1109 ctx.link(name_grid, grid);
1110
1111 file_name
1112 }
1113 FileBrowserMode::Open => Default::default(),
1114 };
1115
1116 let widget = self
1117 .widget_builder
1118 .with_need_update(true)
1119 .with_child(grid)
1120 .build(ctx);
1121
1122 let the_path = match &self.root {
1123 Some(path) => path.clone(),
1124 _ => self.path.clone(),
1125 };
1126 let (fs_sender, fs_receiver) = mpsc::channel();
1127 let browser = FileBrowser {
1128 fs_receiver: Some(fs_receiver),
1129 widget,
1130 tree_root,
1131 home_dir,
1132 desktop_dir,
1133 path_text,
1134 path: match self.mode {
1135 FileBrowserMode::Open => self.path,
1136 FileBrowserMode::Save {
1137 ref default_file_name,
1138 } => self.path.join(default_file_name),
1139 },
1140 file_name_value: match self.mode {
1141 FileBrowserMode::Open => Default::default(),
1142 FileBrowserMode::Save {
1143 ref default_file_name,
1144 } => default_file_name.clone(),
1145 },
1146 filter: self.filter,
1147 mode: self.mode,
1148 scroll_viewer,
1149 root: self.root,
1150 file_name,
1151 watcher: setup_filebrowser_fs_watcher(fs_sender, the_path),
1152 item_context_menu,
1153 };
1154 ctx.add_node(UiNode::new(browser))
1155 }
1156}
1157
1158fn setup_filebrowser_fs_watcher(
1159 fs_sender: mpsc::Sender<notify::Event>,
1160 the_path: PathBuf,
1161) -> Option<(notify::RecommendedWatcher, thread::JoinHandle<()>)> {
1162 let (tx, rx) = mpsc::channel();
1163 match notify::RecommendedWatcher::new(
1164 tx,
1165 notify::Config::default().with_poll_interval(time::Duration::from_secs(1)),
1166 ) {
1167 Ok(mut watcher) => {
1168 #[allow(clippy::while_let_loop)]
1169 let watcher_conversion_thread = std::thread::spawn(move || loop {
1170 match rx.recv() {
1171 Ok(event) => {
1172 if let Ok(event) = event {
1173 let _ = fs_sender.send(event);
1174 }
1175 }
1176 Err(_) => {
1177 break;
1178 }
1179 };
1180 });
1181 let _ = watcher.watch(&the_path, notify::RecursiveMode::Recursive);
1182 Some((watcher, watcher_conversion_thread))
1183 }
1184 Err(_) => None,
1185 }
1186}
1187
1188#[cfg(test)]
1189mod test {
1190 use crate::file_browser::FileBrowserBuilder;
1191 use crate::test::test_widget_deletion;
1192 use crate::{
1193 core::pool::Handle,
1194 file_browser::{build_tree, find_tree},
1195 tree::TreeRootBuilder,
1196 widget::WidgetBuilder,
1197 RcUiNodeHandle, UserInterface,
1198 };
1199 use fyrox_core::algebra::Vector2;
1200 use fyrox_core::parking_lot::Mutex;
1201 use std::path::PathBuf;
1202 use std::sync::Arc;
1203
1204 #[test]
1205 fn test_deletion() {
1206 test_widget_deletion(|ctx| FileBrowserBuilder::new(WidgetBuilder::new()).build(ctx));
1207 }
1208
1209 #[test]
1210 fn test_find_tree() {
1211 let mut ui = UserInterface::new(Vector2::new(100.0, 100.0));
1212
1213 let root = TreeRootBuilder::new(
1214 WidgetBuilder::new().with_user_data(Arc::new(Mutex::new(PathBuf::from("test")))),
1215 )
1216 .build(&mut ui.build_ctx());
1217
1218 let path = build_tree(
1219 root,
1220 true,
1221 "./test/path1",
1222 "./test",
1223 RcUiNodeHandle::new(Handle::new(0, 1), ui.sender()),
1224 &mut ui,
1225 );
1226
1227 while ui.poll_message().is_some() {}
1228
1229 assert_eq!(find_tree(root, &"./test/path1", &ui), path);
1231
1232 assert_eq!(find_tree(root, &"test/path1", &ui), Handle::NONE);
1235 }
1236}