egui_path_picker/
path_picker.rs

1use std::{
2    fs::{canonicalize, read_dir},
3    marker::PhantomData,
4    path::{Path, PathBuf},
5};
6
7use egui::{
8    PopupCloseBehavior, Response, ScrollArea, TextBuffer, Ui, Widget,
9    containers::menu::{MenuButton, MenuConfig},
10};
11
12use super::icon_provider::IconProvider;
13
14/// A widget for picking files.
15///
16/// It boils down to a text input, next to a button that opens a widget that
17/// allows the user to search the local files and directories.
18pub struct PathPicker<'input, 'path, S: TextBuffer, I: IconProvider> {
19    /// Buffer into which store the input
20    input: &'input mut S,
21    /// The path to fall back upon when the user inputs an invalid path
22    default_path: &'path Path,
23    provider: PhantomData<I>,
24}
25
26impl<'input, 'path, S: TextBuffer, I: IconProvider> PathPicker<'input, 'path, S, I> {
27    /// Creates a new PathPicker widget
28    ///
29    /// ## Args
30    ///
31    /// - input: the buffer to use for input storage
32    /// - default_path: the path to fall back onto when the user inputs an invalid path
33    pub fn new<P: AsRef<Path>>(input: &'input mut S, default_path: &'path P) -> Self {
34        Self {
35            input,
36            default_path: default_path.as_ref(),
37            provider: PhantomData::default(),
38        }
39    }
40
41    /// Internal method that renders the file picker widget
42    fn picker_widget(self, ui: &mut Ui) {
43        ui.set_max_height(200.);
44        ui.set_max_width(200.);
45        ScrollArea::vertical().show(ui, |ui| {
46            let mut search_path = PathBuf::from(self.input.as_str());
47            if !search_path.exists() {
48                search_path = self.default_path.to_owned();
49            } else if !search_path.is_dir() {
50                search_path = search_path.parent().unwrap_or(self.default_path).to_owned();
51            }
52
53            if let Ok(search_path) = canonicalize(search_path) {
54                if let Some(parent) = search_path.parent() {
55                    if ui.button(I::BACK_ICON).clicked() {
56                        self.input.replace_with(parent.to_string_lossy().as_ref());
57                    }
58                }
59
60                if let Ok(iter) = read_dir(search_path) {
61                    for ent in iter {
62                        if let Ok(ent) = ent {
63                            let path = ent.path();
64
65                            let icon = if path.is_file() {
66                                I::FILE_ICON
67                            } else {
68                                I::FOLDER_ICON
69                            };
70
71                            if ui
72                                .button(format!("{} {}", icon, ent.file_name().display()))
73                                .clicked()
74                            {
75                                self.input.replace_with(path.to_string_lossy().as_ref());
76                                if path.is_file() {
77                                    ui.close();
78                                }
79                            }
80                        }
81                    }
82                }
83            }
84        });
85    }
86}
87
88impl<S: TextBuffer, I: IconProvider> Widget for PathPicker<'_, '_, S, I> {
89    fn ui(self, ui: &mut Ui) -> Response {
90        ui.horizontal(|ui| {
91            ui.text_edit_singleline(self.input);
92            MenuButton::new(I::OPEN_ICON)
93                .config(
94                    MenuConfig::default().close_behavior(PopupCloseBehavior::CloseOnClickOutside),
95                )
96                .ui(ui, |ui| {
97                    self.picker_widget(ui);
98                });
99        })
100        .response
101    }
102}