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    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        /// Could be empty if a dropped widget is not a file browser item.
87        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        // Generate new tree contents.
215        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        // Replace tree contents.
225        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            // Select item of new path.
233            ui.send_message(TreeRootMessage::select(
234                self.tree_root,
235                MessageDirection::ToWidget,
236                vec![result.path_item],
237            ));
238            // Bring item of new path into view.
239            ui.send_message(ScrollViewerMessage::bring_into_view(
240                self.scroll_viewer,
241                MessageDirection::ToWidget,
242                result.path_item,
243            ));
244        } else {
245            // Clear text field if path is invalid.
246            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                                // Generate new tree contents.
272                                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                                // Replace tree contents.
282                                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                            // Set value of text field.
294                            ui.send_message(TextMessage::text(
295                                self.path_text,
296                                MessageDirection::ToWidget,
297                                path.to_string_lossy().to_string(),
298                            ));
299
300                            // Path can be invalid, so we shouldn't do anything in such case.
301                            if item.is_some() {
302                                // Select item of new path.
303                                ui.send_message(TreeRootMessage::select(
304                                    self.tree_root,
305                                    MessageDirection::ToWidget,
306                                    vec![item],
307                                ));
308
309                                // Bring item of new path into view.
310                                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(&current_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                                // Select item of new path.
407                                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                // Look into internals of directory and build tree items.
442                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                // Nuke everything in collapsed item. This also will free some resources
472                // and will speed up layout pass.
473                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                            // Do response.
533                            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
814/// Builds entire file system tree to given final_path.
815fn 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    // There should be at least one component in the path. If the path is empty, this means that
835    // it "points" to the current directory.
836    if dest_path.as_os_str().is_empty() {
837        dest_path.push(".");
838    }
839
840    // Relative paths must always start from CurDir component (./), otherwise the root dir will be ignored
841    // and the tree will be incorrect.
842    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            // Create items for disks.
876            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    // Try to build tree only for given path.
912    let mut full_path = PathBuf::new();
913    for (i, component) in dest_path_components.iter().enumerate() {
914        // Concat parts of path one by one.
915        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    /// Sets desired path which will be used to build file system tree.
1025    ///
1026    /// # Notes
1027    ///
1028    /// It does **not** bring tree item with given path into view because it is impossible
1029    /// during construction stage - there is not enough layout information to do so. You
1030    /// can send FileBrowserMessage::Path right after creation and it will bring tree item
1031    /// into view without any problems. It is possible because all widgets were created at
1032    /// that moment and layout system can give correct offsets to bring item into view.
1033    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                                        // Disable path if we're in Save mode
1117                                        .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        // This passes.
1309        assert_eq!(find_tree(root, &"./test/path1", &ui), path);
1310
1311        // This expected to fail
1312        // https://github.com/rust-lang/rust/issues/31374
1313        assert_eq!(find_tree(root, &"test/path1", &ui), Handle::NONE);
1314    }
1315}