1use 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 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 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 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(¤t_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 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 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 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 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 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 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 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}