Skip to main content

fyrox_ui/
path.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
21//! Path editor is a simple widget that has a text box, that shows the current path and a "..." button, that opens a file
22//! selector. See [`PathEditor`] docs for more info and usage examples.
23
24#![warn(missing_docs)]
25
26use crate::button::Button;
27use crate::file_browser::{FileSelector, PathFilter};
28use crate::text_box::TextBox;
29use crate::{
30    button::{ButtonBuilder, ButtonMessage},
31    core::{
32        pool::Handle, reflect::prelude::*, type_traits::prelude::*, uuid_provider,
33        variable::InheritableVariable, visitor::prelude::*,
34    },
35    file_browser::{FileSelectorBuilder, FileSelectorMessage},
36    grid::{Column, GridBuilder, Row},
37    message::{MessageData, UiMessage},
38    text::TextMessage,
39    text_box::TextBoxBuilder,
40    widget::{Widget, WidgetBuilder, WidgetMessage},
41    window::{WindowAlignment, WindowBuilder, WindowMessage, WindowTitle},
42    BuildContext, Control, Thickness, UiNode, UserInterface,
43};
44use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
45use std::{path::Path, path::PathBuf};
46
47/// A set of messages for the [`PathEditor`] widget.
48#[derive(Debug, Clone, PartialEq)]
49pub enum PathEditorMessage {
50    /// A message, that is used to set new value of the editor or to receive changes from the editor.
51    Path(PathBuf),
52}
53impl MessageData for PathEditorMessage {}
54
55/// Path editor is a simple widget that has a text box, that shows the current path and a "..." button, that opens a file
56/// selector.
57///
58/// ## Examples
59///
60/// An instance of the editor could be created like so:
61///
62/// ```rust
63/// # use fyrox_ui::{
64/// #     core::pool::Handle, path::{PathEditor, PathEditorBuilder}, widget::WidgetBuilder, BuildContext, UiNode,
65/// # };
66/// # use std::path::PathBuf;
67/// #
68/// fn create_path_editor(path: PathBuf, ctx: &mut BuildContext) -> Handle<PathEditor> {
69///     PathEditorBuilder::new(WidgetBuilder::new())
70///         .with_path(path)
71///         .build(ctx)
72/// }
73/// ```
74///
75/// To receive the changes, listen to [`PathEditorMessage::Path`] and check for its direction, it should be [`crate::message::MessageDirection::FromWidget`].
76/// To set a new path value, send [`PathEditorMessage::Path`] message, but with [`crate::message::MessageDirection::ToWidget`].
77#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
78#[reflect(derived_type = "UiNode")]
79pub struct PathEditor {
80    /// Base widget of the editor.
81    pub widget: Widget,
82    /// A handle of the text field, that is used to show the current path.
83    pub text_field: InheritableVariable<Handle<TextBox>>,
84    /// A button, that opens a file selection.
85    pub select: InheritableVariable<Handle<Button>>,
86    /// The current file selector instance, could be [`Handle::NONE`] if the selector is closed.
87    pub selector: InheritableVariable<Handle<FileSelector>>,
88    /// Current path.
89    pub path: InheritableVariable<PathBuf>,
90    /// Current filter that will be used in the file browser created by clicking on `...` button.
91    pub file_types: PathFilter,
92}
93
94impl ConstructorProvider<UiNode, UserInterface> for PathEditor {
95    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
96        GraphNodeConstructor::new::<Self>()
97            .with_variant("Path Editor", |ui| {
98                PathEditorBuilder::new(WidgetBuilder::new().with_name("Path Editor"))
99                    .build(&mut ui.build_ctx())
100                    .to_base()
101                    .into()
102            })
103            .with_group("Input")
104    }
105}
106
107crate::define_widget_deref!(PathEditor);
108
109uuid_provider!(PathEditor = "51cfe7ec-ec31-4354-9578-047004b213a1");
110
111impl Control for PathEditor {
112    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
113        self.widget.handle_routed_message(ui, message);
114
115        if let Some(ButtonMessage::Click) = message.data() {
116            if message.destination() == *self.select {
117                self.selector.set_value_and_mark_modified(
118                    FileSelectorBuilder::new(
119                        WindowBuilder::new(
120                            WidgetBuilder::new().with_width(300.0).with_height(450.0),
121                        )
122                        .open(false)
123                        .with_title(WindowTitle::text("Select a Path")),
124                    )
125                    .with_filter(self.file_types.clone())
126                    .build(&mut ui.build_ctx()),
127                );
128
129                ui.send(
130                    *self.selector,
131                    FileSelectorMessage::Path((*self.path).clone()),
132                );
133                ui.send(
134                    *self.selector,
135                    WindowMessage::Open {
136                        alignment: WindowAlignment::Center,
137                        modal: true,
138                        focus_content: true,
139                    },
140                );
141                ui.send(*self.selector, FileSelectorMessage::FocusCurrentPath);
142            }
143        } else if let Some(PathEditorMessage::Path(path)) = message.data_for(self.handle) {
144            if &*self.path != path {
145                self.path.set_value_and_mark_modified(path.clone());
146
147                ui.send(
148                    *self.text_field,
149                    TextMessage::Text(path.to_string_lossy().to_string()),
150                );
151                ui.send_message(message.reverse());
152            }
153        }
154    }
155
156    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
157        if let Some(FileSelectorMessage::Commit(path)) = message.data() {
158            if message.destination() == *self.selector && &*self.path != path {
159                ui.send(*self.selector, WidgetMessage::Remove);
160                ui.send(self.handle, PathEditorMessage::Path(path.clone()));
161            }
162        }
163    }
164}
165
166/// Path editor builder creates [`PathEditor`] instances and adds them to the user interface.
167pub struct PathEditorBuilder {
168    widget_builder: WidgetBuilder,
169    path: PathBuf,
170    file_types: PathFilter,
171}
172
173impl PathEditorBuilder {
174    /// Creates new builder instance.
175    pub fn new(widget_builder: WidgetBuilder) -> Self {
176        Self {
177            widget_builder,
178            path: Default::default(),
179            file_types: Default::default(),
180        }
181    }
182
183    /// Sets the desired path.
184    pub fn with_path<P: AsRef<Path>>(mut self, path: P) -> Self {
185        path.as_ref().clone_into(&mut self.path);
186        self
187    }
188
189    /// Sets a filter that will be used in the file browser created by clicking on `...` button.
190    pub fn with_file_types(mut self, filter: PathFilter) -> Self {
191        self.file_types = filter;
192        self
193    }
194
195    /// Finishes widget building and adds it to the user interface returning a handle to the instance.
196    pub fn build(self, ctx: &mut BuildContext) -> Handle<PathEditor> {
197        let text_field;
198        let select;
199        let grid = GridBuilder::new(
200            WidgetBuilder::new()
201                .with_child({
202                    text_field = TextBoxBuilder::new(
203                        WidgetBuilder::new()
204                            .on_column(0)
205                            .with_margin(Thickness::uniform(1.0)),
206                    )
207                    .with_text(self.path.to_string_lossy())
208                    .with_editable(false)
209                    .build(ctx);
210                    text_field
211                })
212                .with_child({
213                    select = ButtonBuilder::new(
214                        WidgetBuilder::new()
215                            .on_column(1)
216                            .with_width(30.0)
217                            .with_margin(Thickness::uniform(1.0)),
218                    )
219                    .with_text("...")
220                    .build(ctx);
221                    select
222                }),
223        )
224        .add_row(Row::stretch())
225        .add_column(Column::stretch())
226        .add_column(Column::auto())
227        .build(ctx);
228
229        let path_editor = PathEditor {
230            widget: self
231                .widget_builder
232                .with_child(grid)
233                .with_preview_messages(true)
234                .build(ctx),
235            text_field: text_field.into(),
236            select: select.into(),
237            selector: Default::default(),
238            path: self.path.into(),
239            file_types: self.file_types,
240        };
241        ctx.add(path_editor)
242    }
243}
244
245#[cfg(test)]
246mod test {
247    use crate::path::PathEditorBuilder;
248    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
249
250    #[test]
251    fn test_deletion() {
252        test_widget_deletion(|ctx| PathEditorBuilder::new(WidgetBuilder::new()).build(ctx));
253    }
254}