egui/containers/
popup.rs

1#![expect(deprecated)] // This is a new, safe wrapper around the old `Memory::popup` API.
2
3use std::iter::once;
4
5use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
6
7use crate::{
8    Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
9    Sense, Ui, UiKind, UiStackInfo,
10    containers::menu::{MenuConfig, MenuState, menu_style},
11    style::StyleModifier,
12};
13
14/// What should we anchor the popup to?
15///
16/// The final position for the popup will be calculated based on [`RectAlign`]
17/// and can be customized with [`Popup::align`] and [`Popup::align_alternatives`].
18/// [`PopupAnchor`] is the parent rect of [`RectAlign`].
19///
20/// For [`PopupAnchor::Pointer`], [`PopupAnchor::PointerFixed`] and [`PopupAnchor::Position`],
21/// the rect will be derived via [`Rect::from_pos`] (so a zero-sized rect at the given position).
22///
23/// The rect should be in global coordinates. `PopupAnchor::from(&response)` will automatically
24/// do this conversion.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum PopupAnchor {
27    /// Show the popup relative to some parent [`Rect`].
28    ParentRect(Rect),
29
30    /// Show the popup relative to the mouse pointer.
31    Pointer,
32
33    /// Remember the mouse position and show the popup relative to that (like a context menu).
34    PointerFixed,
35
36    /// Show the popup relative to a specific position.
37    Position(Pos2),
38}
39
40impl From<Rect> for PopupAnchor {
41    fn from(rect: Rect) -> Self {
42        Self::ParentRect(rect)
43    }
44}
45
46impl From<Pos2> for PopupAnchor {
47    fn from(pos: Pos2) -> Self {
48        Self::Position(pos)
49    }
50}
51
52impl From<&Response> for PopupAnchor {
53    fn from(response: &Response) -> Self {
54        // We use interact_rect so we don't show the popup relative to some clipped point
55        let mut widget_rect = response.interact_rect;
56        if let Some(to_global) = response.ctx.layer_transform_to_global(response.layer_id) {
57            widget_rect = to_global * widget_rect;
58        }
59        Self::ParentRect(widget_rect)
60    }
61}
62
63impl PopupAnchor {
64    /// Get the rect the popup should be shown relative to.
65    /// Returns `Rect::from_pos` for [`PopupAnchor::Pointer`], [`PopupAnchor::PointerFixed`]
66    /// and [`PopupAnchor::Position`] (so the rect will be zero-sized).
67    pub fn rect(self, popup_id: Id, ctx: &Context) -> Option<Rect> {
68        match self {
69            Self::ParentRect(rect) => Some(rect),
70            Self::Pointer => ctx.pointer_hover_pos().map(Rect::from_pos),
71            Self::PointerFixed => Popup::position_of_id(ctx, popup_id).map(Rect::from_pos),
72            Self::Position(pos) => Some(Rect::from_pos(pos)),
73        }
74    }
75}
76
77/// Determines popup's close behavior
78#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
79pub enum PopupCloseBehavior {
80    /// Popup will be closed on click anywhere, inside or outside the popup.
81    ///
82    /// It is used in [`crate::ComboBox`] and in [`crate::containers::menu`]s.
83    #[default]
84    CloseOnClick,
85
86    /// Popup will be closed if the click happened somewhere else
87    /// but in the popup's body
88    CloseOnClickOutside,
89
90    /// Clicks will be ignored. Popup might be closed manually by calling [`crate::Memory::close_all_popups`]
91    /// or by pressing the escape button
92    IgnoreClicks,
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum SetOpenCommand {
97    /// Set the open state to the given value
98    Bool(bool),
99
100    /// Toggle the open state
101    Toggle,
102}
103
104impl From<bool> for SetOpenCommand {
105    fn from(b: bool) -> Self {
106        Self::Bool(b)
107    }
108}
109
110/// How do we determine if the popup should be open or closed
111enum OpenKind<'a> {
112    /// Always open
113    Open,
114
115    /// Always closed
116    Closed,
117
118    /// Open if the bool is true
119    Bool(&'a mut bool),
120
121    /// Store the open state via [`crate::Memory`]
122    Memory { set: Option<SetOpenCommand> },
123}
124
125impl OpenKind<'_> {
126    /// Returns `true` if the popup should be open
127    fn is_open(&self, popup_id: Id, ctx: &Context) -> bool {
128        match self {
129            OpenKind::Open => true,
130            OpenKind::Closed => false,
131            OpenKind::Bool(open) => **open,
132            OpenKind::Memory { .. } => Popup::is_id_open(ctx, popup_id),
133        }
134    }
135}
136
137/// Is the popup a popup, tooltip or menu?
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub enum PopupKind {
140    Popup,
141    Tooltip,
142    Menu,
143}
144
145impl PopupKind {
146    /// Returns the order to be used with this kind.
147    pub fn order(self) -> Order {
148        match self {
149            Self::Tooltip => Order::Tooltip,
150            Self::Menu | Self::Popup => Order::Foreground,
151        }
152    }
153}
154
155impl From<PopupKind> for UiKind {
156    fn from(kind: PopupKind) -> Self {
157        match kind {
158            PopupKind::Popup => Self::Popup,
159            PopupKind::Tooltip => Self::Tooltip,
160            PopupKind::Menu => Self::Menu,
161        }
162    }
163}
164
165/// A popup container.
166#[must_use = "Call `.show()` to actually display the popup"]
167pub struct Popup<'a> {
168    id: Id,
169    ctx: Context,
170    anchor: PopupAnchor,
171    rect_align: RectAlign,
172    alternative_aligns: Option<&'a [RectAlign]>,
173    layer_id: LayerId,
174    open_kind: OpenKind<'a>,
175    close_behavior: PopupCloseBehavior,
176    info: Option<UiStackInfo>,
177    kind: PopupKind,
178
179    /// Gap between the anchor and the popup
180    gap: f32,
181
182    /// Default width passed to the Area
183    width: Option<f32>,
184    sense: Sense,
185    layout: Layout,
186    frame: Option<Frame>,
187    style: StyleModifier,
188}
189
190impl<'a> Popup<'a> {
191    /// Create a new popup
192    pub fn new(id: Id, ctx: Context, anchor: impl Into<PopupAnchor>, layer_id: LayerId) -> Self {
193        Self {
194            id,
195            ctx,
196            anchor: anchor.into(),
197            open_kind: OpenKind::Open,
198            close_behavior: PopupCloseBehavior::default(),
199            info: None,
200            kind: PopupKind::Popup,
201            layer_id,
202            rect_align: RectAlign::BOTTOM_START,
203            alternative_aligns: None,
204            gap: 0.0,
205            width: None,
206            sense: Sense::click(),
207            layout: Layout::default(),
208            frame: None,
209            style: StyleModifier::default(),
210        }
211    }
212
213    /// Show a popup relative to some widget.
214    /// The popup will be always open.
215    ///
216    /// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
217    pub fn from_response(response: &Response) -> Self {
218        Self::new(
219            Self::default_response_id(response),
220            response.ctx.clone(),
221            response,
222            response.layer_id,
223        )
224    }
225
226    /// Show a popup relative to some widget,
227    /// toggling the open state based on the widget's click state.
228    ///
229    /// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
230    pub fn from_toggle_button_response(button_response: &Response) -> Self {
231        Self::from_response(button_response)
232            .open_memory(button_response.clicked().then_some(SetOpenCommand::Toggle))
233    }
234
235    /// Show a popup when the widget was clicked.
236    /// Sets the layout to `Layout::top_down_justified(Align::Min)`.
237    pub fn menu(button_response: &Response) -> Self {
238        Self::from_toggle_button_response(button_response)
239            .kind(PopupKind::Menu)
240            .layout(Layout::top_down_justified(Align::Min))
241            .style(menu_style)
242            .gap(0.0)
243    }
244
245    /// Show a context menu when the widget was secondary clicked.
246    /// Sets the layout to `Layout::top_down_justified(Align::Min)`.
247    /// In contrast to [`Self::menu`], this will open at the pointer position.
248    pub fn context_menu(response: &Response) -> Self {
249        Self::menu(response)
250            .open_memory(if response.secondary_clicked() {
251                Some(SetOpenCommand::Bool(true))
252            } else if response.clicked() {
253                // Explicitly close the menu if the widget was clicked
254                // Without this, the context menu would stay open if the user clicks the widget
255                Some(SetOpenCommand::Bool(false))
256            } else {
257                None
258            })
259            .at_pointer_fixed()
260    }
261
262    /// Set the kind of the popup. Used for [`Area::kind`] and [`Area::order`].
263    #[inline]
264    pub fn kind(mut self, kind: PopupKind) -> Self {
265        self.kind = kind;
266        self
267    }
268
269    /// Set the [`UiStackInfo`] of the popup's [`Ui`].
270    #[inline]
271    pub fn info(mut self, info: UiStackInfo) -> Self {
272        self.info = Some(info);
273        self
274    }
275
276    /// Set the [`RectAlign`] of the popup relative to the [`PopupAnchor`].
277    /// This is the default position, and will be used if it fits.
278    /// See [`Self::align_alternatives`] for more on this.
279    #[inline]
280    pub fn align(mut self, position_align: RectAlign) -> Self {
281        self.rect_align = position_align;
282        self
283    }
284
285    /// Set alternative positions to try if the default one doesn't fit. Set to an empty slice to
286    /// always use the position you set with [`Self::align`].
287    /// By default, this will try [`RectAlign::symmetries`] and then [`RectAlign::MENU_ALIGNS`].
288    #[inline]
289    pub fn align_alternatives(mut self, alternatives: &'a [RectAlign]) -> Self {
290        self.alternative_aligns = Some(alternatives);
291        self
292    }
293
294    /// Force the popup to be open or closed.
295    #[inline]
296    pub fn open(mut self, open: bool) -> Self {
297        self.open_kind = if open {
298            OpenKind::Open
299        } else {
300            OpenKind::Closed
301        };
302        self
303    }
304
305    /// Store the open state via [`crate::Memory`].
306    /// You can set the state via the first [`SetOpenCommand`] param.
307    #[inline]
308    pub fn open_memory(mut self, set_state: impl Into<Option<SetOpenCommand>>) -> Self {
309        self.open_kind = OpenKind::Memory {
310            set: set_state.into(),
311        };
312        self
313    }
314
315    /// Store the open state via a mutable bool.
316    #[inline]
317    pub fn open_bool(mut self, open: &'a mut bool) -> Self {
318        self.open_kind = OpenKind::Bool(open);
319        self
320    }
321
322    /// Set the close behavior of the popup.
323    ///
324    /// This will do nothing if [`Popup::open`] was called.
325    #[inline]
326    pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
327        self.close_behavior = close_behavior;
328        self
329    }
330
331    /// Show the popup relative to the pointer.
332    #[inline]
333    pub fn at_pointer(mut self) -> Self {
334        self.anchor = PopupAnchor::Pointer;
335        self
336    }
337
338    /// Remember the pointer position at the time of opening the popup, and show the popup
339    /// relative to that.
340    #[inline]
341    pub fn at_pointer_fixed(mut self) -> Self {
342        self.anchor = PopupAnchor::PointerFixed;
343        self
344    }
345
346    /// Show the popup relative to a specific position.
347    #[inline]
348    pub fn at_position(mut self, position: Pos2) -> Self {
349        self.anchor = PopupAnchor::Position(position);
350        self
351    }
352
353    /// Show the popup relative to the given [`PopupAnchor`].
354    #[inline]
355    pub fn anchor(mut self, anchor: impl Into<PopupAnchor>) -> Self {
356        self.anchor = anchor.into();
357        self
358    }
359
360    /// Set the gap between the anchor and the popup.
361    #[inline]
362    pub fn gap(mut self, gap: f32) -> Self {
363        self.gap = gap;
364        self
365    }
366
367    /// Set the frame of the popup.
368    #[inline]
369    pub fn frame(mut self, frame: Frame) -> Self {
370        self.frame = Some(frame);
371        self
372    }
373
374    /// Set the sense of the popup.
375    #[inline]
376    pub fn sense(mut self, sense: Sense) -> Self {
377        self.sense = sense;
378        self
379    }
380
381    /// Set the layout of the popup.
382    #[inline]
383    pub fn layout(mut self, layout: Layout) -> Self {
384        self.layout = layout;
385        self
386    }
387
388    /// The width that will be passed to [`Area::default_width`].
389    #[inline]
390    pub fn width(mut self, width: f32) -> Self {
391        self.width = Some(width);
392        self
393    }
394
395    /// Set the id of the Area.
396    #[inline]
397    pub fn id(mut self, id: Id) -> Self {
398        self.id = id;
399        self
400    }
401
402    /// Set the style for the popup contents.
403    ///
404    /// Default:
405    /// - is [`menu_style`] for [`Self::menu`] and [`Self::context_menu`]
406    /// - is [`None`] otherwise
407    #[inline]
408    pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
409        self.style = style.into();
410        self
411    }
412
413    /// Get the [`Context`]
414    pub fn ctx(&self) -> &Context {
415        &self.ctx
416    }
417
418    /// Return the [`PopupAnchor`] of the popup.
419    pub fn get_anchor(&self) -> PopupAnchor {
420        self.anchor
421    }
422
423    /// Return the anchor rect of the popup.
424    ///
425    /// Returns `None` if the anchor is [`PopupAnchor::Pointer`] and there is no pointer.
426    pub fn get_anchor_rect(&self) -> Option<Rect> {
427        self.anchor.rect(self.id, &self.ctx)
428    }
429
430    /// Get the expected rect the popup will be shown in.
431    ///
432    /// Returns `None` if the popup wasn't shown before or anchor is `PopupAnchor::Pointer` and
433    /// there is no pointer.
434    pub fn get_popup_rect(&self) -> Option<Rect> {
435        let size = self.get_expected_size();
436        if let Some(size) = size {
437            self.get_anchor_rect()
438                .map(|anchor| self.get_best_align().align_rect(&anchor, size, self.gap))
439        } else {
440            None
441        }
442    }
443
444    /// Get the id of the popup.
445    pub fn get_id(&self) -> Id {
446        self.id
447    }
448
449    /// Is the popup open?
450    pub fn is_open(&self) -> bool {
451        match &self.open_kind {
452            OpenKind::Open => true,
453            OpenKind::Closed => false,
454            OpenKind::Bool(open) => **open,
455            OpenKind::Memory { .. } => Self::is_id_open(&self.ctx, self.id),
456        }
457    }
458
459    /// Get the expected size of the popup.
460    pub fn get_expected_size(&self) -> Option<Vec2> {
461        AreaState::load(&self.ctx, self.id).and_then(|area| area.size)
462    }
463
464    /// Calculate the best alignment for the popup, based on the last size and screen rect.
465    pub fn get_best_align(&self) -> RectAlign {
466        let expected_popup_size = self
467            .get_expected_size()
468            .unwrap_or(vec2(self.width.unwrap_or(0.0), 0.0));
469
470        let Some(anchor_rect) = self.anchor.rect(self.id, &self.ctx) else {
471            return self.rect_align;
472        };
473
474        RectAlign::find_best_align(
475            #[expect(clippy::iter_on_empty_collections)]
476            once(self.rect_align).chain(
477                self.alternative_aligns
478                    // Need the empty slice so the iters have the same type so we can unwrap_or
479                    .map(|a| a.iter().copied().chain([].iter().copied()))
480                    .unwrap_or(
481                        self.rect_align
482                            .symmetries()
483                            .iter()
484                            .copied()
485                            .chain(RectAlign::MENU_ALIGNS.iter().copied()),
486                    ),
487            ),
488            self.ctx.screen_rect(),
489            anchor_rect,
490            self.gap,
491            expected_popup_size,
492        )
493        .unwrap_or_default()
494    }
495
496    /// Show the popup.
497    ///
498    /// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
499    /// no pointer.
500    pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
501        let id = self.id;
502        // When the popup was just opened with a click we don't want to immediately close it based
503        // on the `PopupCloseBehavior`, so we need to remember if the popup was already open on
504        // last frame. A convenient way to check this is to see if we have a response for the `Area`
505        // from last frame:
506        let was_open_last_frame = self.ctx.read_response(id).is_some();
507
508        let hover_pos = self.ctx.pointer_hover_pos();
509        if let OpenKind::Memory { set } = self.open_kind {
510            match set {
511                Some(SetOpenCommand::Bool(open)) => {
512                    if open {
513                        match self.anchor {
514                            PopupAnchor::PointerFixed => {
515                                self.ctx.memory_mut(|mem| mem.open_popup_at(id, hover_pos));
516                            }
517                            _ => Popup::open_id(&self.ctx, id),
518                        }
519                    } else {
520                        Self::close_id(&self.ctx, id);
521                    }
522                }
523                Some(SetOpenCommand::Toggle) => {
524                    Self::toggle_id(&self.ctx, id);
525                }
526                None => {
527                    self.ctx.memory_mut(|mem| mem.keep_popup_open(id));
528                }
529            }
530        }
531
532        if !self.open_kind.is_open(self.id, &self.ctx) {
533            return None;
534        }
535
536        let best_align = self.get_best_align();
537
538        let Popup {
539            id,
540            ctx,
541            anchor,
542            open_kind,
543            close_behavior,
544            kind,
545            info,
546            layer_id,
547            rect_align: _,
548            alternative_aligns: _,
549            gap,
550            width,
551            sense,
552            layout,
553            frame,
554            style,
555        } = self;
556
557        if kind != PopupKind::Tooltip {
558            ctx.pass_state_mut(|fs| {
559                fs.layers
560                    .entry(layer_id)
561                    .or_default()
562                    .open_popups
563                    .insert(id)
564            });
565        }
566
567        let anchor_rect = anchor.rect(id, &ctx)?;
568
569        let (pivot, anchor) = best_align.pivot_pos(&anchor_rect, gap);
570
571        let mut area = Area::new(id)
572            .order(kind.order())
573            .pivot(pivot)
574            .fixed_pos(anchor)
575            .sense(sense)
576            .layout(layout)
577            .info(info.unwrap_or_else(|| {
578                UiStackInfo::new(kind.into()).with_tag_value(
579                    MenuConfig::MENU_CONFIG_TAG,
580                    MenuConfig::new()
581                        .close_behavior(close_behavior)
582                        .style(style.clone()),
583                )
584            }));
585
586        if let Some(width) = width {
587            area = area.default_width(width);
588        }
589
590        let mut response = area.show(&ctx, |ui| {
591            style.apply(ui.style_mut());
592            let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
593            frame.show(ui, content).inner
594        });
595
596        // If the popup was just opened with a click, we don't want to immediately close it again.
597        let close_click = was_open_last_frame && ctx.input(|i| i.pointer.any_click());
598
599        let closed_by_click = match close_behavior {
600            PopupCloseBehavior::CloseOnClick => close_click,
601            PopupCloseBehavior::CloseOnClickOutside => {
602                close_click && response.response.clicked_elsewhere()
603            }
604            PopupCloseBehavior::IgnoreClicks => false,
605        };
606
607        // If a submenu is open, the CloseBehavior is handled there
608        let is_any_submenu_open = !MenuState::is_deepest_sub_menu(&response.response.ctx, id);
609
610        let should_close = (!is_any_submenu_open && closed_by_click)
611            || ctx.input(|i| i.key_pressed(Key::Escape))
612            || response.response.should_close();
613
614        if should_close {
615            response.response.set_close();
616        }
617
618        match open_kind {
619            OpenKind::Open | OpenKind::Closed => {}
620            OpenKind::Bool(open) => {
621                if should_close {
622                    *open = false;
623                }
624            }
625            OpenKind::Memory { .. } => {
626                if should_close {
627                    ctx.memory_mut(|mem| mem.close_popup(id));
628                }
629            }
630        }
631
632        Some(response)
633    }
634}
635
636/// ## Static methods
637impl Popup<'_> {
638    /// The default ID when constructing a popup from the [`Response`] of e.g. a button.
639    pub fn default_response_id(response: &Response) -> Id {
640        response.id.with("popup")
641    }
642
643    /// Is the given popup open?
644    ///
645    /// This assumes the use of either:
646    /// * [`Self::open_memory`]
647    /// * [`Self::from_toggle_button_response`]
648    /// * [`Self::menu`]
649    /// * [`Self::context_menu`]
650    ///
651    /// The popup id should be the same as either you set with [`Self::id`] or the
652    /// default one from [`Self::default_response_id`].
653    pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
654        ctx.memory(|mem| mem.is_popup_open(popup_id))
655    }
656
657    /// Is any popup open?
658    ///
659    /// This assumes the egui memory is being used to track the open state of popups.
660    pub fn is_any_open(ctx: &Context) -> bool {
661        ctx.memory(|mem| mem.any_popup_open())
662    }
663
664    /// Open the given popup and close all others.
665    ///
666    /// If you are NOT using [`Popup::show`], you must
667    /// also call [`crate::Memory::keep_popup_open`] as long as
668    /// you're showing the popup.
669    pub fn open_id(ctx: &Context, popup_id: Id) {
670        ctx.memory_mut(|mem| mem.open_popup(popup_id));
671    }
672
673    /// Toggle the given popup between closed and open.
674    ///
675    /// Note: At most, only one popup can be open at a time.
676    pub fn toggle_id(ctx: &Context, popup_id: Id) {
677        ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
678    }
679
680    /// Close all currently open popups.
681    pub fn close_all(ctx: &Context) {
682        ctx.memory_mut(|mem| mem.close_all_popups());
683    }
684
685    /// Close the given popup, if it is open.
686    ///
687    /// See also [`Self::close_all`] if you want to close any / all currently open popups.
688    pub fn close_id(ctx: &Context, popup_id: Id) {
689        ctx.memory_mut(|mem| mem.close_popup(popup_id));
690    }
691
692    /// Get the position for this popup, if it is open.
693    pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
694        ctx.memory(|mem| mem.popup_position(popup_id))
695    }
696}