1use 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 let result = build_all(
129 self.root.as_ref(),
130 &self.path,
131 self.filter.clone(),
132 &mut ui.build_ctx(),
133 );
134
135 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 ui.send_message(TreeRootMessage::select(
145 self.tree_root,
146 MessageDirection::ToWidget,
147 vec![result.path_item],
148 ));
149 ui.send_message(ScrollViewerMessage::bring_into_view(
151 self.scroll_viewer,
152 MessageDirection::ToWidget,
153 result.path_item,
154 ));
155 } else {
156 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 let result = build_all(
196 self.root.as_ref(),
197 &existing_path,
198 self.filter.clone(),
199 &mut ui.build_ctx(),
200 );
201
202 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 ui.send_message(TextBoxMessage::text(
216 self.path_text,
217 MessageDirection::ToWidget,
218 path.to_string_lossy().to_string(),
219 ));
220
221 if item.is_some() {
223 ui.send_message(TreeRootMessage::select(
225 self.tree_root,
226 MessageDirection::ToWidget,
227 vec![item],
228 ));
229
230 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 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 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 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
600fn 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 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 let mut full_path = PathBuf::new();
679 for (i, component) in dest_path_components.iter().enumerate() {
680 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 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 .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#[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
989impl 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 assert_eq!(find_tree(root, &"./test/path1", &ui), path);
1236
1237 assert_eq!(find_tree(root, &"test/path1", &ui), Handle::NONE);
1240 }
1241}