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}