Skip to main content

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)?.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_else(|| 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            #[expect(clippy::or_fun_call)]
477            once(self.rect_align).chain(
478                self.alternative_aligns
479                    // Need the empty slice so the iters have the same type so we can unwrap_or
480                    .map(|a| a.iter().copied().chain([].iter().copied()))
481                    .unwrap_or(
482                        self.rect_align
483                            .symmetries()
484                            .iter()
485                            .copied()
486                            .chain(RectAlign::MENU_ALIGNS.iter().copied()),
487                    ),
488            ),
489            self.ctx.content_rect(),
490            anchor_rect,
491            self.gap,
492            expected_popup_size,
493        )
494        .unwrap_or_default()
495    }
496
497    /// Show the popup.
498    ///
499    /// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
500    /// no pointer.
501    pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
502        let id = self.id;
503        // When the popup was just opened with a click we don't want to immediately close it based
504        // on the `PopupCloseBehavior`, so we need to remember if the popup was already open on
505        // last frame. A convenient way to check this is to see if we have a response for the `Area`
506        // from last frame:
507        let was_open_last_frame = self.ctx.read_response(id).is_some();
508
509        let hover_pos = self.ctx.pointer_hover_pos();
510        if let OpenKind::Memory { set } = self.open_kind {
511            match set {
512                Some(SetOpenCommand::Bool(open)) => {
513                    if open {
514                        match self.anchor {
515                            PopupAnchor::PointerFixed => {
516                                self.ctx.memory_mut(|mem| mem.open_popup_at(id, hover_pos));
517                            }
518                            _ => Popup::open_id(&self.ctx, id),
519                        }
520                    } else {
521                        Self::close_id(&self.ctx, id);
522                    }
523                }
524                Some(SetOpenCommand::Toggle) => {
525                    Self::toggle_id(&self.ctx, id);
526                }
527                None => {
528                    self.ctx.memory_mut(|mem| mem.keep_popup_open(id));
529                }
530            }
531        }
532
533        if !self.open_kind.is_open(self.id, &self.ctx) {
534            return None;
535        }
536
537        let best_align = self.get_best_align();
538
539        let Popup {
540            id,
541            ctx,
542            anchor,
543            open_kind,
544            close_behavior,
545            kind,
546            info,
547            layer_id,
548            rect_align: _,
549            alternative_aligns: _,
550            gap,
551            width,
552            sense,
553            layout,
554            frame,
555            style,
556        } = self;
557
558        if kind != PopupKind::Tooltip {
559            ctx.pass_state_mut(|fs| {
560                fs.layers
561                    .entry(layer_id)
562                    .or_default()
563                    .open_popups
564                    .insert(id)
565            });
566        }
567
568        let anchor_rect = anchor.rect(id, &ctx)?;
569
570        let (pivot, anchor) = best_align.pivot_pos(&anchor_rect, gap);
571
572        let mut area = Area::new(id)
573            .order(kind.order())
574            .pivot(pivot)
575            .fixed_pos(anchor)
576            .sense(sense)
577            .layout(layout)
578            .info(info.unwrap_or_else(|| {
579                UiStackInfo::new(kind.into()).with_tag_value(
580                    MenuConfig::MENU_CONFIG_TAG,
581                    MenuConfig::new()
582                        .close_behavior(close_behavior)
583                        .style(style.clone()),
584                )
585            }));
586
587        if let Some(width) = width {
588            area = area.default_width(width);
589        }
590
591        let mut response = area.show(&ctx, |ui| {
592            style.apply(ui.style_mut());
593            let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
594            frame.show(ui, content).inner
595        });
596
597        // If the popup was just opened with a click, we don't want to immediately close it again.
598        let close_click = was_open_last_frame && ctx.input(|i| i.pointer.any_click());
599
600        let closed_by_click = match close_behavior {
601            PopupCloseBehavior::CloseOnClick => close_click,
602            PopupCloseBehavior::CloseOnClickOutside => {
603                close_click && response.response.clicked_elsewhere()
604            }
605            PopupCloseBehavior::IgnoreClicks => false,
606        };
607
608        // Mark the menu as shown, so the sub menu open state is not reset
609        MenuState::mark_shown(&ctx, id);
610
611        // If a submenu is open, the CloseBehavior is handled there
612        let is_any_submenu_open = !MenuState::is_deepest_open_sub_menu(&response.response.ctx, id);
613
614        let should_close = (!is_any_submenu_open && closed_by_click)
615            || ctx.input(|i| i.key_pressed(Key::Escape))
616            || response.response.should_close();
617
618        if should_close {
619            response.response.set_close();
620        }
621
622        match open_kind {
623            OpenKind::Open | OpenKind::Closed => {}
624            OpenKind::Bool(open) => {
625                if should_close {
626                    *open = false;
627                }
628            }
629            OpenKind::Memory { .. } => {
630                if should_close {
631                    ctx.memory_mut(|mem| mem.close_popup(id));
632                }
633            }
634        }
635
636        Some(response)
637    }
638}
639
640/// ## Static methods
641impl Popup<'_> {
642    /// The default ID when constructing a popup from the [`Response`] of e.g. a button.
643    pub fn default_response_id(response: &Response) -> Id {
644        response.id.with("popup")
645    }
646
647    /// Is the given popup open?
648    ///
649    /// This assumes the use of either:
650    /// * [`Self::open_memory`]
651    /// * [`Self::from_toggle_button_response`]
652    /// * [`Self::menu`]
653    /// * [`Self::context_menu`]
654    ///
655    /// The popup id should be the same as either you set with [`Self::id`] or the
656    /// default one from [`Self::default_response_id`].
657    pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
658        ctx.memory(|mem| mem.is_popup_open(popup_id))
659    }
660
661    /// Is any popup open?
662    ///
663    /// This assumes the egui memory is being used to track the open state of popups.
664    pub fn is_any_open(ctx: &Context) -> bool {
665        ctx.memory(|mem| mem.any_popup_open())
666    }
667
668    /// Open the given popup and close all others.
669    ///
670    /// If you are NOT using [`Popup::show`], you must
671    /// also call [`crate::Memory::keep_popup_open`] as long as
672    /// you're showing the popup.
673    pub fn open_id(ctx: &Context, popup_id: Id) {
674        ctx.memory_mut(|mem| mem.open_popup(popup_id));
675    }
676
677    /// Toggle the given popup between closed and open.
678    ///
679    /// Note: At most, only one popup can be open at a time.
680    pub fn toggle_id(ctx: &Context, popup_id: Id) {
681        ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
682    }
683
684    /// Close all currently open popups.
685    pub fn close_all(ctx: &Context) {
686        ctx.memory_mut(|mem| mem.close_all_popups());
687    }
688
689    /// Close the given popup, if it is open.
690    ///
691    /// See also [`Self::close_all`] if you want to close any / all currently open popups.
692    pub fn close_id(ctx: &Context, popup_id: Id) {
693        ctx.memory_mut(|mem| mem.close_popup(popup_id));
694    }
695
696    /// Get the position for this popup, if it is open.
697    pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
698        ctx.memory(|mem| mem.popup_position(popup_id))
699    }
700}