microui_redux/
file_dialog.rs

1use std::path::Path;
2
3//
4// Copyright 2022-Present (c) Raja Lehtihet & Wael El Oraiby
5//
6// Redistribution and use in source and binary forms, with or without
7// modification, are permitted provided that the following conditions are met:
8//
9// 1. Redistributions of source code must retain the above copyright notice,
10// this list of conditions and the following disclaimer.
11//
12// 2. Redistributions in binary form must reproduce the above copyright notice,
13// this list of conditions and the following disclaimer in the documentation
14// and/or other materials provided with the distribution.
15//
16// 3. Neither the name of the copyright holder nor the names of its contributors
17// may be used to endorse or promote products derived from this software without
18// specific prior written permission.
19//
20// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30// POSSIBILITY OF SUCH DAMAGE.
31//
32use crate::*;
33
34/// Simple modal dialog that lets the user browse and pick files.
35pub struct FileDialogState {
36    current_working_directory: String,
37    file_name: Option<String>,
38    tmp_file_name: Textbox,
39    selected_folder: Option<String>,
40    win: WindowHandle,
41    folder_panel: ContainerHandle,
42    file_panel: ContainerHandle,
43    folders: Vec<String>,
44    files: Vec<String>,
45    folder_items: Vec<ListItem>,
46    file_items: Vec<ListItem>,
47    ok_button: Button,
48    cancel_button: Button,
49}
50
51impl FileDialogState {
52    /// Returns the selected file name if the dialog completed successfully.
53    pub fn file_name(&self) -> &Option<String> { &self.file_name }
54
55    /// Returns `true` if the dialog window is currently open.
56    pub fn is_open(&self) -> bool { self.win.is_open() }
57
58    fn list_folders_files(p: &Path, folders: &mut Vec<String>, files: &mut Vec<String>) {
59        folders.clear();
60        files.clear();
61        if let Some(parent) = p.parent() {
62            folders.push(parent.to_string_lossy().to_string());
63        }
64        if let Ok(read_dir) = std::fs::read_dir(p) {
65            for entry in read_dir {
66                if let Ok(e) = entry {
67                    let path = e.path();
68                    if path.is_dir() {
69                        folders.push(path.to_string_lossy().to_string());
70                    } else {
71                        files.push(e.file_name().to_string_lossy().to_string())
72                    }
73                }
74            }
75        }
76    }
77
78    fn refresh_entries(&mut self) {
79        Self::list_folders_files(Path::new(&self.current_working_directory), &mut self.folders, &mut self.files);
80        self.rebuild_item_states();
81    }
82
83    fn rebuild_item_states(&mut self) {
84        let parent_path = Path::new(&self.current_working_directory)
85            .parent()
86            .map(|p| p.to_string_lossy().to_string());
87
88        self.folder_items.clear();
89        self.folder_items.reserve(self.folders.len());
90        for f in &self.folders {
91            let label = if parent_path.as_deref() == Some(f.as_str()) {
92                ".."
93            } else {
94                Path::new(f)
95                    .file_name()
96                    .and_then(|name| name.to_str())
97                    .unwrap_or(f.as_str())
98            };
99            let icon = if self.selected_folder.as_deref() == Some(f.as_str()) {
100                OPEN_FOLDER_16_ICON
101            } else {
102                CLOSED_FOLDER_16_ICON
103            };
104            let mut state = ListItem::new(label);
105            state.icon = Some(icon);
106            self.folder_items.push(state);
107        }
108
109        self.file_items.clear();
110        self.file_items.reserve(self.files.len());
111        for f in &self.files {
112            let mut state = ListItem::new(f.as_str());
113            state.icon = Some(FILE_16_ICON);
114            self.file_items.push(state);
115        }
116    }
117
118    /// Creates a new dialog window and associated panels.
119    pub fn new<R: Renderer>(ctx: &mut Context<R>) -> Self {
120        let current_working_directory = std::env::current_dir()
121            .unwrap_or_else(|_| std::path::PathBuf::from("."))
122            .to_string_lossy()
123            .to_string();
124        let mut dialog = Self {
125            current_working_directory,
126            file_name: None,
127            tmp_file_name: Textbox::new(""),
128            selected_folder: None,
129            win: ctx.new_dialog("File Dialog", Recti::new(50, 50, 500, 500)),
130            folder_panel: ctx.new_panel("folders"),
131            file_panel: ctx.new_panel("files"),
132            folders: Vec::new(),
133            files: Vec::new(),
134            folder_items: Vec::new(),
135            file_items: Vec::new(),
136            ok_button: Button::new("Ok"),
137            cancel_button: Button::new("Cancel"),
138        };
139        dialog.refresh_entries();
140        dialog
141    }
142
143    /// Marks the dialog as open for the next frame.
144    pub fn open<R: Renderer>(&mut self, ctx: &mut Context<R>) { ctx.open_dialog(&mut self.win); }
145
146    /// Renders the dialog and updates the selected file when confirmed.
147    pub fn eval<R: Renderer>(&mut self, ctx: &mut Context<R>) {
148        let mut needs_refresh = false;
149        {
150            let win = &mut self.win;
151            let folder_panel = &mut self.folder_panel;
152            let file_panel = &mut self.file_panel;
153            let folders = &self.folders;
154            let files = &self.files;
155            let folder_items = &mut self.folder_items;
156            let file_items = &mut self.file_items;
157            let current_working_directory = &mut self.current_working_directory;
158            let tmp_file_name = &mut self.tmp_file_name;
159            let selected_folder = &mut self.selected_folder;
160            let ok_button = &mut self.ok_button;
161            let cancel_button = &mut self.cancel_button;
162            let file_name = &mut self.file_name;
163
164            ctx.dialog(win, ContainerOption::NONE, WidgetBehaviourOption::NONE, |cont| {
165                let mut dialog_state = WindowState::Open;
166                let half_width = cont.body.width / 2;
167                cont.with_row(&[SizePolicy::Remainder(0)], SizePolicy::Auto, |cont| {
168                    cont.label(current_working_directory.as_str());
169                    cont.textbox_ex(tmp_file_name);
170                });
171                let left_column = if half_width > 0 {
172                    SizePolicy::Remainder(half_width - 1)
173                } else {
174                    SizePolicy::Auto
175                };
176                let top_row_widths = [left_column, SizePolicy::Remainder(0)];
177                cont.with_row(&top_row_widths, SizePolicy::Remainder(24), |cont| {
178                    cont.panel(folder_panel, ContainerOption::NONE, WidgetBehaviourOption::NONE, |container_handle| {
179                        let container = &mut container_handle.inner_mut();
180
181                        container.with_row(&[SizePolicy::Remainder(0)], SizePolicy::Auto, |container| {
182                            let mut refresh = false;
183                            for index in 0..folder_items.len() {
184                                let submitted = {
185                                    let item = &mut folder_items[index];
186                                    container.list_item(item).is_submitted()
187                                };
188                                if submitted {
189                                    if let Some(path) = folders.get(index) {
190                                        *current_working_directory = path.to_string();
191                                        *selected_folder = Some(path.to_string());
192                                    }
193                                    refresh = true;
194                                }
195                            }
196                            if refresh {
197                                needs_refresh = true;
198                            }
199                        });
200                    });
201                    cont.panel(file_panel, ContainerOption::NONE, WidgetBehaviourOption::NONE, |container_handle| {
202                        let container = &mut container_handle.inner_mut();
203
204                        container.with_row(&[SizePolicy::Remainder(0)], SizePolicy::Auto, |container| {
205                            if !file_items.is_empty() {
206                                for index in 0..file_items.len() {
207                                    let submitted = {
208                                        let item = &mut file_items[index];
209                                        container.list_item(item).is_submitted()
210                                    };
211                                    if submitted {
212                                        if let Some(name) = files.get(index) {
213                                            tmp_file_name.buf = name.to_string();
214                                        }
215                                    }
216                                }
217                            } else {
218                                container.label("No Files");
219                            }
220                        });
221                    });
222                });
223                let bottom_row_widths = [left_column, SizePolicy::Remainder(0)];
224                cont.with_row(&bottom_row_widths, SizePolicy::Remainder(0), |cont| {
225                    if cont.button(ok_button).is_submitted() {
226                        if tmp_file_name.buf.is_empty() {
227                            *file_name = None;
228                        } else {
229                            *file_name = Some(tmp_file_name.buf.clone());
230                        }
231                        dialog_state = WindowState::Closed;
232                    }
233                    if cont.button(cancel_button).is_submitted() {
234                        *file_name = None;
235                        dialog_state = WindowState::Closed;
236                    }
237                });
238                dialog_state
239            });
240        }
241
242        if needs_refresh {
243            self.refresh_entries();
244        }
245    }
246}