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