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