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::<MessageBoxMessage>() {
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 }
273 }
274 }
275}
276
277/// Creates [`MessageBox`] widgets and adds them to the user interface.
278pub struct MessageBoxBuilder<'b> {
279 window_builder: WindowBuilder,
280 buttons: MessageBoxButtons,
281 text: &'b str,
282}
283
284impl<'b> MessageBoxBuilder<'b> {
285 /// Creates new builder instance. `window_builder` could be used to customize the look of your message box.
286 pub fn new(window_builder: WindowBuilder) -> Self {
287 Self {
288 window_builder,
289 buttons: MessageBoxButtons::Ok,
290 text: "",
291 }
292 }
293
294 /// Sets a desired text of the message box.
295 pub fn with_text(mut self, text: &'b str) -> Self {
296 self.text = text;
297 self
298 }
299
300 /// Sets a desired set of buttons of the message box.
301 pub fn with_buttons(mut self, buttons: MessageBoxButtons) -> Self {
302 self.buttons = buttons;
303 self
304 }
305
306 /// Finished message box building and adds it to the user interface.
307 pub fn build(mut self, ctx: &mut BuildContext) -> Handle<MessageBox> {
308 let ok_yes;
309 let mut no = Default::default();
310 let mut cancel = Default::default();
311 let text;
312 let content = match self.buttons {
313 MessageBoxButtons::Ok => GridBuilder::new(
314 WidgetBuilder::new()
315 .with_child({
316 text = TextBuilder::new(
317 WidgetBuilder::new().with_margin(Thickness::uniform(4.0)),
318 )
319 .with_text(self.text)
320 .with_wrap(WrapMode::Word)
321 .build(ctx);
322 text
323 })
324 .with_child({
325 ok_yes = ButtonBuilder::new(
326 WidgetBuilder::new()
327 .with_margin(Thickness::uniform(1.0))
328 .with_width(80.0)
329 .on_row(1)
330 .with_horizontal_alignment(HorizontalAlignment::Center),
331 )
332 .with_text("OK")
333 .build(ctx);
334 ok_yes
335 })
336 .with_margin(Thickness::uniform(5.0)),
337 )
338 .add_row(Row::stretch())
339 .add_row(Row::strict(25.0))
340 .add_column(Column::stretch())
341 .build(ctx),
342 MessageBoxButtons::YesNo => GridBuilder::new(
343 WidgetBuilder::new()
344 .with_child({
345 text = TextBuilder::new(WidgetBuilder::new())
346 .with_text(self.text)
347 .with_wrap(WrapMode::Word)
348 .build(ctx);
349 text
350 })
351 .with_child(
352 StackPanelBuilder::new(
353 WidgetBuilder::new()
354 .with_horizontal_alignment(HorizontalAlignment::Right)
355 .on_row(1)
356 .with_child({
357 ok_yes = ButtonBuilder::new(
358 WidgetBuilder::new()
359 .with_width(80.0)
360 .with_margin(Thickness::uniform(1.0)),
361 )
362 .with_text("Yes")
363 .build(ctx);
364 ok_yes
365 })
366 .with_child({
367 no = ButtonBuilder::new(
368 WidgetBuilder::new()
369 .with_width(80.0)
370 .with_margin(Thickness::uniform(1.0)),
371 )
372 .with_text("No")
373 .build(ctx);
374 no
375 }),
376 )
377 .with_orientation(Orientation::Horizontal)
378 .build(ctx),
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::YesNoCancel => 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 .with_child({
421 cancel = ButtonBuilder::new(
422 WidgetBuilder::new()
423 .with_width(80.0)
424 .with_margin(Thickness::uniform(1.0)),
425 )
426 .with_text("Cancel")
427 .build(ctx);
428 cancel
429 }),
430 )
431 .with_orientation(Orientation::Horizontal)
432 .build(ctx),
433 )
434 .with_margin(Thickness::uniform(5.0)),
435 )
436 .add_row(Row::stretch())
437 .add_row(Row::strict(25.0))
438 .add_column(Column::stretch())
439 .build(ctx),
440 };
441
442 if self.window_builder.widget_builder.min_size.is_none() {
443 self.window_builder.widget_builder.min_size = Some(Vector2::new(200.0, 100.0));
444 }
445
446 self.window_builder.widget_builder.handle_os_events = true;
447
448 let is_open = self.window_builder.open;
449
450 let message_box = MessageBox {
451 buttons: self.buttons.into(),
452 window: self.window_builder.with_content(content).build_window(ctx),
453 ok_yes: ok_yes.into(),
454 no: no.into(),
455 cancel: cancel.into(),
456 text: text.into(),
457 };
458
459 let handle = ctx.add(message_box);
460
461 if is_open {
462 // We must restrict picking because the message box is modal.
463 ctx.push_picking_restriction(RestrictionEntry {
464 handle: handle.to_base(),
465 stop: true,
466 });
467 }
468
469 handle.to_variant()
470 }
471}
472
473#[cfg(test)]
474mod test {
475 use crate::navigation::NavigationLayerBuilder;
476 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
477
478 #[test]
479 fn test_deletion() {
480 test_widget_deletion(|ctx| NavigationLayerBuilder::new(WidgetBuilder::new()).build(ctx));
481 }
482}