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