Skip to main content

fyrox_ui/
messagebox.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//! Message 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//! [`MessageBox`] docs for more info and usage examples.
24
25#![warn(missing_docs)]
26
27use crate::{
28    button::{ButtonBuilder, ButtonMessage},
29    control_trait_proxy_impls,
30    core::{
31        algebra::Vector2, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
32        visitor::prelude::*,
33    },
34    formatted_text::WrapMode,
35    grid::{Column, GridBuilder, Row},
36    message::UiMessage,
37    stack_panel::StackPanelBuilder,
38    text::{TextBuilder, TextMessage},
39    widget::{Widget, WidgetBuilder},
40    window::{Window, WindowBuilder, WindowMessage, WindowTitle},
41    BuildContext, Control, HorizontalAlignment, Orientation, RestrictionEntry, Thickness, UiNode,
42    UserInterface,
43};
44
45use crate::button::Button;
46use crate::message::MessageData;
47use crate::text::Text;
48use crate::window::WindowAlignment;
49use fyrox_core::uuid_provider;
50use fyrox_core::variable::InheritableVariable;
51use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
52use std::ops::{Deref, DerefMut};
53
54/// A set of messages that can be used to communicate with message boxes.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum MessageBoxMessage {
57    /// A message that can be used to open message box, and optionally change its title and/or text.
58    Open {
59        /// If [`Some`], a message box title will be set to the new value.
60        title: Option<String>,
61        /// If [`Some`], a message box text will be set to the new value.
62        text: Option<String>,
63    },
64    /// A message that can be used to close a message box with some result. It can also be read to get the changes
65    /// from the UI. See [`MessageBox`] docs for examples.
66    Close(MessageBoxResult),
67}
68impl MessageData for MessageBoxMessage {}
69
70/// A set of possible reasons why a message box was closed.
71#[derive(Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Hash, Debug)]
72pub enum MessageBoxResult {
73    /// `Ok` button was pressed. It can be emitted only if your message box was created with [`MessageBoxButtons::Ok`].
74    Ok,
75    /// `No` button was pressed. It can be emitted only if your message box was created with [`MessageBoxButtons::YesNo`] or
76    /// [`MessageBoxButtons::YesNoCancel`].
77    No,
78    /// `Yes` button was pressed. It can be emitted only if your message box was created with [`MessageBoxButtons::YesNo`] or
79    /// [`MessageBoxButtons::YesNoCancel`].
80    Yes,
81    /// `Cancel` button was pressed. It can be emitted only if your message box was created with [`MessageBoxButtons::YesNoCancel`].
82    Cancel,
83}
84
85/// A fixed set of possible buttons in a message box.
86#[derive(Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Hash, Debug, Visit, Reflect, Default)]
87pub enum MessageBoxButtons {
88    /// Only `Ok` button. It is typically used to show a message with the results of some finished action.
89    #[default]
90    Ok,
91    /// `Yes` and `No` buttons. It is typically used to show a message to ask a user if they want to continue or not.
92    YesNo,
93    /// `Yes`, `No`, `Cancel` buttons. It is typically used to show a message to ask a user if they want to confirm action,
94    /// refuse, cancel the next action completely.
95    YesNoCancel,
96}
97
98/// Message box is a window that is used to show standard confirmation/information dialogues, for example, closing a document with
99/// unsaved changes. It has a title, some text, and a fixed set of buttons (Yes, No, Cancel in different combinations).
100///
101/// ## Examples
102///
103/// A simple message box with two buttons (Yes and No) and some text can be created like so:
104///
105/// ```rust
106/// # use fyrox_ui::{
107/// #     core::pool::Handle,
108/// #     messagebox::{MessageBoxBuilder, MessageBoxButtons},
109/// #     widget::WidgetBuilder,
110/// #     window::WindowBuilder,
111/// #     BuildContext, UiNode,
112/// # };
113/// # use fyrox_ui::messagebox::MessageBox;
114/// #
115/// fn create_message_box(ctx: &mut BuildContext) -> Handle<MessageBox> {
116///     MessageBoxBuilder::new(WindowBuilder::new(WidgetBuilder::new()))
117///         .with_buttons(MessageBoxButtons::YesNo)
118///         .with_text("Do you want to save your changes?")
119///         .build(ctx)
120/// }
121/// ```
122///
123/// To "catch" the moment when any of the buttons will be clicked, you should listen for [`MessageBoxMessage::Close`] message:
124///
125/// ```rust
126/// # use fyrox_ui::{
127/// #     core::pool::Handle,
128/// #     message::UiMessage,
129/// #     messagebox::{MessageBoxMessage, MessageBoxResult},
130/// #     UiNode,
131/// # };
132/// # fn on_ui_message(my_message_box: Handle<UiNode>, message: &UiMessage) {
133/// if message.destination() == my_message_box {
134///     if let Some(MessageBoxMessage::Close(result)) = message.data() {
135///         match result {
136///             MessageBoxResult::No => {
137///                 println!("No");
138///             }
139///             MessageBoxResult::Yes => {
140///                 println!("Yes");
141///             }
142///             _ => (),
143///         }
144///     }
145/// }
146/// # }
147/// ```
148///
149/// To open an existing message box, use [`MessageBoxMessage::Open`]. You can optionally specify a new title and a text for the
150/// message box:
151///
152/// ```rust
153/// # use fyrox_ui::{
154/// #     core::pool::Handle, message::MessageDirection, messagebox::MessageBoxMessage, UiNode,
155/// #     UserInterface,
156/// # };
157/// # fn open_message_box(my_message_box: Handle<UiNode>, ui: &UserInterface) {
158/// ui.send(my_message_box, MessageBoxMessage::Open{
159///     title: Some("This is the new title".to_string()),
160///     text: Some("This is the new text".to_string()),
161/// })
162/// # }
163/// ```
164///
165/// ## Styling
166///
167/// There's no way to change the style of the message box, nor add some widgets to it. If you need a custom message box, then you
168/// need to create your own widget. This message box is meant to be used as a standard dialog box for standard situations in the UI.
169#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
170#[reflect(derived_type = "UiNode")]
171pub struct MessageBox {
172    /// Base window of the message box.
173    #[component(include)]
174    pub window: Window,
175    /// Current set of buttons of the message box.
176    pub buttons: InheritableVariable<MessageBoxButtons>,
177    /// A handle of `Ok`/`Yes` buttons.
178    pub ok_yes: InheritableVariable<Handle<Button>>,
179    /// A handle of `No` button.
180    pub no: InheritableVariable<Handle<Button>>,
181    /// A handle of `Cancel` button.
182    pub cancel: InheritableVariable<Handle<Button>>,
183    /// A handle of text widget.
184    pub text: InheritableVariable<Handle<Text>>,
185}
186
187impl ConstructorProvider<UiNode, UserInterface> for MessageBox {
188    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
189        GraphNodeConstructor::new::<Self>()
190            .with_variant("Message Box", |ui| {
191                MessageBoxBuilder::new(WindowBuilder::new(
192                    WidgetBuilder::new().with_name("Message Box"),
193                ))
194                .build(&mut ui.build_ctx())
195                .to_base()
196                .into()
197            })
198            .with_group("Input")
199    }
200}
201
202impl Deref for MessageBox {
203    type Target = Widget;
204
205    fn deref(&self) -> &Self::Target {
206        &self.window
207    }
208}
209
210impl DerefMut for MessageBox {
211    fn deref_mut(&mut self) -> &mut Self::Target {
212        &mut self.window
213    }
214}
215
216uuid_provider!(MessageBox = "b14c0012-4383-45cf-b9a1-231415d95373");
217
218// Message box extends Window widget so it delegates most of the calls
219//  to the inner window.
220impl Control for MessageBox {
221    control_trait_proxy_impls!(window);
222
223    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
224        self.window.handle_routed_message(ui, message);
225
226        if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
227            if message.destination() == *self.ok_yes {
228                let result = match *self.buttons {
229                    MessageBoxButtons::Ok => MessageBoxResult::Ok,
230                    MessageBoxButtons::YesNo => MessageBoxResult::Yes,
231                    MessageBoxButtons::YesNoCancel => MessageBoxResult::Yes,
232                };
233                ui.send(self.handle, MessageBoxMessage::Close(result));
234            } else if message.destination() == *self.cancel {
235                ui.send(
236                    self.handle(),
237                    MessageBoxMessage::Close(MessageBoxResult::Cancel),
238                );
239            } else if message.destination() == *self.no {
240                ui.send(
241                    self.handle(),
242                    MessageBoxMessage::Close(MessageBoxResult::No),
243                );
244            }
245        } else if let Some(msg) = message.data_for::<MessageBoxMessage>(self.handle) {
246            match msg {
247                MessageBoxMessage::Open { title, text } => {
248                    if let Some(title) = title {
249                        ui.send(
250                            self.handle(),
251                            WindowMessage::Title(WindowTitle::text(title.clone())),
252                        );
253                    }
254
255                    if let Some(text) = text {
256                        ui.send(*self.text, TextMessage::Text(text.clone()));
257                    }
258
259                    ui.send(
260                        self.handle(),
261                        WindowMessage::Open {
262                            alignment: WindowAlignment::Center,
263                            modal: true,
264                            focus_content: true,
265                        },
266                    );
267                }
268                MessageBoxMessage::Close(_) => {
269                    // Translate message box message into window message.
270                    ui.send(self.handle(), WindowMessage::Close);
271
272                    ui.try_send_response(message);
273                }
274            }
275        }
276    }
277}
278
279/// Creates [`MessageBox`] widgets and adds them to the user interface.
280pub struct MessageBoxBuilder<'b> {
281    window_builder: WindowBuilder,
282    buttons: MessageBoxButtons,
283    text: &'b str,
284}
285
286impl<'b> MessageBoxBuilder<'b> {
287    /// Creates new builder instance. `window_builder` could be used to customize the look of your message box.
288    pub fn new(window_builder: WindowBuilder) -> Self {
289        Self {
290            window_builder,
291            buttons: MessageBoxButtons::Ok,
292            text: "",
293        }
294    }
295
296    /// Sets a desired text of the message box.
297    pub fn with_text(mut self, text: &'b str) -> Self {
298        self.text = text;
299        self
300    }
301
302    /// Sets a desired set of buttons of the message box.
303    pub fn with_buttons(mut self, buttons: MessageBoxButtons) -> Self {
304        self.buttons = buttons;
305        self
306    }
307
308    /// Finished message box building and adds it to the user interface.
309    pub fn build(mut self, ctx: &mut BuildContext) -> Handle<MessageBox> {
310        let ok_yes;
311        let mut no = Default::default();
312        let mut cancel = Default::default();
313        let text;
314        let content = match self.buttons {
315            MessageBoxButtons::Ok => GridBuilder::new(
316                WidgetBuilder::new()
317                    .with_child({
318                        text = TextBuilder::new(
319                            WidgetBuilder::new().with_margin(Thickness::uniform(4.0)),
320                        )
321                        .with_text(self.text)
322                        .with_wrap(WrapMode::Word)
323                        .build(ctx);
324                        text
325                    })
326                    .with_child({
327                        ok_yes = ButtonBuilder::new(
328                            WidgetBuilder::new()
329                                .with_margin(Thickness::uniform(1.0))
330                                .with_width(80.0)
331                                .on_row(1)
332                                .with_horizontal_alignment(HorizontalAlignment::Center),
333                        )
334                        .with_text("OK")
335                        .build(ctx);
336                        ok_yes
337                    })
338                    .with_margin(Thickness::uniform(5.0)),
339            )
340            .add_row(Row::stretch())
341            .add_row(Row::strict(25.0))
342            .add_column(Column::stretch())
343            .build(ctx),
344            MessageBoxButtons::YesNo => GridBuilder::new(
345                WidgetBuilder::new()
346                    .with_child({
347                        text = TextBuilder::new(WidgetBuilder::new())
348                            .with_text(self.text)
349                            .with_wrap(WrapMode::Word)
350                            .build(ctx);
351                        text
352                    })
353                    .with_child(
354                        StackPanelBuilder::new(
355                            WidgetBuilder::new()
356                                .with_horizontal_alignment(HorizontalAlignment::Right)
357                                .on_row(1)
358                                .with_child({
359                                    ok_yes = ButtonBuilder::new(
360                                        WidgetBuilder::new()
361                                            .with_width(80.0)
362                                            .with_margin(Thickness::uniform(1.0)),
363                                    )
364                                    .with_text("Yes")
365                                    .build(ctx);
366                                    ok_yes
367                                })
368                                .with_child({
369                                    no = ButtonBuilder::new(
370                                        WidgetBuilder::new()
371                                            .with_width(80.0)
372                                            .with_margin(Thickness::uniform(1.0)),
373                                    )
374                                    .with_text("No")
375                                    .build(ctx);
376                                    no
377                                }),
378                        )
379                        .with_orientation(Orientation::Horizontal)
380                        .build(ctx),
381                    )
382                    .with_margin(Thickness::uniform(5.0)),
383            )
384            .add_row(Row::stretch())
385            .add_row(Row::strict(25.0))
386            .add_column(Column::stretch())
387            .build(ctx),
388            MessageBoxButtons::YesNoCancel => GridBuilder::new(
389                WidgetBuilder::new()
390                    .with_child({
391                        text = TextBuilder::new(WidgetBuilder::new())
392                            .with_text(self.text)
393                            .with_wrap(WrapMode::Word)
394                            .build(ctx);
395                        text
396                    })
397                    .with_child(
398                        StackPanelBuilder::new(
399                            WidgetBuilder::new()
400                                .with_horizontal_alignment(HorizontalAlignment::Right)
401                                .on_row(1)
402                                .with_child({
403                                    ok_yes = ButtonBuilder::new(
404                                        WidgetBuilder::new()
405                                            .with_width(80.0)
406                                            .with_margin(Thickness::uniform(1.0)),
407                                    )
408                                    .with_text("Yes")
409                                    .build(ctx);
410                                    ok_yes
411                                })
412                                .with_child({
413                                    no = ButtonBuilder::new(
414                                        WidgetBuilder::new()
415                                            .with_width(80.0)
416                                            .with_margin(Thickness::uniform(1.0)),
417                                    )
418                                    .with_text("No")
419                                    .build(ctx);
420                                    no
421                                })
422                                .with_child({
423                                    cancel = ButtonBuilder::new(
424                                        WidgetBuilder::new()
425                                            .with_width(80.0)
426                                            .with_margin(Thickness::uniform(1.0)),
427                                    )
428                                    .with_text("Cancel")
429                                    .build(ctx);
430                                    cancel
431                                }),
432                        )
433                        .with_orientation(Orientation::Horizontal)
434                        .build(ctx),
435                    )
436                    .with_margin(Thickness::uniform(5.0)),
437            )
438            .add_row(Row::stretch())
439            .add_row(Row::strict(25.0))
440            .add_column(Column::stretch())
441            .build(ctx),
442        };
443
444        if self.window_builder.widget_builder.min_size.is_none() {
445            self.window_builder.widget_builder.min_size = Some(Vector2::new(200.0, 100.0));
446        }
447
448        self.window_builder.widget_builder.handle_os_events = true;
449
450        let is_open = self.window_builder.open;
451
452        let message_box = MessageBox {
453            buttons: self.buttons.into(),
454            window: self.window_builder.with_content(content).build_window(ctx),
455            ok_yes: ok_yes.into(),
456            no: no.into(),
457            cancel: cancel.into(),
458            text: text.into(),
459        };
460
461        let handle = ctx.add(message_box);
462
463        if is_open {
464            // We must restrict picking because the message box is modal.
465            ctx.push_picking_restriction(RestrictionEntry {
466                handle: handle.to_base(),
467                stop: true,
468            });
469        }
470
471        handle.to_variant()
472    }
473}
474
475#[cfg(test)]
476mod test {
477    use crate::navigation::NavigationLayerBuilder;
478    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
479
480    #[test]
481    fn test_deletion() {
482        test_widget_deletion(|ctx| NavigationLayerBuilder::new(WidgetBuilder::new()).build(ctx));
483    }
484}