fyrox_ui/
popup.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//! Popup is used to display other widgets in floating panel, that could lock input in self bounds. See [`Popup`] docs
22//! for more info and usage examples.
23
24#![warn(missing_docs)]
25
26use crate::style::resource::StyleResourceExt;
27use crate::style::Style;
28use crate::{
29    border::BorderBuilder,
30    core::{
31        algebra::Vector2, math::Rect, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
32        uuid_provider, variable::InheritableVariable, visitor::prelude::*,
33    },
34    define_constructor,
35    message::{ButtonState, KeyCode, MessageDirection, OsEvent, UiMessage},
36    widget::{Widget, WidgetBuilder, WidgetMessage},
37    BuildContext, Control, RestrictionEntry, Thickness, UiNode, UserInterface,
38};
39use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
40use fyrox_graph::BaseSceneGraph;
41use std::ops::{Deref, DerefMut};
42
43/// A set of messages for [`Popup`] widget.
44#[derive(Debug, Clone, PartialEq)]
45pub enum PopupMessage {
46    /// Used to open a [`Popup`] widgets. Use [`PopupMessage::open`] to create the message.
47    Open,
48    /// Used to close a [`Popup`] widgets. Use [`PopupMessage::close`] to create the message.
49    Close,
50    /// Used to change the content of a [`Popup`] widgets. Use [`PopupMessage::content`] to create the message.
51    Content(Handle<UiNode>),
52    /// Used to change popup's placement. Use [`PopupMessage::placement`] to create the message.
53    Placement(Placement),
54    /// Used to adjust position of a popup widget, so it will be on screen. Use [`PopupMessage::adjust_position`] to create
55    /// the message.
56    AdjustPosition,
57    /// Used to set the owner of a Popup. The owner will receive Event messages.
58    Owner(Handle<UiNode>),
59    /// Sent by the Popup to its owner when handling messages from the Popup's children.
60    RelayedMessage(UiMessage),
61}
62
63impl PopupMessage {
64    define_constructor!(
65        /// Creates [`PopupMessage::Open`] message.
66        PopupMessage:Open => fn open(), layout: false
67    );
68    define_constructor!(
69        /// Creates [`PopupMessage::Close`] message.
70        PopupMessage:Close => fn close(), layout: false
71    );
72    define_constructor!(
73        /// Creates [`PopupMessage::Content`] message.
74        PopupMessage:Content => fn content(Handle<UiNode>), layout: false
75    );
76    define_constructor!(
77        /// Creates [`PopupMessage::Placement`] message.
78        PopupMessage:Placement => fn placement(Placement), layout: false
79    );
80    define_constructor!(
81        /// Creates [`PopupMessage::AdjustPosition`] message.
82        PopupMessage:AdjustPosition => fn adjust_position(), layout: true
83    );
84    define_constructor!(
85        /// Creates [`PopupMessage::Owner`] message.
86        PopupMessage:Owner => fn owner(Handle<UiNode>), layout: false
87    );
88    define_constructor!(
89        /// Creates [`PopupMessage::RelayedMessage`] message.
90        PopupMessage:RelayedMessage => fn relayed_message(UiMessage), layout: false
91    );
92}
93
94/// Defines a method of popup placement.
95#[derive(Copy, Clone, PartialEq, Debug, Visit, Reflect)]
96pub enum Placement {
97    /// A popup should be placed relative to given widget at the left top corner of the widget screen bounds.
98    /// Widget handle could be [`Handle::NONE`], in this case the popup will be placed at the left top corner of the screen.
99    LeftTop(Handle<UiNode>),
100
101    /// A popup should be placed relative to given widget at the right top corner of the widget screen bounds.
102    /// Widget handle could be [`Handle::NONE`], in this case the popup will be placed at the right top corner of the screen.
103    RightTop(Handle<UiNode>),
104
105    /// A popup should be placed relative to given widget at the center of the widget screen bounds.
106    /// Widget handle could be [`Handle::NONE`], in this case the popup will be placed at the center of the screen.
107    Center(Handle<UiNode>),
108
109    /// A popup should be placed relative to given widget at the left bottom corner of the widget screen bounds.
110    /// Widget handle could be [`Handle::NONE`], in this case the popup will be placed at the left bottom corner of the screen.
111    LeftBottom(Handle<UiNode>),
112
113    /// A popup should be placed relative to given widget at the right bottom corner of the widget screen bounds.
114    /// Widget handle could be [`Handle::NONE`], in this case the popup will be placed at the right bottom corner of the screen.
115    RightBottom(Handle<UiNode>),
116
117    /// A popup should be placed at the cursor position. The widget handle could be either [`Handle::NONE`] or a handle of a
118    /// widget that is directly behind the cursor.
119    Cursor(Handle<UiNode>),
120
121    /// A popup should be placed at given screen-space position.
122    Position {
123        /// Screen-space position.
124        position: Vector2<f32>,
125
126        /// A handle of the node that is located behind the given position. Could be [`Handle::NONE`] if there is nothing behind
127        /// given position.
128        target: Handle<UiNode>,
129    },
130}
131
132impl Default for Placement {
133    fn default() -> Self {
134        Self::LeftTop(Default::default())
135    }
136}
137
138impl Placement {
139    /// Returns a handle of the node to which this placement corresponds to.
140    pub fn target(&self) -> Handle<UiNode> {
141        match self {
142            Placement::LeftTop(target)
143            | Placement::RightTop(target)
144            | Placement::Center(target)
145            | Placement::LeftBottom(target)
146            | Placement::RightBottom(target)
147            | Placement::Cursor(target)
148            | Placement::Position { target, .. } => *target,
149        }
150    }
151}
152
153/// Popup is used to display other widgets in floating panel, that could lock input in self bounds.
154///
155/// ## How to create
156///
157/// A simple popup with a button could be created using the following code:
158///
159/// ```rust
160/// # use fyrox_ui::{
161/// #     button::ButtonBuilder, core::pool::Handle, popup::PopupBuilder, widget::WidgetBuilder,
162/// #     BuildContext, UiNode,
163/// # };
164/// fn create_popup_with_button(ctx: &mut BuildContext) -> Handle<UiNode> {
165///     PopupBuilder::new(WidgetBuilder::new())
166///         .with_content(
167///             ButtonBuilder::new(WidgetBuilder::new())
168///                 .with_text("Click Me!")
169///                 .build(ctx),
170///         )
171///         .build(ctx)
172/// }
173/// ```
174///
175/// Keep in mind, that the popup is closed by default. You need to open it explicitly by sending a [`PopupMessage::Open`] to it,
176/// otherwise you won't see it:
177///
178/// ```rust
179/// # use fyrox_ui::{
180/// #     button::ButtonBuilder,
181/// #     core::pool::Handle,
182/// #     message::MessageDirection,
183/// #     popup::{Placement, PopupBuilder, PopupMessage},
184/// #     widget::WidgetBuilder,
185/// #     UiNode, UserInterface,
186/// # };
187/// fn create_popup_with_button_and_open_it(ui: &mut UserInterface) -> Handle<UiNode> {
188///     let popup = PopupBuilder::new(WidgetBuilder::new())
189///         .with_content(
190///             ButtonBuilder::new(WidgetBuilder::new())
191///                 .with_text("Click Me!")
192///                 .build(&mut ui.build_ctx()),
193///         )
194///         .build(&mut ui.build_ctx());
195///
196///     // Open the popup explicitly.
197///     ui.send_message(PopupMessage::open(popup, MessageDirection::ToWidget));
198///
199///     popup
200/// }
201/// ```
202///
203/// ## Placement
204///
205/// Since popups are usually used to show useful context-specific information (like context menus, drop-down lists, etc.), they're usually
206/// open above some other widget with specific alignment (right, left, center, etc.).
207///
208/// ```rust
209/// # use fyrox_ui::{
210/// #     button::ButtonBuilder,
211/// #     core::pool::Handle,
212/// #     message::MessageDirection,
213/// #     popup::{Placement, PopupBuilder, PopupMessage},
214/// #     widget::WidgetBuilder,
215/// #     UiNode, UserInterface,
216/// # };
217/// fn create_popup_with_button_and_open_it(ui: &mut UserInterface) -> Handle<UiNode> {
218///     let popup = PopupBuilder::new(WidgetBuilder::new())
219///         .with_content(
220///             ButtonBuilder::new(WidgetBuilder::new())
221///                 .with_text("Click Me!")
222///                 .build(&mut ui.build_ctx()),
223///         )
224///         // Set the placement. For simplicity it is just a cursor position with Handle::NONE as placement target.
225///         .with_placement(Placement::Cursor(Handle::NONE))
226///         .build(&mut ui.build_ctx());
227///
228///     // Open the popup explicitly at the current placement.
229///     ui.send_message(PopupMessage::open(popup, MessageDirection::ToWidget));
230///
231///     popup
232/// }
233/// ```
234///
235/// The example uses [`Placement::Cursor`] with [`Handle::NONE`] placement target for simplicity reasons, however in
236/// the real-world usages this handle must be a handle of some widget that is located under the popup. It is very
237/// important to specify it correctly, otherwise you will lost the built-in ability to fetch the actual placement target.
238/// For example, imagine that you're building your own custom [`crate::dropdown_list::DropdownList`] widget and the popup
239/// is used to display content of the list. In this case you could specify the placement target like this:
240///
241/// ```rust
242/// # use fyrox_ui::{
243/// #     button::ButtonBuilder,
244/// #     core::pool::Handle,
245/// #     message::MessageDirection,
246/// #     popup::{Placement, PopupBuilder, PopupMessage},
247/// #     widget::WidgetBuilder,
248/// #     UiNode, UserInterface,
249/// # };
250/// fn create_popup_with_button_and_open_it(
251///     dropdown_list: Handle<UiNode>,
252///     ui: &mut UserInterface,
253/// ) -> Handle<UiNode> {
254///     let popup = PopupBuilder::new(WidgetBuilder::new())
255///         .with_content(
256///             ButtonBuilder::new(WidgetBuilder::new())
257///                 .with_text("Click Me!")
258///                 .build(&mut ui.build_ctx()),
259///         )
260///         // Set the placement to the dropdown list.
261///         .with_placement(Placement::LeftBottom(dropdown_list))
262///         .build(&mut ui.build_ctx());
263///
264///     // Open the popup explicitly at the current placement.
265///     ui.send_message(PopupMessage::open(popup, MessageDirection::ToWidget));
266///
267///     popup
268/// }
269/// ```
270///
271/// In this case, the popup will open at the left bottom corner of the dropdown list automatically. Placement target is also
272/// useful to build context menus, especially for lists with multiple items. Each item in the list usually have the same context
273/// menu, and this is ideal use case for popups, since the single context menu can be shared across multiple list items. To find
274/// which item cause the context menu to open, catch [`PopupMessage::Placement`] and extract the node handle - this will be your
275/// actual item.
276///
277/// ## Opening mode
278///
279/// By default, when you click outside of your popup it will automatically close. It is pretty common behaviour in the UI, you
280/// can see it almost everytime you use context menus in various apps. There are cases when this behaviour is undesired and it
281/// can be turned off:
282///
283/// ```rust
284/// # use fyrox_ui::{
285/// #     button::ButtonBuilder, core::pool::Handle, popup::PopupBuilder, widget::WidgetBuilder,
286/// #     BuildContext, UiNode,
287/// # };
288/// fn create_popup_with_button(ctx: &mut BuildContext) -> Handle<UiNode> {
289///     PopupBuilder::new(WidgetBuilder::new())
290///         .with_content(
291///             ButtonBuilder::new(WidgetBuilder::new())
292///                 .with_text("Click Me!")
293///                 .build(ctx),
294///         )
295///         // This forces the popup to stay open when clicked outside of its bounds
296///         .stays_open(true)
297///         .build(ctx)
298/// }
299/// ```
300///
301/// ## Smart placement
302///
303/// Popup widget can automatically adjust its position to always remain on screen, which is useful for tooltips, dropdown lists,
304/// etc. To enable this option, use [`PopupBuilder::with_smart_placement`] with `true` as the first argument.
305#[derive(Default, Clone, Visit, Debug, Reflect, ComponentProvider)]
306pub struct Popup {
307    /// Base widget of the popup.
308    pub widget: Widget,
309    /// Current placement of the popup.
310    pub placement: InheritableVariable<Placement>,
311    /// A flag, that defines whether the popup will stay open if a user click outside of its bounds.
312    pub stays_open: InheritableVariable<bool>,
313    /// A flag, that defines whether the popup is open or not.
314    pub is_open: InheritableVariable<bool>,
315    /// Current content of the popup.
316    pub content: InheritableVariable<Handle<UiNode>>,
317    /// Background widget of the popup. It is used as a container for the content.
318    pub body: InheritableVariable<Handle<UiNode>>,
319    /// Smart placement prevents the popup from going outside of the screen bounds. It is usually used for tooltips,
320    /// dropdown lists, etc. to prevent the content from being outside of the screen.
321    pub smart_placement: InheritableVariable<bool>,
322    /// The destination for Event messages that relay messages from the children of this popup.
323    pub owner: Handle<UiNode>,
324}
325
326impl ConstructorProvider<UiNode, UserInterface> for Popup {
327    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
328        GraphNodeConstructor::new::<Self>()
329            .with_variant("Popup", |ui| {
330                PopupBuilder::new(WidgetBuilder::new().with_name("Popup"))
331                    .build(&mut ui.build_ctx())
332                    .into()
333            })
334            .with_group("Layout")
335    }
336}
337
338crate::define_widget_deref!(Popup);
339
340fn adjust_placement_position(
341    node_screen_bounds: Rect<f32>,
342    screen_size: Vector2<f32>,
343) -> Vector2<f32> {
344    let mut new_position = node_screen_bounds.position;
345    let right_bottom = node_screen_bounds.right_bottom_corner();
346    if right_bottom.x > screen_size.x {
347        new_position.x -= right_bottom.x - screen_size.x;
348    }
349    if right_bottom.y > screen_size.y {
350        new_position.y -= right_bottom.y - screen_size.y;
351    }
352    new_position
353}
354
355impl Popup {
356    fn left_top_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
357        ui.try_get(target)
358            .map(|n| n.screen_position())
359            .unwrap_or_default()
360    }
361
362    fn right_top_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
363        ui.try_get(target)
364            .map(|n| n.screen_position() + Vector2::new(n.actual_global_size().x, 0.0))
365            .unwrap_or_else(|| {
366                Vector2::new(ui.screen_size().x - self.widget.actual_global_size().x, 0.0)
367            })
368    }
369
370    fn center_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
371        ui.try_get(target)
372            .map(|n| n.screen_position() + n.actual_global_size().scale(0.5))
373            .unwrap_or_else(|| (ui.screen_size - self.widget.actual_global_size()).scale(0.5))
374    }
375
376    fn left_bottom_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
377        ui.try_get(target)
378            .map(|n| n.screen_position() + Vector2::new(0.0, n.actual_global_size().y))
379            .unwrap_or_else(|| {
380                Vector2::new(0.0, ui.screen_size().y - self.widget.actual_global_size().y)
381            })
382    }
383
384    fn right_bottom_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
385        ui.try_get(target)
386            .map(|n| n.screen_position() + n.actual_global_size())
387            .unwrap_or_else(|| ui.screen_size - self.widget.actual_global_size())
388    }
389}
390
391uuid_provider!(Popup = "1c641540-59eb-4ccd-a090-2173dab02245");
392
393impl Control for Popup {
394    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
395        self.widget.handle_routed_message(ui, message);
396
397        if let Some(msg) = message.data::<PopupMessage>() {
398            if message.destination() == self.handle() {
399                match msg {
400                    PopupMessage::Open => {
401                        if !*self.is_open {
402                            self.is_open.set_value_and_mark_modified(true);
403                            ui.send_message(WidgetMessage::visibility(
404                                self.handle(),
405                                MessageDirection::ToWidget,
406                                true,
407                            ));
408                            ui.push_picking_restriction(RestrictionEntry {
409                                handle: self.handle(),
410                                stop: false,
411                            });
412                            ui.send_message(WidgetMessage::topmost(
413                                self.handle(),
414                                MessageDirection::ToWidget,
415                            ));
416                            let position = match *self.placement {
417                                Placement::LeftTop(target) => self.left_top_placement(ui, target),
418                                Placement::RightTop(target) => self.right_top_placement(ui, target),
419                                Placement::Center(target) => self.center_placement(ui, target),
420                                Placement::LeftBottom(target) => {
421                                    self.left_bottom_placement(ui, target)
422                                }
423                                Placement::RightBottom(target) => {
424                                    self.right_bottom_placement(ui, target)
425                                }
426                                Placement::Cursor(_) => ui.cursor_position(),
427                                Placement::Position { position, .. } => position,
428                            };
429
430                            ui.send_message(WidgetMessage::desired_position(
431                                self.handle(),
432                                MessageDirection::ToWidget,
433                                ui.screen_to_root_canvas_space(position),
434                            ));
435                            ui.send_message(WidgetMessage::focus(
436                                if self.content.is_some() {
437                                    *self.content
438                                } else {
439                                    self.handle
440                                },
441                                MessageDirection::ToWidget,
442                            ));
443                            if *self.smart_placement {
444                                ui.send_message(PopupMessage::adjust_position(
445                                    self.handle,
446                                    MessageDirection::ToWidget,
447                                ));
448                            }
449                        }
450                    }
451                    PopupMessage::Close => {
452                        if *self.is_open {
453                            self.is_open.set_value_and_mark_modified(false);
454                            ui.send_message(WidgetMessage::visibility(
455                                self.handle(),
456                                MessageDirection::ToWidget,
457                                false,
458                            ));
459                            ui.remove_picking_restriction(self.handle());
460
461                            if let Some(top) = ui.top_picking_restriction() {
462                                ui.send_message(WidgetMessage::focus(
463                                    top.handle,
464                                    MessageDirection::ToWidget,
465                                ));
466                            }
467
468                            if ui.captured_node() == self.handle() {
469                                ui.release_mouse_capture();
470                            }
471                        }
472                    }
473                    PopupMessage::Content(content) => {
474                        if self.content.is_some() {
475                            ui.send_message(WidgetMessage::remove(
476                                *self.content,
477                                MessageDirection::ToWidget,
478                            ));
479                        }
480                        self.content.set_value_and_mark_modified(*content);
481
482                        ui.send_message(WidgetMessage::link(
483                            *self.content,
484                            MessageDirection::ToWidget,
485                            *self.body,
486                        ));
487                    }
488                    PopupMessage::Placement(placement) => {
489                        self.placement.set_value_and_mark_modified(*placement);
490                        self.invalidate_layout();
491                    }
492                    PopupMessage::AdjustPosition => {
493                        let new_position =
494                            adjust_placement_position(self.screen_bounds(), ui.screen_size());
495
496                        if new_position != self.screen_position() {
497                            ui.send_message(WidgetMessage::desired_position(
498                                self.handle,
499                                MessageDirection::ToWidget,
500                                ui.screen_to_root_canvas_space(new_position),
501                            ));
502                        }
503                    }
504                    PopupMessage::Owner(owner) => {
505                        self.owner = *owner;
506                    }
507                    PopupMessage::RelayedMessage(_) => (),
508                }
509            }
510        } else if let Some(WidgetMessage::KeyDown(key)) = message.data() {
511            if !message.handled() && *key == KeyCode::Escape {
512                ui.send_message(PopupMessage::close(self.handle, MessageDirection::ToWidget));
513                message.set_handled(true);
514            }
515        }
516        if ui.is_valid_handle(self.owner) && !message.handled() {
517            ui.send_message(PopupMessage::relayed_message(
518                self.owner,
519                MessageDirection::ToWidget,
520                message.clone(),
521            ));
522        }
523    }
524
525    fn handle_os_event(
526        &mut self,
527        self_handle: Handle<UiNode>,
528        ui: &mut UserInterface,
529        event: &OsEvent,
530    ) {
531        if let OsEvent::MouseInput { state, .. } = event {
532            if let Some(top_restriction) = ui.top_picking_restriction() {
533                if *state == ButtonState::Pressed
534                    && top_restriction.handle == self_handle
535                    && *self.is_open
536                {
537                    let pos = ui.cursor_position();
538                    if !self.widget.screen_bounds().contains(pos) && !*self.stays_open {
539                        ui.send_message(PopupMessage::close(
540                            self.handle(),
541                            MessageDirection::ToWidget,
542                        ));
543                    }
544                }
545            }
546        }
547    }
548}
549
550/// Popup widget builder is used to create [`Popup`] widget instances and add them to the user interface.
551pub struct PopupBuilder {
552    widget_builder: WidgetBuilder,
553    placement: Placement,
554    stays_open: bool,
555    content: Handle<UiNode>,
556    smart_placement: bool,
557    owner: Handle<UiNode>,
558}
559
560impl PopupBuilder {
561    /// Creates new builder instance.
562    pub fn new(widget_builder: WidgetBuilder) -> Self {
563        Self {
564            widget_builder,
565            placement: Placement::Cursor(Default::default()),
566            stays_open: false,
567            content: Default::default(),
568            smart_placement: true,
569            owner: Default::default(),
570        }
571    }
572
573    /// Sets the desired popup placement.
574    pub fn with_placement(mut self, placement: Placement) -> Self {
575        self.placement = placement;
576        self
577    }
578
579    /// Enables or disables smart placement.
580    pub fn with_smart_placement(mut self, smart_placement: bool) -> Self {
581        self.smart_placement = smart_placement;
582        self
583    }
584
585    /// Defines whether to keep the popup open when user clicks outside of its content or not.
586    pub fn stays_open(mut self, value: bool) -> Self {
587        self.stays_open = value;
588        self
589    }
590
591    /// Sets the content of the popup.
592    pub fn with_content(mut self, content: Handle<UiNode>) -> Self {
593        self.content = content;
594        self
595    }
596
597    /// Sets the desired owner of the popup, to which the popup will relay its own messages.
598    pub fn with_owner(mut self, owner: Handle<UiNode>) -> Self {
599        self.owner = owner;
600        self
601    }
602
603    /// Builds the popup widget, but does not add it to the user interface. Could be useful if you're making your
604    /// own derived version of the popup.
605    pub fn build_popup(self, ctx: &mut BuildContext) -> Popup {
606        let style = &ctx.style;
607
608        let body = BorderBuilder::new(
609            WidgetBuilder::new()
610                .with_background(style.property(Style::BRUSH_PRIMARY))
611                .with_foreground(style.property(Style::BRUSH_DARKEST))
612                .with_child(self.content),
613        )
614        .with_stroke_thickness(Thickness::uniform(1.0).into())
615        .build(ctx);
616
617        Popup {
618            widget: self
619                .widget_builder
620                .with_child(body)
621                .with_visibility(false)
622                .with_handle_os_events(true)
623                .build(ctx),
624            placement: self.placement.into(),
625            stays_open: self.stays_open.into(),
626            is_open: false.into(),
627            content: self.content.into(),
628            smart_placement: self.smart_placement.into(),
629            body: body.into(),
630            owner: self.owner,
631        }
632    }
633
634    /// Finishes building the [`Popup`] instance and adds to the user interface and returns its handle.
635    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
636        let popup = self.build_popup(ctx);
637        ctx.add_node(UiNode::new(popup))
638    }
639}
640
641#[cfg(test)]
642mod test {
643    use crate::popup::PopupBuilder;
644    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
645
646    #[test]
647    fn test_deletion() {
648        test_widget_deletion(|ctx| PopupBuilder::new(WidgetBuilder::new()).build(ctx));
649    }
650}