egui_file_dialog/
create_directory_dialog.rs

1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use crate::{FileDialogConfig, FileDialogLabels, FileSystem};
5
6pub struct CreateDirectoryResponse {
7    /// Contains the path to the directory that was created.
8    directory: Option<PathBuf>,
9}
10
11impl CreateDirectoryResponse {
12    /// Creates a new response object with the given directory.
13    pub fn new(directory: &Path) -> Self {
14        Self {
15            directory: Some(directory.to_path_buf()),
16        }
17    }
18
19    /// Creates a new response with no directory set.
20    pub const fn new_empty() -> Self {
21        Self { directory: None }
22    }
23
24    /// Returns the directory that was created.
25    /// None is returned if no directory has been created yet.
26    pub fn directory(&self) -> Option<PathBuf> {
27        self.directory.clone()
28    }
29}
30
31/// A dialog to create new folder.
32#[derive(Debug)]
33pub struct CreateDirectoryDialog {
34    /// If the dialog is currently open
35    open: bool,
36    /// If the update method is called for the first time.
37    /// Used to initialize some stuff and scroll to the dialog.
38    init: bool,
39    /// The directory that is currently open and where the folder is created.
40    directory: Option<PathBuf>,
41
42    /// Buffer to hold the data of the folder name input
43    input: String,
44    /// This contains the error message if the folder name is invalid
45    error: Option<String>,
46    /// If we should scroll to the error in the next frame
47    scroll_to_error: bool,
48    /// If the text input should request focus in the next frame
49    request_focus: bool,
50
51    file_system: Arc<dyn FileSystem + Send + Sync>,
52}
53
54impl CreateDirectoryDialog {
55    /// Creates a new dialog with default values
56    pub fn from_filesystem(file_system: Arc<dyn FileSystem + Send + Sync>) -> Self {
57        Self {
58            open: false,
59            init: false,
60            directory: None,
61
62            input: String::new(),
63            error: None,
64            scroll_to_error: false,
65            request_focus: true,
66            file_system,
67        }
68    }
69
70    /// Resets the dialog and opens it.
71    pub fn open(&mut self, directory: PathBuf) {
72        self.reset();
73
74        self.open = true;
75        self.init = true;
76        self.directory = Some(directory);
77    }
78
79    /// Closes and resets the dialog without creating the directory.
80    pub fn close(&mut self) {
81        self.reset();
82    }
83
84    /// Tries to create the given folder.
85    pub fn submit(&mut self) -> CreateDirectoryResponse {
86        // Only necessary in the event of an error
87        self.request_focus = true;
88
89        if self.error.is_none() {
90            return self.create_directory();
91        }
92
93        CreateDirectoryResponse::new_empty()
94    }
95
96    /// Main update function of the dialog. Should be called in every frame
97    /// in which the dialog is to be displayed.
98    pub fn update(
99        &mut self,
100        ui: &mut egui::Ui,
101        config: &FileDialogConfig,
102    ) -> CreateDirectoryResponse {
103        if !self.open {
104            return CreateDirectoryResponse::new_empty();
105        }
106
107        let mut result = CreateDirectoryResponse::new_empty();
108
109        ui.horizontal(|ui| {
110            ui.label(&config.default_folder_icon);
111
112            let text_edit_response = ui.text_edit_singleline(&mut self.input);
113
114            if self.init {
115                text_edit_response.scroll_to_me(Some(egui::Align::Center));
116                text_edit_response.request_focus();
117
118                self.error = self.validate_input(&config.labels);
119                self.init = false;
120                self.request_focus = false;
121            }
122
123            if self.request_focus {
124                text_edit_response.request_focus();
125                self.request_focus = false;
126            }
127
128            if text_edit_response.changed() {
129                self.error = self.validate_input(&config.labels);
130            }
131
132            let apply_button_response =
133                ui.add_enabled(self.error.is_none(), egui::Button::new("✔"));
134
135            if apply_button_response.clicked() {
136                result = self.submit();
137            }
138
139            if ui.button("✖").clicked()
140                || (text_edit_response.lost_focus() && !apply_button_response.contains_pointer())
141            {
142                self.close();
143            }
144        });
145
146        if let Some(err) = &self.error {
147            ui.add_space(5.0);
148
149            let response = ui
150                .horizontal_wrapped(|ui| {
151                    ui.spacing_mut().item_spacing.x = 0.0;
152
153                    ui.colored_label(
154                        ui.style().visuals.error_fg_color,
155                        format!("{} ", config.err_icon),
156                    );
157
158                    ui.label(err);
159                })
160                .response;
161
162            if self.scroll_to_error {
163                response.scroll_to_me(Some(egui::Align::Center));
164                self.scroll_to_error = false;
165            }
166        }
167
168        result
169    }
170
171    /// Returns if the dialog is currently open
172    pub const fn is_open(&self) -> bool {
173        self.open
174    }
175
176    /// Creates a new folder in the current directory.
177    /// The variable `input` is used as the folder name.
178    /// Might change the `error` variable when an error occurred creating the new folder.
179    fn create_directory(&mut self) -> CreateDirectoryResponse {
180        if let Some(mut dir) = self.directory.clone() {
181            dir.push(self.input.as_str());
182
183            match self.file_system.create_dir(&dir) {
184                Ok(()) => {
185                    self.close();
186                    return CreateDirectoryResponse::new(dir.as_path());
187                }
188                Err(err) => {
189                    self.error = Some(self.create_error(format!("Error: {err}").as_str()));
190                    return CreateDirectoryResponse::new_empty();
191                }
192            }
193        }
194
195        // This error should not occur because the create_directory function is only
196        // called when the dialog is open and the directory is set.
197        // If this error occurs, there is most likely a bug in the code.
198        self.error = Some(self.create_error("No directory given"));
199
200        CreateDirectoryResponse::new_empty()
201    }
202
203    /// Validates the folder name input.
204    /// Returns None if the name is valid. Otherwise returns the error message.
205    fn validate_input(&mut self, labels: &FileDialogLabels) -> Option<String> {
206        if self.input.is_empty() {
207            return Some(self.create_error(&labels.err_empty_file_name));
208        }
209
210        if let Some(mut x) = self.directory.clone() {
211            x.push(self.input.as_str());
212
213            if x.is_dir() {
214                return Some(self.create_error(&labels.err_directory_exists));
215            }
216            if x.is_file() {
217                return Some(self.create_error(&labels.err_file_exists));
218            }
219        } else {
220            // This error should not occur because the validate_input function is only
221            // called when the dialog is open and the directory is set.
222            // If this error occurs, there is most likely a bug in the code.
223            return Some(self.create_error("No directory given"));
224        }
225
226        None
227    }
228
229    /// Creates the specified error and sets to scroll to the error in the next frame.
230    fn create_error(&mut self, error: &str) -> String {
231        self.scroll_to_error = true;
232        error.to_string()
233    }
234
235    /// Resets the dialog.
236    /// Configuration variables are not changed.
237    fn reset(&mut self) {
238        self.open = false;
239        self.init = false;
240        self.directory = None;
241        self.input.clear();
242        self.error = None;
243        self.scroll_to_error = false;
244    }
245}