Skip to main content

egui/containers/
popup.rs

1use core::iter::once;
2
3use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
4
5use crate::{
6    Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
7    Sense, Ui, UiKind, UiStackInfo,
8    containers::menu::{MenuConfig, MenuState, menu_style},
9    style::StyleModifier,
10};
11
12/// What should we anchor the popup to?
13///
14/// The final position for the popup will be calculated based on [`RectAlign`]
15/// and can be customized with [`Popup::align`] and [`Popup::align_alternatives`].
16/// [`PopupAnchor`] is the parent rect of [`RectAlign`].
17///
18/// For [`PopupAnchor::Pointer`], [`PopupAnchor::PointerFixed`] and [`PopupAnchor::Position`],
19/// the rect will be derived via [`Rect::from_pos`] (so a zero-sized rect at the given position).
20///
21/// The rect should be in global coordinates. `PopupAnchor::from(&response)` will automatically
22/// do this conversion.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum PopupAnchor {
25    /// Show the popup relative to some parent [`Rect`].
26    ParentRect(Rect),
27
28    /// Show the popup relative to the mouse pointer.
29    Pointer,
30
31    /// Remember the mouse position and show the popup relative to that (like a context menu).
32    PointerFixed,
33
34    /// Show the popup relative to a specific position.
35    Position(Pos2),
36}
37
38impl From<Rect> for PopupAnchor {
39    fn from(rect: Rect) -> Self {
40        Self::ParentRect(rect)
41    }
42}
43
44impl From<Pos2> for PopupAnchor {
45    fn from(pos: Pos2) -> Self {
46        Self::Position(pos)
47    }
48}
49
50impl From<&Response> for PopupAnchor {
51    fn from(response: &Response) -> Self {
52        // We use interact_rect so we don't show the popup relative to some clipped point
53        let mut widget_rect = response.interact_rect;
54        if let Some(to_global) = response.ctx.layer_transform_to_global(response.layer_id) {
55            widget_rect = to_global * widget_rect;
56        }
57        Self::ParentRect(widget_rect)
58    }
59}
60
61impl PopupAnchor {
62    /// Get the rect the popup should be shown relative to.
63    /// Returns `Rect::from_pos` for [`PopupAnchor::Pointer`], [`PopupAnchor::PointerFixed`]
64    /// and [`PopupAnchor::Position`] (so the rect will be zero-sized).
65    pub fn rect(self, popup_id: Id, ctx: &Context) -> Option<Rect> {
66        match self {
67            Self::ParentRect(rect) => Some(rect),
68            Self::Pointer => ctx.pointer_hover_pos().map(Rect::from_pos),
69            Self::PointerFixed => Popup::position_of_id(ctx, popup_id).map(Rect::from_pos),
70            Self::Position(pos) => Some(Rect::from_pos(pos)),
71        }
72    }
73}
74
75/// Determines popup's close behavior
76#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
77pub enum PopupCloseBehavior {
78    /// Popup will be closed on click anywhere, inside or outside the popup.
79    ///
80    /// It is used in [`crate::ComboBox`] and in [`crate::containers::menu`]s.
81    #[default]
82    CloseOnClick,
83
84    /// Popup will be closed if the click happened somewhere else
85    /// but in the popup's body
86    CloseOnClickOutside,
87
88    /// Clicks will be ignored. Popup might be closed manually by calling [`Popup::close_all`]
89    /// or by pressing the escape button
90    IgnoreClicks,
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum SetOpenCommand {
95    /// Set the open state to the given value
96    Bool(bool),
97
98    /// Toggle the open state
99    Toggle,
100}
101
102impl From<bool> for SetOpenCommand {
103    fn from(b: bool) -> Self {
104        Self::Bool(b)
105    }
106}
107
108/// How do we determine if the popup should be open or closed
109enum OpenKind<'a> {
110    /// Always open
111    Open,
112
113    /// Always closed
114    Closed,
115
116    /// Open if the bool is true
117    Bool(&'a mut bool),
118
119    /// Store the open state via [`crate::Memory`]
120    Memory { set: Option<SetOpenCommand> },
121}
122
123impl OpenKind<'_> {
124    /// Returns `true` if the popup should be open
125    fn is_open(&self, popup_id: Id, ctx: &Context) -> bool {
126        match self {
127            OpenKind::Open => true,
128            OpenKind::Closed => false,
129            OpenKind::Bool(open) => **open,
130            OpenKind::Memory { .. } => Popup::is_id_open(ctx, popup_id),
131        }
132    }
133}
134
135/// Is the popup a popup, tooltip or menu?
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum PopupKind {
138    Popup,
139    Tooltip,
140    Menu,
141}
142
143impl PopupKind {
144    /// Returns the order to be used with this kind.
145    pub fn order(self) -> Order {
146        match self {
147            Self::Tooltip => Order::Tooltip,
148            Self::Menu | Self::Popup => Order::Foreground,
149        }
150    }
151}
152
153impl From<PopupKind> for UiKind {
154    fn from(kind: PopupKind) -> Self {
155        match kind {
156            PopupKind::Popup => Self::Popup,
157            PopupKind::Tooltip => Self::Tooltip,
158            PopupKind::Menu => Self::Menu,
159        }
160    }
161}
162
163/// A popup container.
164#[must_use = "Call `.show()` to actually display the popup"]
165pub struct Popup<'a> {
166    id: Id,
167    ctx: Context,
168    anchor: PopupAnchor,
169    rect_align: RectAlign,
170    alternative_aligns: Option<&'a [RectAlign]>,
171    layer_id: LayerId,
172    open_kind: OpenKind<'a>,
173    close_behavior: PopupCloseBehavior,
174    info: Option<UiStackInfo>,
175    kind: PopupKind,
176
177    /// Gap between the anchor and the popup
178    gap: f32,
179
180    /// Default width passed to the Area
181    width: Option<f32>,
182    sense: Sense,
183    interactable: bool,
184    layout: Layout,
185    frame: Option<Frame>,
186    style: StyleModifier,
187}
188
189impl<'a> Popup<'a> {
190    /// Create a new popup
191    pub fn new(id: Id, ctx: Context, anchor: impl Into<PopupAnchor>, layer_id: LayerId) -> Self {
192        Self {
193            id,
194            ctx,
195            anchor: anchor.into(),
196            open_kind: OpenKind::Open,
197            close_behavior: PopupCloseBehavior::default(),
198            info: None,
199            kind: PopupKind::Popup,
200            layer_id,
201            rect_align: RectAlign::BOTTOM_START,
202            alternative_aligns: None,
203            gap: 0.0,
204            width: None,
205            sense: Sense::click(),
206            interactable: true,
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    /// If `false`, the pointer goes straight through the popup and it's widgets to whatever is behind it.
375    ///
376    /// Default: `true`.
377    #[inline]
378    pub fn interactable(mut self, interactable: bool) -> Self {
379        self.interactable = interactable;
380        self
381    }
382
383    /// Set the sense of the popup.
384    #[inline]
385    pub fn sense(mut self, sense: Sense) -> Self {
386        self.sense = sense;
387        self
388    }
389
390    /// Set the layout of the popup.
391    #[inline]
392    pub fn layout(mut self, layout: Layout) -> Self {
393        self.layout = layout;
394        self
395    }
396
397    /// The width that will be passed to [`Area::default_width`].
398    #[inline]
399    pub fn width(mut self, width: f32) -> Self {
400        self.width = Some(width);
401        self
402    }
403
404    /// Set the id of the Area.
405    #[inline]
406    pub fn id(mut self, id: Id) -> Self {
407        self.id = id;
408        self
409    }
410
411    /// Set the style for the popup contents.
412    ///
413    /// Default:
414    /// - is [`menu_style`] for [`Self::menu`] and [`Self::context_menu`]
415    /// - is [`None`] otherwise
416    #[inline]
417    pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
418        self.style = style.into();
419        self
420    }
421
422    /// Get the [`Context`]
423    pub fn ctx(&self) -> &Context {
424        &self.ctx
425    }
426
427    /// Return the [`PopupAnchor`] of the popup.
428    pub fn get_anchor(&self) -> PopupAnchor {
429        self.anchor
430    }
431
432    /// Return the anchor rect of the popup.
433    ///
434    /// Returns `None` if the anchor is [`PopupAnchor::Pointer`] and there is no pointer.
435    pub fn get_anchor_rect(&self) -> Option<Rect> {
436        self.anchor.rect(self.id, &self.ctx)
437    }
438
439    /// Get the expected rect the popup will be shown in.
440    ///
441    /// Returns `None` if the popup wasn't shown before or anchor is `PopupAnchor::Pointer` and
442    /// there is no pointer.
443    pub fn get_popup_rect(&self) -> Option<Rect> {
444        let size = self.get_expected_size();
445        if let Some(size) = size {
446            self.get_anchor_rect()
447                .map(|anchor| self.get_best_align().align_rect(&anchor, size, self.gap))
448        } else {
449            None
450        }
451    }
452
453    /// Get the id of the popup.
454    pub fn get_id(&self) -> Id {
455        self.id
456    }
457
458    /// Is the popup open?
459    pub fn is_open(&self) -> bool {
460        match &self.open_kind {
461            OpenKind::Open => true,
462            OpenKind::Closed => false,
463            OpenKind::Bool(open) => **open,
464            OpenKind::Memory { .. } => Self::is_id_open(&self.ctx, self.id),
465        }
466    }
467
468    /// Get the expected size of the popup.
469    pub fn get_expected_size(&self) -> Option<Vec2> {
470        AreaState::load(&self.ctx, self.id)?.size
471    }
472
473    /// Calculate the best alignment for the popup, based on the last size and screen rect.
474    pub fn get_best_align(&self) -> RectAlign {
475        let expected_popup_size = self
476            .get_expected_size()
477            .unwrap_or_else(|| vec2(self.width.unwrap_or(0.0), 0.0));
478
479        let Some(anchor_rect) = self.anchor.rect(self.id, &self.ctx) else {
480            return self.rect_align;
481        };
482
483        RectAlign::find_best_align(
484            #[expect(clippy::iter_on_empty_collections)]
485            #[expect(clippy::or_fun_call)]
486            core::iter::chain(
487                once(self.rect_align),
488                self.alternative_aligns
489                    // Need the empty slice so the iters have the same type so we can unwrap_or
490                    .map(|a| core::iter::chain(a.iter().copied(), [].iter().copied()))
491                    .unwrap_or(core::iter::chain(
492                        self.rect_align.symmetries().iter().copied(),
493                        RectAlign::MENU_ALIGNS.iter().copied(),
494                    )),
495            ),
496            self.ctx.content_rect(),
497            anchor_rect,
498            self.gap,
499            expected_popup_size,
500        )
501        .unwrap_or_default()
502    }
503
504    /// Show the popup.
505    ///
506    /// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
507    /// no pointer.
508    pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
509        let id = self.id;
510        // When the popup was just opened with a click we don't want to immediately close it based
511        // on the `PopupCloseBehavior`, so we need to remember if the popup was already open on
512        // last frame. A convenient way to check this is to see if we have a response for the `Area`
513        // from last frame:
514        let was_open_last_frame = self.ctx.read_response(id).is_some();
515
516        let hover_pos = self.ctx.pointer_hover_pos();
517        if let OpenKind::Memory { set } = self.open_kind {
518            match set {
519                Some(SetOpenCommand::Bool(open)) => {
520                    if open {
521                        match self.anchor {
522                            PopupAnchor::PointerFixed => {
523                                self.ctx.memory_mut(|mem| mem.open_popup_at(id, hover_pos));
524                            }
525                            _ => Popup::open_id(&self.ctx, id),
526                        }
527                    } else {
528                        Self::close_id(&self.ctx, id);
529                    }
530                }
531                Some(SetOpenCommand::Toggle) => {
532                    Self::toggle_id(&self.ctx, id);
533                }
534                None => {
535                    self.ctx.memory_mut(|mem| mem.keep_popup_open(id));
536                }
537            }
538        }
539
540        if !self.open_kind.is_open(self.id, &self.ctx) {
541            return None;
542        }
543
544        let best_align = self.get_best_align();
545
546        let Popup {
547            id,
548            ctx,
549            anchor,
550            open_kind,
551            close_behavior,
552            kind,
553            info,
554            layer_id,
555            rect_align: _,
556            alternative_aligns: _,
557            gap,
558            width,
559            sense,
560            interactable,
561            layout,
562            frame,
563            style,
564        } = self;
565
566        if kind != PopupKind::Tooltip {
567            ctx.pass_state_mut(|fs| {
568                fs.layers
569                    .entry(layer_id)
570                    .or_default()
571                    .open_popups
572                    .insert(id)
573            });
574        }
575
576        let anchor_rect = anchor.rect(id, &ctx)?;
577
578        let (pivot, anchor) = best_align.pivot_pos(&anchor_rect, gap);
579
580        let mut area = Area::new(id)
581            .order(kind.order())
582            .pivot(pivot)
583            .fixed_pos(anchor)
584            .sense(sense)
585            .interactable(interactable)
586            .layout(layout)
587            .sizing_pass(!was_open_last_frame)
588            .info(info.unwrap_or_else(|| {
589                UiStackInfo::new(kind.into()).with_tag_value(
590                    MenuConfig::MENU_CONFIG_TAG,
591                    MenuConfig::new()
592                        .close_behavior(close_behavior)
593                        .style(style.clone()),
594                )
595            }));
596
597        if let Some(width) = width {
598            area = area.default_width(width);
599        }
600
601        let mut response = area.show(&ctx, |ui| {
602            style.apply(ui.style_mut());
603            let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
604            frame.show(ui, content).inner
605        });
606
607        // If the popup was just opened with a click, we don't want to immediately close it again.
608        let close_click = was_open_last_frame && ctx.input(|i| i.pointer.any_click());
609
610        let closed_by_click = match close_behavior {
611            PopupCloseBehavior::CloseOnClick => close_click,
612            PopupCloseBehavior::CloseOnClickOutside => {
613                close_click && response.response.clicked_elsewhere()
614            }
615            PopupCloseBehavior::IgnoreClicks => false,
616        };
617
618        // Mark the menu as shown, so the sub menu open state is not reset
619        MenuState::mark_shown(&ctx, id);
620
621        // If a submenu is open, the CloseBehavior is handled there
622        let is_any_submenu_open = !MenuState::is_deepest_open_sub_menu(&response.response.ctx, id);
623
624        let should_close = (!is_any_submenu_open && closed_by_click)
625            || ctx.input(|i| i.key_pressed(Key::Escape))
626            || response.response.should_close();
627
628        if should_close {
629            response.response.set_close();
630        }
631
632        match open_kind {
633            OpenKind::Open | OpenKind::Closed => {}
634            OpenKind::Bool(open) => {
635                if should_close {
636                    *open = false;
637                }
638            }
639            OpenKind::Memory { .. } => {
640                if should_close {
641                    ctx.memory_mut(|mem| mem.close_popup(id));
642                }
643            }
644        }
645
646        Some(response)
647    }
648}
649
650/// ## Static methods
651impl Popup<'_> {
652    /// The default ID when constructing a popup from the [`Response`] of e.g. a button.
653    pub fn default_response_id(response: &Response) -> Id {
654        response.id.with("popup")
655    }
656
657    /// Is the given popup open?
658    ///
659    /// This assumes the use of either:
660    /// * [`Self::open_memory`]
661    /// * [`Self::from_toggle_button_response`]
662    /// * [`Self::menu`]
663    /// * [`Self::context_menu`]
664    ///
665    /// The popup id should be the same as either you set with [`Self::id`] or the
666    /// default one from [`Self::default_response_id`].
667    pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
668        ctx.memory(|mem| mem.is_popup_open(popup_id))
669    }
670
671    /// Is any popup open?
672    ///
673    /// This assumes the egui memory is being used to track the open state of popups.
674    pub fn is_any_open(ctx: &Context) -> bool {
675        ctx.memory(|mem| mem.any_popup_open())
676    }
677
678    /// Open the given popup and close all others.
679    pub fn open_id(ctx: &Context, popup_id: Id) {
680        ctx.memory_mut(|mem| mem.open_popup(popup_id));
681    }
682
683    /// Toggle the given popup between closed and open.
684    ///
685    /// Note: At most, only one popup can be open at a time.
686    pub fn toggle_id(ctx: &Context, popup_id: Id) {
687        ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
688    }
689
690    /// Close all currently open popups.
691    pub fn close_all(ctx: &Context) {
692        ctx.memory_mut(|mem| mem.close_all_popups());
693    }
694
695    /// Close the given popup, if it is open.
696    ///
697    /// See also [`Self::close_all`] if you want to close any / all currently open popups.
698    pub fn close_id(ctx: &Context, popup_id: Id) {
699        ctx.memory_mut(|mem| mem.close_popup(popup_id));
700    }
701
702    /// Get the position for this popup, if it is open.
703    pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
704        ctx.memory(|mem| mem.popup_position(popup_id))
705    }
706}