fyrox_ui/file_browser/
mod.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! File browser is a tree view over file system. It allows to select file or folder.
22//!
23//! File selector is dialog window with file browser, it somewhat similar to standard
24//! OS file selector.
25
26use 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        /// Could be empty if a dropped widget is not a file browser item.
85        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        // Generate new tree contents.
210        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        // Replace tree contents.
219        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            // Select item of new path.
227            ui.send_message(TreeRootMessage::select(
228                self.tree_root,
229                MessageDirection::ToWidget,
230                vec![result.path_item],
231            ));
232            // Bring item of new path into view.
233            ui.send_message(ScrollViewerMessage::bring_into_view(
234                self.scroll_viewer,
235                MessageDirection::ToWidget,
236                result.path_item,
237            ));
238        } else {
239            // Clear text field if path is invalid.
240            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                                // Generate new tree contents.
266                                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                                // Replace tree contents.
275                                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                            // Set value of text field.
287                            ui.send_message(TextMessage::text(
288                                self.path_text,
289                                MessageDirection::ToWidget,
290                                path.to_string_lossy().to_string(),
291                            ));
292
293                            // Path can be invalid, so we shouldn't do anything in such case.
294                            if item.is_some() {
295                                // Select item of new path.
296                                ui.send_message(TreeRootMessage::select(
297                                    self.tree_root,
298                                    MessageDirection::ToWidget,
299                                    vec![item],
300                                ));
301
302                                // Bring item of new path into view.
303                                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(&current_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                                // Select item of new path.
399                                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                // Look into internals of directory and build tree items.
434                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                // Nuke everything in collapsed item. This also will free some resources
463                // and will speed up layout pass.
464                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                            // Do response.
524                            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
753/// Builds entire file system tree to given final_path.
754fn 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    // There should be at least one component in the path. If the path is empty, this means that
773    // it "points" to the current directory.
774    if dest_path.as_os_str().is_empty() {
775        dest_path.push(".");
776    }
777
778    // Relative paths must always start from CurDir component (./), otherwise the root dir will be ignored
779    // and the tree will be incorrect.
780    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            // Create items for disks.
814            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    // Try to build tree only for given path.
844    let mut full_path = PathBuf::new();
845    for (i, component) in dest_path_components.iter().enumerate() {
846        // Concat parts of path one by one.
847        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    /// Sets desired path which will be used to build file system tree.
949    ///
950    /// # Notes
951    ///
952    /// It does **not** bring tree item with given path into view because it is impossible
953    /// during construction stage - there is not enough layout information to do so. You
954    /// can send FileBrowserMessage::Path right after creation and it will bring tree item
955    /// into view without any problems. It is possible because all widgets were created at
956    /// that moment and layout system can give correct offsets to bring item into view.
957    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                                        // Disable path if we're in Save mode
1040                                        .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        // This passes.
1230        assert_eq!(find_tree(root, &"./test/path1", &ui), path);
1231
1232        // This expected to fail
1233        // https://github.com/rust-lang/rust/issues/31374
1234        assert_eq!(find_tree(root, &"test/path1", &ui), Handle::NONE);
1235    }
1236}