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