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