Skip to main content

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 is somewhat similar to standard
24//! OS file selector.
25
26use crate::{
27    button::{ButtonBuilder, ButtonMessage},
28    core::{
29        err, log::Log, ok_or_continue, ok_or_return, parking_lot::Mutex, pool::Handle,
30        reflect::prelude::*, some_or_return, type_traits::prelude::*, visitor::prelude::*,
31        SafeLock,
32    },
33    file_browser::{
34        fs_tree::{sanitize_path, TreeItemPath},
35        menu::ItemContextMenu,
36    },
37    formatted_text::WrapMode,
38    grid::{Column, GridBuilder, Row},
39    message::{MessageData, UiMessage},
40    scroll_viewer::{ScrollViewerBuilder, ScrollViewerMessage},
41    style::{resource::StyleResourceExt, Style},
42    text::{TextBuilder, TextMessage},
43    text_box::{TextBoxBuilder, TextCommitMode},
44    tree::{Tree, TreeMessage, TreeRoot, TreeRootBuilder, TreeRootMessage},
45    utils::make_simple_tooltip,
46    widget::{Widget, WidgetBuilder, WidgetMessage},
47    BuildContext, Control, HorizontalAlignment, RcUiNodeHandle, Thickness, UiNode, UserInterface,
48    VerticalAlignment,
49};
50use core::time;
51use fyrox_graph::{
52    constructor::{ConstructorProvider, GraphNodeConstructor},
53    SceneGraph,
54};
55use notify::{Event, Watcher};
56use std::{
57    collections::VecDeque,
58    fmt::{Debug, Formatter},
59    path::{Path, PathBuf},
60    sync::{mpsc::Sender, Arc},
61};
62
63mod dialog;
64mod field;
65mod filter;
66mod fs_tree;
67mod menu;
68mod selector;
69#[cfg(test)]
70mod test;
71
72use crate::button::Button;
73use crate::scroll_viewer::ScrollViewer;
74use crate::text::Text;
75use crate::text_box::TextBox;
76pub use field::*;
77pub use filter::*;
78pub use selector::*;
79
80#[derive(Debug, Clone, PartialEq)]
81pub enum FileBrowserMessage {
82    Root(Option<PathBuf>),
83    Path(PathBuf),
84    Filter(PathFilter),
85    FocusCurrentPath,
86    Rescan,
87    Drop {
88        dropped: Handle<UiNode>,
89        path_item: Handle<UiNode>,
90        path: PathBuf,
91        /// Could be empty if a dropped widget is not a file browser item.
92        dropped_path: PathBuf,
93    },
94}
95impl MessageData for FileBrowserMessage {}
96
97#[derive(Debug, Clone, PartialEq)]
98enum FsEventMessage {
99    Add(PathBuf),
100    Remove(PathBuf),
101}
102impl MessageData for FsEventMessage {}
103
104#[derive(Default, Visit, Reflect, ComponentProvider, TypeUuidProvider)]
105#[type_uuid(id = "b7f4610e-4b0c-4671-9b4a-60bb45268928")]
106#[reflect(derived_type = "UiNode")]
107pub struct FileBrowser {
108    pub widget: Widget,
109    pub tree_root: Handle<TreeRoot>,
110    pub home_dir: Handle<Button>,
111    pub desktop_dir: Handle<Button>,
112    pub path_text: Handle<TextBox>,
113    pub scroll_viewer: Handle<ScrollViewer>,
114    pub no_items_message: Handle<Text>,
115    pub path: PathBuf,
116    pub root: Option<PathBuf>,
117    pub filter: PathFilter,
118    #[visit(skip)]
119    #[reflect(hidden)]
120    fs_events: VecDeque<FsEventMessage>,
121    #[visit(skip)]
122    #[reflect(hidden)]
123    pub item_context_menu: RcUiNodeHandle,
124    #[allow(clippy::type_complexity)]
125    #[visit(skip)]
126    #[reflect(hidden)]
127    pub watcher: Option<notify::RecommendedWatcher>,
128}
129
130impl ConstructorProvider<UiNode, UserInterface> for FileBrowser {
131    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
132        GraphNodeConstructor::new::<Self>()
133            .with_variant("File Browser", |ui| {
134                FileBrowserBuilder::new(WidgetBuilder::new().with_name("File Browser"))
135                    .build(&mut ui.build_ctx())
136                    .to_base()
137                    .into()
138            })
139            .with_group("File System")
140    }
141}
142
143impl Clone for FileBrowser {
144    fn clone(&self) -> Self {
145        Self {
146            widget: self.widget.clone(),
147            tree_root: self.tree_root,
148            home_dir: self.home_dir,
149            desktop_dir: self.desktop_dir,
150            path_text: self.path_text,
151            scroll_viewer: self.scroll_viewer,
152            no_items_message: self.no_items_message,
153            path: self.path.clone(),
154            root: self.root.clone(),
155            filter: self.filter.clone(),
156            fs_events: self.fs_events.clone(),
157            item_context_menu: self.item_context_menu.clone(),
158            watcher: None,
159        }
160    }
161}
162
163impl Debug for FileBrowser {
164    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
165        writeln!(f, "FileBrowser")
166    }
167}
168
169crate::define_widget_deref!(FileBrowser);
170
171fn parent_path(path: &Path) -> PathBuf {
172    let mut parent_path = path.to_owned();
173    parent_path.pop();
174    parent_path
175}
176
177impl FileBrowser {
178    fn select_and_bring_into_view(&self, item: Handle<Tree>, ui: &UserInterface) {
179        ui.send(self.tree_root, TreeRootMessage::Select(vec![item]));
180        ui.send(
181            self.scroll_viewer,
182            ScrollViewerMessage::BringIntoView(item.to_base()),
183        );
184    }
185
186    fn rebuild_fs_tree(&mut self, ui: &mut UserInterface) {
187        let fs_tree = fs_tree::FsTree::new_or_empty(
188            self.root.as_ref(),
189            &self.path,
190            &self.filter,
191            self.item_context_menu.clone(),
192            &mut ui.build_ctx(),
193        );
194
195        ui.send(self.tree_root, TreeRootMessage::Items(fs_tree.root_items));
196        if fs_tree.path_item.is_some() {
197            self.select_and_bring_into_view(fs_tree.path_item, ui);
198        }
199    }
200
201    fn set_path_internal(&mut self, path: PathBuf) {
202        assert!(path.is_absolute());
203        self.path = path.clone();
204    }
205
206    /// Tries to set a new path. This method keeps only the valid part of the supplied path. For
207    /// example, if the path `foo/bar/baz` is supplied and only `foo/bar` exists, then the `foo/bar`
208    /// will be set. This method also does path normalization, which requires FS access, and the actual
209    /// path will be absolute even if the input path was relative.
210    fn set_path(&mut self, path: &Path, ui: &UserInterface) -> bool {
211        fn discard_nonexistent_sub_dirs(path: &Path) -> PathBuf {
212            let mut potentially_existing_path = path.to_owned();
213            while !potentially_existing_path.exists() {
214                if !potentially_existing_path.pop() {
215                    break;
216                }
217            }
218            potentially_existing_path
219        }
220
221        let existing_part = discard_nonexistent_sub_dirs(path);
222
223        match fs_tree::sanitize_path(&existing_part) {
224            Ok(existing_sanitized_path) => {
225                if self.path != existing_sanitized_path {
226                    self.set_path_internal(existing_sanitized_path);
227                    ui.send(
228                        self.path_text,
229                        TextMessage::Text(self.path.to_string_lossy().to_string()),
230                    );
231                    true
232                } else {
233                    false
234                }
235            }
236            Err(err) => {
237                err!(
238                    "Unable to set existing part {} of the path {}. Reason {:?}",
239                    existing_part.display(),
240                    path.display(),
241                    err
242                );
243                false
244            }
245        }
246    }
247
248    /// Same as [`Self::set_path`], but also rebuilds the file system tree to the given path.
249    /// This method keeps only the valid part of the supplied path. For example, if the path
250    /// `foo/bar/baz` is supplied and only `foo/bar` exists, then the tree will be built only to
251    /// that path.
252    fn set_path_and_rebuild_tree(&mut self, path: &Path, ui: &mut UserInterface) -> bool {
253        if !self.set_path(path, ui) {
254            return false;
255        }
256        let existing_item = fs_tree::find_tree_item(self.tree_root, &self.path, ui);
257        if existing_item.is_some() {
258            self.select_and_bring_into_view(existing_item.to_variant(), ui)
259        } else {
260            self.rebuild_fs_tree(ui)
261        }
262        true
263    }
264
265    fn set_root(&mut self, root: Option<&Path>, ui: &mut UserInterface) {
266        self.root = root.as_ref().and_then(|root| sanitize_path(root).ok());
267        let watcher_replacement = match self.watcher.take() {
268            Some(mut watcher) => {
269                let current_root = match &self.root {
270                    Some(path) => path.clone(),
271                    None => self.path.clone(),
272                };
273                if current_root.exists() {
274                    Log::verify(watcher.unwatch(&current_root));
275                }
276                let new_root = match &self.root {
277                    Some(path) => path.clone(),
278                    None => self.path.clone(),
279                };
280                Log::verify(watcher.watch(&new_root, notify::RecursiveMode::Recursive));
281                Some(watcher)
282            }
283            None => None,
284        };
285        if let Some(root) = self.root.clone() {
286            self.set_path(&root, ui);
287            let tree_item_path = Arc::new(Mutex::new(TreeItemPath::root(root.clone())));
288            ui[self.tree_root].user_data = Some(tree_item_path.clone());
289            self.user_data = Some(tree_item_path);
290        }
291        self.rebuild_fs_tree(ui);
292        for button in [self.home_dir, self.desktop_dir] {
293            ui.send(button, WidgetMessage::Visibility(self.root.is_none()));
294        }
295        self.watcher = watcher_replacement;
296    }
297
298    fn on_file_added(&mut self, path: &Path, ui: &mut UserInterface) {
299        if !self.filter.supports_all(path) {
300            return;
301        }
302
303        if fs_tree::find_tree_item(self.tree_root, path, ui).is_some() {
304            return;
305        }
306
307        let parent_path = parent_path(path);
308        let parent_node = fs_tree::find_tree_item(self.tree_root, &parent_path, ui);
309        if parent_node.is_none() {
310            return;
311        }
312
313        let mut need_build_tree = false;
314        if let Some(tree) = ui.node(parent_node).cast::<Tree>() {
315            if tree.is_expanded {
316                need_build_tree = true;
317            } else if !tree.always_show_expander {
318                ui.send(tree.handle(), TreeMessage::ExpanderVisible(true))
319            }
320        } else if ui.node(parent_node).cast::<TreeRoot>().is_some() {
321            need_build_tree = true;
322        }
323        if need_build_tree {
324            fs_tree::build_tree(
325                parent_node,
326                path,
327                &parent_path,
328                self.item_context_menu.clone(),
329                &self.filter,
330                ui,
331            );
332        }
333    }
334
335    fn on_items_changed(&self, ui: &UserInterface) {
336        let show_no_items_message = ui[self.tree_root].items.is_empty();
337        ui.send(
338            self.no_items_message,
339            WidgetMessage::Visibility(show_no_items_message),
340        );
341    }
342
343    fn on_file_removed(&mut self, path: &Path, ui: &mut UserInterface) {
344        let tree_item = fs_tree::find_tree_item(self.tree_root, path, ui);
345        if tree_item.is_some() {
346            let parent_path = parent_path(path);
347            let parent_tree = fs_tree::find_tree_item(self.tree_root, &parent_path, ui);
348            if let Ok(parent_tree_node) = ui.try_get(parent_tree) {
349                if parent_tree_node.has_component::<TreeRoot>() {
350                    ui.send(
351                        parent_tree,
352                        TreeRootMessage::RemoveItem(tree_item.to_variant()),
353                    )
354                } else {
355                    ui.send(parent_tree, TreeMessage::RemoveItem(tree_item.to_variant()))
356                }
357            }
358        }
359    }
360
361    fn handle_fs_event_message(&mut self, msg: &FsEventMessage, ui: &mut UserInterface) {
362        match msg {
363            FsEventMessage::Add(path) => self.on_file_added(path, ui),
364            FsEventMessage::Remove(path) => self.on_file_removed(path, ui),
365        }
366    }
367
368    fn on_file_browser_message(
369        &mut self,
370        message: &UiMessage,
371        message_data: &FileBrowserMessage,
372        ui: &mut UserInterface,
373    ) {
374        match message_data {
375            FileBrowserMessage::Path(path) => {
376                if self.set_path_and_rebuild_tree(path, ui) {
377                    ui.send_message(UiMessage::from_widget(
378                        message.destination(),
379                        FileBrowserMessage::Path(self.path.clone()),
380                    ));
381                }
382            }
383            FileBrowserMessage::Root(root) => {
384                if &self.root != root {
385                    self.set_root(root.as_deref(), ui)
386                }
387            }
388            FileBrowserMessage::Filter(filter) => {
389                if &self.filter != filter {
390                    self.filter.clone_from(filter);
391                    self.rebuild_fs_tree(ui);
392                }
393            }
394            FileBrowserMessage::Rescan => {
395                self.rebuild_fs_tree(ui);
396            }
397            FileBrowserMessage::Drop { .. } => (),
398            FileBrowserMessage::FocusCurrentPath => {
399                let item = fs_tree::find_tree_item(self.tree_root, &self.path, ui);
400                if item.is_some() {
401                    // Select item of new path.
402                    ui.send(
403                        self.tree_root,
404                        TreeRootMessage::Select(vec![item.to_variant()]),
405                    );
406                    ui.send(self.scroll_viewer, ScrollViewerMessage::BringIntoView(item));
407                }
408            }
409        }
410    }
411
412    fn on_sub_tree_expanded(
413        &mut self,
414        sub_tree: Handle<Tree>,
415        expand: bool,
416        ui: &mut UserInterface,
417    ) {
418        if expand {
419            // Look into internals of directory and build tree items.
420            if let Some(parent_tree_item) = fs_tree::tree_path(sub_tree, ui) {
421                fs_tree::build_single_folder(
422                    parent_tree_item.path(),
423                    sub_tree,
424                    self.item_context_menu.clone(),
425                    &self.filter,
426                    ui,
427                )
428            }
429        } else {
430            // Nuke everything in collapsed item. This also will free some resources
431            // and will speed up layout pass.
432            ui.send(
433                sub_tree,
434                TreeMessage::SetItems {
435                    items: vec![],
436                    remove_previous: true,
437                },
438            );
439        }
440    }
441
442    fn on_sub_tree_selected(&mut self, sub_tree: Handle<Tree>, ui: &UserInterface) {
443        let path = some_or_return!(fs_tree::tree_path(sub_tree, ui)).into_path();
444        if self.path != path {
445            // Here we trust the content of the tree items.
446            self.set_path_internal(path.clone());
447
448            ui.send(
449                self.path_text,
450                TextMessage::Text(path.to_string_lossy().to_string()),
451            );
452
453            // Do response.
454            ui.post(self.handle, FileBrowserMessage::Path(path));
455        }
456    }
457
458    fn on_selection_cleared(&mut self, ui: &UserInterface) {
459        let root = some_or_return!(self.root.clone());
460        if self.set_path(&root, ui) {
461            ui.post(self.handle, FileBrowserMessage::Path(self.path.clone()));
462        }
463    }
464
465    fn on_drop(
466        &self,
467        what_dropped: Handle<UiNode>,
468        where_dropped: Handle<UiNode>,
469        ui: &UserInterface,
470    ) {
471        let path = some_or_return!(fs_tree::tree_path(where_dropped.to_variant(), ui)).into_path();
472        let dropped_path =
473            some_or_return!(fs_tree::tree_path(what_dropped.to_variant(), ui)).into_path();
474        ui.post(
475            self.handle,
476            FileBrowserMessage::Drop {
477                dropped: what_dropped,
478                path_item: where_dropped,
479                path,
480                dropped_path,
481            },
482        );
483    }
484
485    fn on_desktop_dir_clicked(&self, #[allow(unused_variables)] ui: &UserInterface) {
486        #[cfg(not(target_arch = "wasm32"))]
487        {
488            let user_dirs = directories::UserDirs::new();
489            if let Some(desktop_dir) = user_dirs.as_ref().and_then(|dirs| dirs.desktop_dir()) {
490                ui.send(
491                    self.handle,
492                    FileBrowserMessage::Path(desktop_dir.to_path_buf()),
493                );
494            }
495        }
496    }
497
498    fn on_home_dir_clicked(&self, #[allow(unused_variables)] ui: &UserInterface) {
499        #[cfg(not(target_arch = "wasm32"))]
500        {
501            let user_dirs = directories::UserDirs::new();
502            if let Some(home_dir) = user_dirs.as_ref().map(|dirs| dirs.home_dir()) {
503                ui.send(
504                    self.handle,
505                    FileBrowserMessage::Path(home_dir.to_path_buf()),
506                );
507            }
508        }
509    }
510
511    fn register_fs_event(&mut self, fs_event: FsEventMessage) {
512        // Accumulate events in the queue with deduplication.
513        if !self.fs_events.contains(&fs_event) {
514            self.fs_events.push_back(fs_event);
515        }
516    }
517}
518
519impl Control for FileBrowser {
520    fn update(&mut self, _dt: f32, ui: &mut UserInterface) {
521        while let Some(event) = self.fs_events.pop_front() {
522            self.handle_fs_event_message(&event, ui);
523        }
524    }
525
526    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
527        self.widget.handle_routed_message(ui, message);
528        if let Some(msg) = message.data_for::<FsEventMessage>(self.handle) {
529            self.register_fs_event(msg.clone());
530        } else if let Some(message_data) = message.data_for::<FileBrowserMessage>(self.handle) {
531            self.on_file_browser_message(message, message_data, ui)
532        } else if let Some(TreeMessage::Expand { expand, .. }) = message.data() {
533            self.on_sub_tree_expanded(message.destination().to_variant(), *expand, ui)
534        } else if let Some(WidgetMessage::Drop(dropped)) = message.data() {
535            if !message.handled() {
536                self.on_drop(*dropped, message.destination(), ui);
537                message.set_handled(true);
538            }
539        } else if let Some(TreeRootMessage::Select(selection)) = message.data_from(self.tree_root) {
540            if let Some(&first_selected) = selection.first() {
541                self.on_sub_tree_selected(first_selected, ui)
542            } else {
543                self.on_selection_cleared(ui)
544            }
545        } else if let Some(ButtonMessage::Click) = message.data_from(self.desktop_dir) {
546            self.on_desktop_dir_clicked(ui)
547        } else if let Some(ButtonMessage::Click) = message.data_from(self.home_dir) {
548            self.on_home_dir_clicked(ui)
549        } else if let Some(TreeRootMessage::ItemsChanged) = message.data_from(self.tree_root) {
550            self.on_items_changed(ui)
551        } else if let Some(WidgetMessage::MouseDown { .. }) = message.data() {
552            if self.root.is_some() && !message.handled() {
553                ui.send(self.tree_root, TreeRootMessage::Select(vec![]));
554                message.set_handled(true);
555            }
556        }
557    }
558
559    fn accepts_drop(&self, widget: Handle<UiNode>, ui: &UserInterface) -> bool {
560        ui.node(widget)
561            .user_data
562            .as_ref()
563            .is_some_and(|data| data.safe_lock().downcast_ref::<TreeItemPath>().is_some())
564    }
565}
566
567pub struct FileBrowserBuilder {
568    widget_builder: WidgetBuilder,
569    path: PathBuf,
570    filter: PathFilter,
571    root: Option<PathBuf>,
572    show_path: bool,
573    no_items_text: String,
574}
575
576impl FileBrowserBuilder {
577    pub fn new(widget_builder: WidgetBuilder) -> Self {
578        Self {
579            widget_builder,
580            path: "./".into(),
581            filter: Default::default(),
582            root: None,
583            show_path: true,
584            no_items_text: "This folder is empty".to_string(),
585        }
586    }
587
588    pub fn with_filter(mut self, filter: PathFilter) -> Self {
589        self.filter = filter;
590        self
591    }
592
593    pub fn with_show_path(mut self, show_path: bool) -> Self {
594        self.show_path = show_path;
595        self
596    }
597
598    pub fn with_no_items_text(mut self, no_items_text: impl AsRef<str>) -> Self {
599        self.no_items_text = no_items_text.as_ref().to_string();
600        self
601    }
602
603    /// Sets desired path which will be used to build file system tree.
604    ///
605    /// # Notes
606    ///
607    /// It does **not** bring tree item with given path into view because it is impossible
608    /// during construction stage - there is not enough layout information to do so. You
609    /// can send FileBrowserMessage::Path right after creation, and it will bring tree item
610    /// into view without any problems. It is possible because all widgets were created at
611    /// that moment and layout system can give correct offsets to bring item into view.
612    pub fn with_path<P: AsRef<Path>>(mut self, path: P) -> Self {
613        path.as_ref().clone_into(&mut self.path);
614        self
615    }
616
617    pub fn with_root(mut self, root: PathBuf) -> Self {
618        self.root = Some(root);
619        self
620    }
621
622    pub fn with_opt_root(mut self, root: Option<PathBuf>) -> Self {
623        self.root = root;
624        self
625    }
626
627    pub fn build(self, ctx: &mut BuildContext) -> Handle<FileBrowser> {
628        let item_context_menu = RcUiNodeHandle::new(ItemContextMenu::build(ctx), ctx.sender());
629
630        let fs_tree::FsTree {
631            root_items: items,
632            items_count,
633            sanitized_root,
634            ..
635        } = fs_tree::FsTree::new_or_empty(
636            self.root.as_ref(),
637            self.path.as_path(),
638            &self.filter,
639            item_context_menu.clone(),
640            ctx,
641        );
642
643        let root_path = sanitized_root.map(TreeItemPath::root);
644
645        let path_text;
646        let tree_root;
647        let scroll_viewer = ScrollViewerBuilder::new(WidgetBuilder::new().on_row(1).on_column(0))
648            .with_content({
649                tree_root = TreeRootBuilder::new(
650                    WidgetBuilder::new().with_user_data_value_opt(root_path.clone()),
651                )
652                .with_items(items)
653                .build(ctx);
654                tree_root
655            })
656            .build(ctx);
657
658        let home_dir;
659        let desktop_dir;
660        let grid = GridBuilder::new(
661            WidgetBuilder::new()
662                .with_child(
663                    GridBuilder::new(
664                        WidgetBuilder::new()
665                            .with_visibility(self.show_path)
666                            .with_height(24.0)
667                            .with_child({
668                                home_dir = ButtonBuilder::new(
669                                    WidgetBuilder::new()
670                                        .with_visibility(self.root.is_none())
671                                        .on_column(0)
672                                        .with_width(24.0)
673                                        .with_tooltip(make_simple_tooltip(ctx, "Home Folder"))
674                                        .with_margin(Thickness::uniform(1.0)),
675                                )
676                                .with_text("H")
677                                .build(ctx);
678                                home_dir
679                            })
680                            .with_child({
681                                desktop_dir = ButtonBuilder::new(
682                                    WidgetBuilder::new()
683                                        .with_visibility(self.root.is_none())
684                                        .on_column(1)
685                                        .with_width(24.0)
686                                        .with_tooltip(make_simple_tooltip(ctx, "Desktop Folder"))
687                                        .with_margin(Thickness::uniform(1.0)),
688                                )
689                                .with_text("D")
690                                .build(ctx);
691                                desktop_dir
692                            })
693                            .with_child({
694                                path_text = TextBoxBuilder::new(
695                                    WidgetBuilder::new()
696                                        .on_row(0)
697                                        .on_column(2)
698                                        .with_margin(Thickness::uniform(2.0)),
699                                )
700                                .with_editable(false)
701                                .with_text_commit_mode(TextCommitMode::Immediate)
702                                .with_vertical_text_alignment(VerticalAlignment::Center)
703                                .with_text(
704                                    fs_tree::sanitize_path(&self.path)
705                                        .ok()
706                                        .map(|p| p.to_string_lossy().to_string())
707                                        .unwrap_or_default(),
708                                )
709                                .build(ctx);
710                                path_text
711                            }),
712                    )
713                    .add_row(Row::stretch())
714                    .add_column(Column::auto())
715                    .add_column(Column::auto())
716                    .add_column(Column::stretch())
717                    .build(ctx),
718                )
719                .with_child(scroll_viewer),
720        )
721        .add_column(Column::stretch())
722        .add_rows(vec![Row::auto(), Row::stretch()])
723        .build(ctx);
724
725        let no_items_message = TextBuilder::new(
726            WidgetBuilder::new()
727                .with_foreground(ctx.style.property(Style::BRUSH_BRIGHT))
728                .with_visibility(items_count == 0)
729                .with_hit_test_visibility(false),
730        )
731        .with_wrap(WrapMode::Word)
732        .with_vertical_text_alignment(VerticalAlignment::Center)
733        .with_horizontal_text_alignment(HorizontalAlignment::Center)
734        .with_text(self.no_items_text)
735        .build(ctx);
736
737        let root_container = GridBuilder::new(
738            WidgetBuilder::new()
739                .with_child(grid)
740                .with_child(no_items_message),
741        )
742        .add_row(Row::stretch())
743        .add_column(Column::stretch())
744        .build(ctx);
745
746        let widget = self
747            .widget_builder
748            .with_user_data_value_opt(root_path)
749            .with_context_menu(item_context_menu.clone())
750            .with_need_update(true)
751            .with_child(root_container)
752            .build(ctx);
753
754        let the_path = match &self.root {
755            Some(path) => path.clone(),
756            _ => self.path.clone(),
757        };
758        let browser = FileBrowser {
759            widget,
760            tree_root,
761            home_dir,
762            desktop_dir,
763            path_text,
764            path: self.path,
765            filter: self.filter,
766            scroll_viewer,
767            root: self.root,
768            watcher: None,
769            item_context_menu,
770            no_items_message,
771            fs_events: Default::default(),
772        };
773        let file_browser_handle = ctx.add(browser);
774        let sender = ctx.sender();
775        ctx[file_browser_handle].watcher =
776            setup_file_browser_fs_watcher(sender, file_browser_handle, the_path);
777        file_browser_handle
778    }
779}
780
781struct EventReceiver {
782    file_browser_handle: Handle<FileBrowser>,
783    sender: Sender<UiMessage>,
784}
785
786impl EventReceiver {
787    fn send(&self, message: impl MessageData) {
788        Log::verify(
789            self.sender
790                .send(UiMessage::for_widget(self.file_browser_handle, message)),
791        )
792    }
793}
794
795impl notify::EventHandler for EventReceiver {
796    fn handle_event(&mut self, event: notify::Result<Event>) {
797        let event = ok_or_return!(event);
798
799        if event.need_rescan() {
800            self.send(FileBrowserMessage::Rescan);
801            return;
802        }
803
804        for path in event.paths.iter() {
805            let path = ok_or_continue!(std::path::absolute(path));
806
807            match event.kind {
808                notify::EventKind::Remove(_) => {
809                    self.send(FsEventMessage::Remove(path.clone()));
810                }
811                notify::EventKind::Create(_) => {
812                    self.send(FsEventMessage::Add(path.clone()));
813                }
814                _ => (),
815            }
816        }
817    }
818}
819
820fn setup_file_browser_fs_watcher(
821    sender: Sender<UiMessage>,
822    file_browser_handle: Handle<FileBrowser>,
823    the_path: PathBuf,
824) -> Option<notify::RecommendedWatcher> {
825    let handler = EventReceiver {
826        file_browser_handle,
827        sender,
828    };
829    let config = notify::Config::default().with_poll_interval(time::Duration::from_secs(1));
830    match notify::RecommendedWatcher::new(handler, config) {
831        Ok(mut watcher) => {
832            Log::verify(watcher.watch(&the_path, notify::RecursiveMode::Recursive));
833            Some(watcher)
834        }
835        Err(_) => None,
836    }
837}