fyrox_ui/file_browser/
selector.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21use crate::{
22    button::{ButtonBuilder, ButtonMessage},
23    core::{
24        algebra::Vector2, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
25        visitor::prelude::*,
26    },
27    define_constructor, define_widget_deref,
28    draw::DrawingContext,
29    file_browser::{FileBrowser, FileBrowserBuilder, FileBrowserMessage, FileBrowserMode, Filter},
30    grid::{Column, GridBuilder, Row},
31    message::{MessageDirection, OsEvent, UiMessage},
32    stack_panel::StackPanelBuilder,
33    text::TextMessage,
34    text_box::TextBoxBuilder,
35    widget::{Widget, WidgetBuilder, WidgetMessage},
36    window::{Window, WindowBuilder, WindowMessage, WindowTitle},
37    BuildContext, Control, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
38    VerticalAlignment,
39};
40use fyrox_core::uuid_provider;
41use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
42use fyrox_graph::BaseSceneGraph;
43use std::{
44    ops::{Deref, DerefMut},
45    path::{Path, PathBuf},
46};
47
48#[derive(Debug, Clone, PartialEq)]
49pub enum FileSelectorMessage {
50    Root(Option<PathBuf>),
51    Path(PathBuf),
52    Commit(PathBuf),
53    FocusCurrentPath,
54    Cancel,
55    Filter(Option<Filter>),
56}
57
58impl FileSelectorMessage {
59    define_constructor!(FileSelectorMessage:Commit => fn commit(PathBuf), layout: false);
60    define_constructor!(FileSelectorMessage:Root => fn root(Option<PathBuf>), layout: false);
61    define_constructor!(FileSelectorMessage:Path => fn path(PathBuf), layout: false);
62    define_constructor!(FileSelectorMessage:Cancel => fn cancel(), layout: false);
63    define_constructor!(FileSelectorMessage:FocusCurrentPath => fn focus_current_path(), layout: false);
64    define_constructor!(FileSelectorMessage:Filter => fn filter(Option<Filter>), layout: false);
65}
66
67/// File selector is a modal window that allows you to select a file (or directory) and commit or
68/// cancel selection.
69#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
70pub struct FileSelector {
71    #[component(include)]
72    pub window: Window,
73    pub browser: Handle<UiNode>,
74    pub ok: Handle<UiNode>,
75    pub cancel: Handle<UiNode>,
76}
77
78impl ConstructorProvider<UiNode, UserInterface> for FileSelector {
79    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
80        GraphNodeConstructor::new::<Self>()
81            .with_variant("File Selector", |ui| {
82                FileSelectorBuilder::new(WindowBuilder::new(
83                    WidgetBuilder::new().with_name("File Selector"),
84                ))
85                .build(&mut ui.build_ctx())
86                .into()
87            })
88            .with_group("File System")
89    }
90}
91
92impl Deref for FileSelector {
93    type Target = Widget;
94
95    fn deref(&self) -> &Self::Target {
96        &self.window
97    }
98}
99
100impl DerefMut for FileSelector {
101    fn deref_mut(&mut self) -> &mut Self::Target {
102        &mut self.window
103    }
104}
105
106uuid_provider!(FileSelector = "878b2220-03e6-4a50-a97d-3a8e5397b6cb");
107
108// File selector extends Window widget so it delegates most of calls
109// to inner window.
110impl Control for FileSelector {
111    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
112        self.window.measure_override(ui, available_size)
113    }
114
115    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
116        self.window.arrange_override(ui, final_size)
117    }
118
119    fn draw(&self, drawing_context: &mut DrawingContext) {
120        self.window.draw(drawing_context)
121    }
122
123    fn update(&mut self, dt: f32, ui: &mut UserInterface) {
124        self.window.update(dt, ui);
125    }
126
127    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
128        self.window.handle_routed_message(ui, message);
129
130        if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
131            if message.destination() == self.ok {
132                let path = ui
133                    .node(self.browser)
134                    .cast::<FileBrowser>()
135                    .expect("self.browser must be FileBrowser")
136                    .path
137                    .clone();
138
139                ui.send_message(FileSelectorMessage::commit(
140                    self.handle,
141                    MessageDirection::ToWidget,
142                    path,
143                ));
144            } else if message.destination() == self.cancel {
145                ui.send_message(FileSelectorMessage::cancel(
146                    self.handle,
147                    MessageDirection::ToWidget,
148                ))
149            }
150        } else if let Some(msg) = message.data::<FileSelectorMessage>() {
151            if message.destination() == self.handle {
152                match msg {
153                    FileSelectorMessage::Commit(_) | FileSelectorMessage::Cancel => ui
154                        .send_message(WindowMessage::close(
155                            self.handle,
156                            MessageDirection::ToWidget,
157                        )),
158                    FileSelectorMessage::Path(path) => ui.send_message(FileBrowserMessage::path(
159                        self.browser,
160                        MessageDirection::ToWidget,
161                        path.clone(),
162                    )),
163                    FileSelectorMessage::Root(root) => {
164                        ui.send_message(FileBrowserMessage::root(
165                            self.browser,
166                            MessageDirection::ToWidget,
167                            root.clone(),
168                        ));
169                    }
170                    FileSelectorMessage::Filter(filter) => {
171                        ui.send_message(FileBrowserMessage::filter(
172                            self.browser,
173                            MessageDirection::ToWidget,
174                            filter.clone(),
175                        ));
176                    }
177                    FileSelectorMessage::FocusCurrentPath => {
178                        ui.send_message(FileBrowserMessage::focus_current_path(
179                            self.browser,
180                            MessageDirection::ToWidget,
181                        ));
182                    }
183                }
184            }
185        }
186    }
187
188    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
189        self.window.preview_message(ui, message);
190    }
191
192    fn handle_os_event(
193        &mut self,
194        self_handle: Handle<UiNode>,
195        ui: &mut UserInterface,
196        event: &OsEvent,
197    ) {
198        self.window.handle_os_event(self_handle, ui, event);
199    }
200}
201
202pub struct FileSelectorBuilder {
203    window_builder: WindowBuilder,
204    filter: Option<Filter>,
205    mode: FileBrowserMode,
206    path: PathBuf,
207    root: Option<PathBuf>,
208}
209
210impl FileSelectorBuilder {
211    pub fn new(window_builder: WindowBuilder) -> Self {
212        Self {
213            window_builder,
214            filter: None,
215            mode: FileBrowserMode::Open,
216            path: "./".into(),
217            root: None,
218        }
219    }
220
221    pub fn with_filter(mut self, filter: Filter) -> Self {
222        self.filter = Some(filter);
223        self
224    }
225
226    pub fn with_path<P: AsRef<Path>>(mut self, path: P) -> Self {
227        path.as_ref().clone_into(&mut self.path);
228        self
229    }
230
231    pub fn with_mode(mut self, mode: FileBrowserMode) -> Self {
232        self.mode = mode;
233        self
234    }
235
236    pub fn with_root(mut self, root: PathBuf) -> Self {
237        self.root = Some(root);
238        self
239    }
240
241    pub fn build(mut self, ctx: &mut BuildContext) -> Handle<UiNode> {
242        let browser;
243        let ok;
244        let cancel;
245
246        if self.window_builder.title.is_none() {
247            self.window_builder.title = Some(WindowTitle::text("Select File"));
248        }
249
250        let window = self
251            .window_builder
252            .with_content(
253                GridBuilder::new(
254                    WidgetBuilder::new()
255                        .with_child(
256                            StackPanelBuilder::new(
257                                WidgetBuilder::new()
258                                    .with_margin(Thickness::uniform(1.0))
259                                    .with_horizontal_alignment(HorizontalAlignment::Right)
260                                    .on_column(0)
261                                    .on_row(1)
262                                    .with_child({
263                                        ok = ButtonBuilder::new(
264                                            WidgetBuilder::new()
265                                                .with_tab_index(Some(1))
266                                                .with_margin(Thickness::uniform(1.0))
267                                                .with_width(100.0)
268                                                .with_height(30.0),
269                                        )
270                                        .with_text(match &self.mode {
271                                            FileBrowserMode::Open => "Open",
272                                            FileBrowserMode::Save { .. } => "Save",
273                                        })
274                                        .build(ctx);
275                                        ok
276                                    })
277                                    .with_child({
278                                        cancel = ButtonBuilder::new(
279                                            WidgetBuilder::new()
280                                                .with_tab_index(Some(2))
281                                                .with_margin(Thickness::uniform(1.0))
282                                                .with_width(100.0)
283                                                .with_height(30.0),
284                                        )
285                                        .with_text("Cancel")
286                                        .build(ctx);
287                                        cancel
288                                    }),
289                            )
290                            .with_orientation(Orientation::Horizontal)
291                            .build(ctx),
292                        )
293                        .with_child({
294                            browser = FileBrowserBuilder::new(
295                                WidgetBuilder::new().on_column(0).with_tab_index(Some(0)),
296                            )
297                            .with_mode(self.mode)
298                            .with_opt_filter(self.filter)
299                            .with_path(self.path)
300                            .with_opt_root(self.root)
301                            .build(ctx);
302                            browser
303                        }),
304                )
305                .add_column(Column::stretch())
306                .add_row(Row::stretch())
307                .add_row(Row::auto())
308                .build(ctx),
309            )
310            .build_window(ctx);
311
312        let file_selector = FileSelector {
313            window,
314            browser,
315            ok,
316            cancel,
317        };
318
319        ctx.add_node(UiNode::new(file_selector))
320    }
321}
322
323#[derive(Debug, Clone, PartialEq)]
324pub enum FileSelectorFieldMessage {
325    Path(PathBuf),
326}
327
328impl FileSelectorFieldMessage {
329    define_constructor!(FileSelectorFieldMessage:Path => fn path(PathBuf), layout: false);
330}
331
332#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
333pub struct FileSelectorField {
334    widget: Widget,
335    path: PathBuf,
336    path_field: Handle<UiNode>,
337    select: Handle<UiNode>,
338    file_selector: Handle<UiNode>,
339}
340
341impl ConstructorProvider<UiNode, UserInterface> for FileSelectorField {
342    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
343        GraphNodeConstructor::new::<Self>()
344            .with_variant("File Selector Field", |ui| {
345                FileSelectorFieldBuilder::new(WidgetBuilder::new().with_name("File Selector Field"))
346                    .build(&mut ui.build_ctx())
347                    .into()
348            })
349            .with_group("File System")
350    }
351}
352
353define_widget_deref!(FileSelectorField);
354
355uuid_provider!(FileSelectorField = "2dbda730-8a60-4f62-aee8-2ff0ccd15bf2");
356
357impl Control for FileSelectorField {
358    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
359        self.widget.handle_routed_message(ui, message);
360
361        if let Some(TextMessage::Text(text)) = message.data() {
362            if message.destination() == self.path_field
363                && message.direction() == MessageDirection::FromWidget
364                && Path::new(text.as_str()) != self.path
365            {
366                ui.send_message(FileSelectorFieldMessage::path(
367                    self.handle,
368                    MessageDirection::ToWidget,
369                    text.into(),
370                ));
371            }
372        } else if let Some(ButtonMessage::Click) = message.data() {
373            if message.destination() == self.select {
374                let file_selector = FileSelectorBuilder::new(
375                    WindowBuilder::new(WidgetBuilder::new().with_width(300.0).with_height(400.0))
376                        .open(false)
377                        .can_minimize(false),
378                )
379                .with_path(self.path.clone())
380                .with_root(std::env::current_dir().unwrap_or_default())
381                .with_mode(FileBrowserMode::Open)
382                .build(&mut ui.build_ctx());
383
384                self.file_selector = file_selector;
385
386                ui.send_message(WindowMessage::open_modal(
387                    file_selector,
388                    MessageDirection::ToWidget,
389                    true,
390                    true,
391                ));
392            }
393        } else if let Some(FileSelectorFieldMessage::Path(new_path)) = message.data() {
394            if message.destination() == self.handle
395                && message.direction() == MessageDirection::ToWidget
396                && &self.path != new_path
397            {
398                self.path.clone_from(new_path);
399                ui.send_message(TextMessage::text(
400                    self.path_field,
401                    MessageDirection::ToWidget,
402                    self.path.to_string_lossy().to_string(),
403                ));
404
405                ui.send_message(message.reverse());
406            }
407        }
408    }
409
410    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
411        if let Some(FileSelectorMessage::Commit(new_path)) = message.data() {
412            if message.destination() == self.file_selector {
413                ui.send_message(FileSelectorFieldMessage::path(
414                    self.handle,
415                    MessageDirection::ToWidget,
416                    new_path.clone(),
417                ));
418            }
419        } else if let Some(WindowMessage::Close) = message.data() {
420            if message.destination() == self.file_selector {
421                ui.send_message(WidgetMessage::remove(
422                    self.file_selector,
423                    MessageDirection::ToWidget,
424                ));
425            }
426        }
427    }
428}
429
430pub struct FileSelectorFieldBuilder {
431    widget_builder: WidgetBuilder,
432    path: PathBuf,
433}
434
435impl FileSelectorFieldBuilder {
436    pub fn new(widget_builder: WidgetBuilder) -> Self {
437        Self {
438            widget_builder,
439            path: Default::default(),
440        }
441    }
442
443    pub fn with_path(mut self, path: PathBuf) -> Self {
444        self.path = path;
445        self
446    }
447
448    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
449        let select;
450        let path_field;
451        let field = FileSelectorField {
452            widget: self
453                .widget_builder
454                .with_preview_messages(true)
455                .with_child(
456                    GridBuilder::new(
457                        WidgetBuilder::new()
458                            .with_child({
459                                path_field = TextBoxBuilder::new(WidgetBuilder::new().on_column(0))
460                                    .with_text(self.path.to_string_lossy())
461                                    .with_vertical_text_alignment(VerticalAlignment::Center)
462                                    .build(ctx);
463                                path_field
464                            })
465                            .with_child({
466                                select = ButtonBuilder::new(
467                                    WidgetBuilder::new().on_column(1).with_width(25.0),
468                                )
469                                .with_text("...")
470                                .build(ctx);
471                                select
472                            }),
473                    )
474                    .add_row(Row::stretch())
475                    .add_column(Column::stretch())
476                    .add_column(Column::auto())
477                    .build(ctx),
478                )
479                .build(ctx),
480            path: self.path,
481            path_field,
482            select,
483            file_selector: Default::default(),
484        };
485
486        ctx.add_node(UiNode::new(field))
487    }
488}
489
490#[cfg(test)]
491mod test {
492    use crate::file_browser::FileSelectorBuilder;
493    use crate::window::WindowBuilder;
494    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
495
496    #[test]
497    fn test_deletion() {
498        test_widget_deletion(|ctx| {
499            FileSelectorBuilder::new(WindowBuilder::new(WidgetBuilder::new())).build(ctx)
500        });
501    }
502}