Skip to main content

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