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