Skip to main content

fyrox_ui/
input.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//! input box is a window that is used to show standard confirmation/information dialogues, for example, closing a document with
22//! unsaved changes. It has a title, some text, and a fixed set of buttons (Yes, No, Cancel in different combinations). See
23//! [`InputBox`] docs for more info and usage examples.
24
25use crate::{
26    button::{Button, ButtonBuilder, ButtonMessage},
27    control_trait_proxy_impls,
28    core::{
29        algebra::Vector2, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
30        variable::InheritableVariable, visitor::prelude::*,
31    },
32    formatted_text::WrapMode,
33    grid::{Column, GridBuilder, Row},
34    message::{KeyCode, MessageData, UiMessage},
35    stack_panel::StackPanelBuilder,
36    text::{Text, TextBuilder, TextMessage},
37    text_box::{TextBox, TextBoxBuilder, TextCommitMode},
38    widget::{Widget, WidgetBuilder, WidgetMessage},
39    window::{Window, WindowAlignment, WindowBuilder, WindowMessage, WindowTitle},
40    BuildContext, Control, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
41    VerticalAlignment,
42};
43use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
44use std::ops::{Deref, DerefMut};
45
46/// A set of messages that can be used to communicate with input boxes.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum InputBoxMessage {
49    /// A message that can be used to open input box, and optionally change its title and/or text.
50    Open {
51        /// If [`Some`], the input box title will be set to the new value.
52        title: Option<String>,
53        /// If [`Some`], the input box text will be set to the new value.
54        text: Option<String>,
55        /// If [`Some`], the input box value will be set to the new value.
56        value: Option<String>,
57    },
58    /// A message that can be used to close a input box with some result. It can also be read to get the changes
59    /// from the UI. See [`InputBox`] docs for examples.
60    Close(InputBoxResult),
61}
62impl MessageData for InputBoxMessage {}
63
64impl InputBoxMessage {
65    pub fn open_as_is() -> Self {
66        Self::Open {
67            title: None,
68            text: None,
69            value: None,
70        }
71    }
72}
73
74/// A set of possible reasons why a input box was closed.
75#[derive(Clone, PartialOrd, PartialEq, Ord, Eq, Hash, Debug)]
76pub enum InputBoxResult {
77    /// `Ok` button was pressed.
78    Ok(String),
79    /// `Cancel` button was pressed.
80    Cancel,
81}
82
83/// Input box is a window that is used to show standard input dialogues, for example, a rename dialog.
84/// It has a title, some description text, input field and `Ok` + `Cancel` buttons.
85///
86/// ## Styling
87///
88/// There's no way to change the style of the input box, nor add some widgets to it. If you need a
89/// custom input box, then you need to create your own widget. This input box is meant to be used as
90/// a standard dialog box for standard situations in the UI.
91#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider, TypeUuidProvider)]
92#[type_uuid(id = "6b7b6b82-939b-4f98-9bb9-9bd19ce68b21")]
93#[reflect(derived_type = "UiNode")]
94pub struct InputBox {
95    /// Base window of the input box.
96    #[component(include)]
97    pub window: Window,
98    /// A handle of `Ok`/`Yes` buttons.
99    pub ok: InheritableVariable<Handle<Button>>,
100    /// A handle of `Cancel` button.
101    pub cancel: InheritableVariable<Handle<Button>>,
102    /// A handle of text widget.
103    pub text: InheritableVariable<Handle<Text>>,
104    pub value_box: InheritableVariable<Handle<TextBox>>,
105    pub value: String,
106}
107
108impl ConstructorProvider<UiNode, UserInterface> for InputBox {
109    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
110        GraphNodeConstructor::new::<Self>()
111            .with_variant("Input Box", |ui| {
112                InputBoxBuilder::new(WindowBuilder::new(
113                    WidgetBuilder::new().with_name("Input Box"),
114                ))
115                .build(&mut ui.build_ctx())
116                .to_base()
117                .into()
118            })
119            .with_group("Input")
120    }
121}
122
123impl Deref for InputBox {
124    type Target = Widget;
125
126    fn deref(&self) -> &Self::Target {
127        &self.window
128    }
129}
130
131impl DerefMut for InputBox {
132    fn deref_mut(&mut self) -> &mut Self::Target {
133        &mut self.window
134    }
135}
136
137impl InputBox {
138    fn close_ok(&self, ui: &UserInterface) {
139        ui.send(
140            self.handle(),
141            InputBoxMessage::Close(InputBoxResult::Ok(self.value.clone())),
142        );
143    }
144}
145
146impl Control for InputBox {
147    control_trait_proxy_impls!(window);
148
149    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
150        self.window.handle_routed_message(ui, message);
151
152        if let Some(ButtonMessage::Click) = message.data_from(*self.ok) {
153            self.close_ok(ui);
154        } else if let Some(ButtonMessage::Click) = message.data_from(*self.cancel) {
155            ui.send(
156                self.handle(),
157                InputBoxMessage::Close(InputBoxResult::Cancel),
158            );
159        } else if let Some(msg) = message.data_for::<InputBoxMessage>(self.handle) {
160            match msg {
161                InputBoxMessage::Open { title, text, value } => {
162                    if let Some(title) = title {
163                        ui.send(
164                            self.handle(),
165                            WindowMessage::Title(WindowTitle::text(title.clone())),
166                        );
167                    }
168
169                    if let Some(text) = text {
170                        ui.send(*self.text, TextMessage::Text(text.clone()));
171                    }
172
173                    if let Some(value) = value {
174                        ui.send(*self.value_box, TextMessage::Text(value.clone()));
175                    }
176
177                    ui.send(
178                        self.handle(),
179                        WindowMessage::Open {
180                            alignment: WindowAlignment::Center,
181                            modal: true,
182                            focus_content: false,
183                        },
184                    );
185
186                    ui.send(*self.value_box, WidgetMessage::Focus);
187
188                    ui.try_send_response(message);
189                }
190                InputBoxMessage::Close(_) => {
191                    // Translate input box message into window message.
192                    ui.send(self.handle(), WindowMessage::Close);
193
194                    ui.try_send_response(message);
195                }
196            }
197        } else if let Some(TextMessage::Text(text)) = message.data_from(*self.value_box) {
198            self.value = text.clone();
199        } else if let Some(WidgetMessage::KeyDown(code)) = message.data() {
200            if matches!(*code, KeyCode::Enter | KeyCode::NumpadEnter) {
201                self.close_ok(ui);
202            }
203        }
204    }
205}
206
207/// Creates [`InputBox`] widgets and adds them to the user interface.
208pub struct InputBoxBuilder<'b> {
209    window_builder: WindowBuilder,
210    text: &'b str,
211    value: String,
212}
213
214impl<'b> InputBoxBuilder<'b> {
215    /// Creates new builder instance. `window_builder` could be used to customize the look of your input box.
216    pub fn new(window_builder: WindowBuilder) -> Self {
217        Self {
218            window_builder,
219            text: "",
220            value: Default::default(),
221        }
222    }
223
224    /// Sets a desired text of the input box.
225    pub fn with_text(mut self, text: &'b str) -> Self {
226        self.text = text;
227        self
228    }
229
230    /// Sets the desired value of the input box.
231    pub fn with_value(mut self, value: String) -> Self {
232        self.value = value;
233        self
234    }
235
236    /// Finished input box building and adds it to the user interface.
237    pub fn build(mut self, ctx: &mut BuildContext) -> Handle<InputBox> {
238        let ok;
239        let cancel;
240        let text;
241        let value_box;
242        let content = GridBuilder::new(
243            WidgetBuilder::new()
244                .with_child({
245                    text =
246                        TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(4.0)))
247                            .with_text(self.text)
248                            .with_wrap(WrapMode::Word)
249                            .build(ctx);
250                    text
251                })
252                .with_child({
253                    value_box = TextBoxBuilder::new(
254                        WidgetBuilder::new()
255                            .with_margin(Thickness::top(4.0))
256                            .with_tab_index(Some(0))
257                            .on_row(1)
258                            .with_min_size(Vector2::new(f32::INFINITY, 24.0)),
259                    )
260                    .with_vertical_text_alignment(VerticalAlignment::Center)
261                    .with_text_commit_mode(TextCommitMode::Immediate)
262                    .with_text(&self.value)
263                    .build(ctx);
264                    value_box
265                })
266                .with_child(
267                    StackPanelBuilder::new(
268                        WidgetBuilder::new()
269                            .with_margin(Thickness::top(4.0))
270                            .with_horizontal_alignment(HorizontalAlignment::Right)
271                            .on_row(2)
272                            .with_child({
273                                ok = ButtonBuilder::new(
274                                    WidgetBuilder::new()
275                                        .with_tab_index(Some(1))
276                                        .with_margin(Thickness::uniform(1.0))
277                                        .with_width(80.0)
278                                        .with_horizontal_alignment(HorizontalAlignment::Center),
279                                )
280                                .with_text("OK")
281                                .build(ctx);
282                                ok
283                            })
284                            .with_child({
285                                cancel = ButtonBuilder::new(
286                                    WidgetBuilder::new()
287                                        .with_tab_index(Some(2))
288                                        .with_margin(Thickness::uniform(1.0))
289                                        .with_width(80.0)
290                                        .with_horizontal_alignment(HorizontalAlignment::Center),
291                                )
292                                .with_text("Cancel")
293                                .build(ctx);
294                                cancel
295                            }),
296                    )
297                    .with_orientation(Orientation::Horizontal)
298                    .build(ctx),
299                )
300                .with_margin(Thickness::uniform(4.0)),
301        )
302        .add_row(Row::stretch())
303        .add_row(Row::auto())
304        .add_row(Row::strict(30.0))
305        .add_column(Column::stretch())
306        .build(ctx);
307
308        if self.window_builder.widget_builder.min_size.is_none() {
309            self.window_builder.widget_builder.min_size = Some(Vector2::new(200.0, 100.0));
310        }
311
312        self.window_builder.widget_builder.handle_os_events = true;
313
314        let input_box = InputBox {
315            window: self.window_builder.with_content(content).build_window(ctx),
316            ok: ok.into(),
317            cancel: cancel.into(),
318            text: text.into(),
319            value_box: value_box.into(),
320            value: self.value,
321        };
322
323        ctx.add(input_box)
324    }
325}