rg3d_ui/
file_browser.rs

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