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    /// Used later depending on close behavior
183    widget_clicked_elsewhere: bool,
184
185    /// Default width passed to the Area
186    width: Option<f32>,
187    sense: Sense,
188    layout: Layout,
189    frame: Option<Frame>,
190    style: StyleModifier,
191}
192
193impl<'a> Popup<'a> {
194    /// Create a new popup
195    pub fn new(id: Id, ctx: Context, anchor: impl Into<PopupAnchor>, layer_id: LayerId) -> Self {
196        Self {
197            id,
198            ctx,
199            anchor: anchor.into(),
200            open_kind: OpenKind::Open,
201            close_behavior: PopupCloseBehavior::default(),
202            info: None,
203            kind: PopupKind::Popup,
204            layer_id,
205            rect_align: RectAlign::BOTTOM_START,
206            alternative_aligns: None,
207            gap: 0.0,
208            widget_clicked_elsewhere: false,
209            width: None,
210            sense: Sense::click(),
211            layout: Layout::default(),
212            frame: None,
213            style: StyleModifier::default(),
214        }
215    }
216
217    /// Show a popup relative to some widget.
218    /// The popup will be always open.
219    ///
220    /// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
221    pub fn from_response(response: &Response) -> Self {
222        let mut popup = Self::new(
223            Self::default_response_id(response),
224            response.ctx.clone(),
225            response,
226            response.layer_id,
227        );
228        popup.widget_clicked_elsewhere = response.clicked_elsewhere();
229        popup
230    }
231
232    /// Show a popup relative to some widget,
233    /// toggling the open state based on the widget's click state.
234    ///
235    /// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
236    pub fn from_toggle_button_response(button_response: &Response) -> Self {
237        Self::from_response(button_response)
238            .open_memory(button_response.clicked().then_some(SetOpenCommand::Toggle))
239    }
240
241    /// Show a popup when the widget was clicked.
242    /// Sets the layout to `Layout::top_down_justified(Align::Min)`.
243    pub fn menu(button_response: &Response) -> Self {
244        Self::from_toggle_button_response(button_response)
245            .kind(PopupKind::Menu)
246            .layout(Layout::top_down_justified(Align::Min))
247            .style(menu_style)
248            .gap(0.0)
249    }
250
251    /// Show a context menu when the widget was secondary clicked.
252    /// Sets the layout to `Layout::top_down_justified(Align::Min)`.
253    /// In contrast to [`Self::menu`], this will open at the pointer position.
254    pub fn context_menu(response: &Response) -> Self {
255        Self::menu(response)
256            .open_memory(if response.secondary_clicked() {
257                Some(SetOpenCommand::Bool(true))
258            } else if response.clicked() {
259                // Explicitly close the menu if the widget was clicked
260                // Without this, the context menu would stay open if the user clicks the widget
261                Some(SetOpenCommand::Bool(false))
262            } else {
263                None
264            })
265            .at_pointer_fixed()
266    }
267
268    /// Set the kind of the popup. Used for [`Area::kind`] and [`Area::order`].
269    #[inline]
270    pub fn kind(mut self, kind: PopupKind) -> Self {
271        self.kind = kind;
272        self
273    }
274
275    /// Set the [`UiStackInfo`] of the popup's [`Ui`].
276    #[inline]
277    pub fn info(mut self, info: UiStackInfo) -> Self {
278        self.info = Some(info);
279        self
280    }
281
282    /// Set the [`RectAlign`] of the popup relative to the [`PopupAnchor`].
283    /// This is the default position, and will be used if it fits.
284    /// See [`Self::align_alternatives`] for more on this.
285    #[inline]
286    pub fn align(mut self, position_align: RectAlign) -> Self {
287        self.rect_align = position_align;
288        self
289    }
290
291    /// Set alternative positions to try if the default one doesn't fit. Set to an empty slice to
292    /// always use the position you set with [`Self::align`].
293    /// By default, this will try [`RectAlign::symmetries`] and then [`RectAlign::MENU_ALIGNS`].
294    #[inline]
295    pub fn align_alternatives(mut self, alternatives: &'a [RectAlign]) -> Self {
296        self.alternative_aligns = Some(alternatives);
297        self
298    }
299
300    /// Force the popup to be open or closed.
301    #[inline]
302    pub fn open(mut self, open: bool) -> Self {
303        self.open_kind = if open {
304            OpenKind::Open
305        } else {
306            OpenKind::Closed
307        };
308        self
309    }
310
311    /// Store the open state via [`crate::Memory`].
312    /// You can set the state via the first [`SetOpenCommand`] param.
313    #[inline]
314    pub fn open_memory(mut self, set_state: impl Into<Option<SetOpenCommand>>) -> Self {
315        self.open_kind = OpenKind::Memory {
316            set: set_state.into(),
317        };
318        self
319    }
320
321    /// Store the open state via a mutable bool.
322    #[inline]
323    pub fn open_bool(mut self, open: &'a mut bool) -> Self {
324        self.open_kind = OpenKind::Bool(open);
325        self
326    }
327
328    /// Set the close behavior of the popup.
329    ///
330    /// This will do nothing if [`Popup::open`] was called.
331    #[inline]
332    pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
333        self.close_behavior = close_behavior;
334        self
335    }
336
337    /// Show the popup relative to the pointer.
338    #[inline]
339    pub fn at_pointer(mut self) -> Self {
340        self.anchor = PopupAnchor::Pointer;
341        self
342    }
343
344    /// Remember the pointer position at the time of opening the popup, and show the popup
345    /// relative to that.
346    #[inline]
347    pub fn at_pointer_fixed(mut self) -> Self {
348        self.anchor = PopupAnchor::PointerFixed;
349        self
350    }
351
352    /// Show the popup relative to a specific position.
353    #[inline]
354    pub fn at_position(mut self, position: Pos2) -> Self {
355        self.anchor = PopupAnchor::Position(position);
356        self
357    }
358
359    /// Show the popup relative to the given [`PopupAnchor`].
360    #[inline]
361    pub fn anchor(mut self, anchor: impl Into<PopupAnchor>) -> Self {
362        self.anchor = anchor.into();
363        self
364    }
365
366    /// Set the gap between the anchor and the popup.
367    #[inline]
368    pub fn gap(mut self, gap: f32) -> Self {
369        self.gap = gap;
370        self
371    }
372
373    /// Set the frame of the popup.
374    #[inline]
375    pub fn frame(mut self, frame: Frame) -> Self {
376        self.frame = Some(frame);
377        self
378    }
379
380    /// Set the sense of the popup.
381    #[inline]
382    pub fn sense(mut self, sense: Sense) -> Self {
383        self.sense = sense;
384        self
385    }
386
387    /// Set the layout of the popup.
388    #[inline]
389    pub fn layout(mut self, layout: Layout) -> Self {
390        self.layout = layout;
391        self
392    }
393
394    /// The width that will be passed to [`Area::default_width`].
395    #[inline]
396    pub fn width(mut self, width: f32) -> Self {
397        self.width = Some(width);
398        self
399    }
400
401    /// Set the id of the Area.
402    #[inline]
403    pub fn id(mut self, id: Id) -> Self {
404        self.id = id;
405        self
406    }
407
408    /// Set the style for the popup contents.
409    ///
410    /// Default:
411    /// - is [`menu_style`] for [`Self::menu`] and [`Self::context_menu`]
412    /// - is [`None`] otherwise
413    #[inline]
414    pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
415        self.style = style.into();
416        self
417    }
418
419    /// Get the [`Context`]
420    pub fn ctx(&self) -> &Context {
421        &self.ctx
422    }
423
424    /// Return the [`PopupAnchor`] of the popup.
425    pub fn get_anchor(&self) -> PopupAnchor {
426        self.anchor
427    }
428
429    /// Return the anchor rect of the popup.
430    ///
431    /// Returns `None` if the anchor is [`PopupAnchor::Pointer`] and there is no pointer.
432    pub fn get_anchor_rect(&self) -> Option<Rect> {
433        self.anchor.rect(self.id, &self.ctx)
434    }
435
436    /// Get the expected rect the popup will be shown in.
437    ///
438    /// Returns `None` if the popup wasn't shown before or anchor is `PopupAnchor::Pointer` and
439    /// there is no pointer.
440    pub fn get_popup_rect(&self) -> Option<Rect> {
441        let size = self.get_expected_size();
442        if let Some(size) = size {
443            self.get_anchor_rect()
444                .map(|anchor| self.get_best_align().align_rect(&anchor, size, self.gap))
445        } else {
446            None
447        }
448    }
449
450    /// Get the id of the popup.
451    pub fn get_id(&self) -> Id {
452        self.id
453    }
454
455    /// Is the popup open?
456    pub fn is_open(&self) -> bool {
457        match &self.open_kind {
458            OpenKind::Open => true,
459            OpenKind::Closed => false,
460            OpenKind::Bool(open) => **open,
461            OpenKind::Memory { .. } => Self::is_id_open(&self.ctx, self.id),
462        }
463    }
464
465    /// Get the expected size of the popup.
466    pub fn get_expected_size(&self) -> Option<Vec2> {
467        AreaState::load(&self.ctx, self.id).and_then(|area| area.size)
468    }
469
470    /// Calculate the best alignment for the popup, based on the last size and screen rect.
471    pub fn get_best_align(&self) -> RectAlign {
472        let expected_popup_size = self
473            .get_expected_size()
474            .unwrap_or(vec2(self.width.unwrap_or(0.0), 0.0));
475
476        let Some(anchor_rect) = self.anchor.rect(self.id, &self.ctx) else {
477            return self.rect_align;
478        };
479
480        RectAlign::find_best_align(
481            #[expect(clippy::iter_on_empty_collections)]
482            once(self.rect_align).chain(
483                self.alternative_aligns
484                    // Need the empty slice so the iters have the same type so we can unwrap_or
485                    .map(|a| a.iter().copied().chain([].iter().copied()))
486                    .unwrap_or(
487                        self.rect_align
488                            .symmetries()
489                            .iter()
490                            .copied()
491                            .chain(RectAlign::MENU_ALIGNS.iter().copied()),
492                    ),
493            ),
494            self.ctx.screen_rect(),
495            anchor_rect,
496            self.gap,
497            expected_popup_size,
498        )
499        .unwrap_or_default()
500    }
501
502    /// Show the popup.
503    /// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
504    /// no pointer.
505    pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
506        let hover_pos = self.ctx.pointer_hover_pos();
507
508        let id = self.id;
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            widget_clicked_elsewhere,
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        let closed_by_click = match close_behavior {
598            PopupCloseBehavior::CloseOnClick => widget_clicked_elsewhere,
599            PopupCloseBehavior::CloseOnClickOutside => {
600                widget_clicked_elsewhere && response.response.clicked_elsewhere()
601            }
602            PopupCloseBehavior::IgnoreClicks => false,
603        };
604
605        // If a submenu is open, the CloseBehavior is handled there
606        let is_any_submenu_open = !MenuState::is_deepest_sub_menu(&response.response.ctx, id);
607
608        let should_close = (!is_any_submenu_open && closed_by_click)
609            || ctx.input(|i| i.key_pressed(Key::Escape))
610            || response.response.should_close();
611
612        if should_close {
613            response.response.set_close();
614        }
615
616        match open_kind {
617            OpenKind::Open | OpenKind::Closed => {}
618            OpenKind::Bool(open) => {
619                if should_close {
620                    *open = false;
621                }
622            }
623            OpenKind::Memory { .. } => {
624                if should_close {
625                    ctx.memory_mut(|mem| mem.close_popup(id));
626                }
627            }
628        }
629
630        Some(response)
631    }
632}
633
634/// ## Static methods
635impl Popup<'_> {
636    /// The default ID when constructing a popup from the [`Response`] of e.g. a button.
637    pub fn default_response_id(response: &Response) -> Id {
638        response.id.with("popup")
639    }
640
641    /// Is the given popup open?
642    ///
643    /// This assumes the use of either:
644    /// * [`Self::open_memory`]
645    /// * [`Self::from_toggle_button_response`]
646    /// * [`Self::menu`]
647    /// * [`Self::context_menu`]
648    ///
649    /// The popup id should be the same as either you set with [`Self::id`] or the
650    /// default one from [`Self::default_response_id`].
651    pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
652        ctx.memory(|mem| mem.is_popup_open(popup_id))
653    }
654
655    /// Is any popup open?
656    ///
657    /// This assumes the egui memory is being used to track the open state of popups.
658    pub fn is_any_open(ctx: &Context) -> bool {
659        ctx.memory(|mem| mem.any_popup_open())
660    }
661
662    /// Open the given popup and close all others.
663    ///
664    /// If you are NOT using [`Popup::show`], you must
665    /// also call [`crate::Memory::keep_popup_open`] as long as
666    /// you're showing the popup.
667    pub fn open_id(ctx: &Context, popup_id: Id) {
668        ctx.memory_mut(|mem| mem.open_popup(popup_id));
669    }
670
671    /// Toggle the given popup between closed and open.
672    ///
673    /// Note: At most, only one popup can be open at a time.
674    pub fn toggle_id(ctx: &Context, popup_id: Id) {
675        ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
676    }
677
678    /// Close all currently open popups.
679    pub fn close_all(ctx: &Context) {
680        ctx.memory_mut(|mem| mem.close_all_popups());
681    }
682
683    /// Close the given popup, if it is open.
684    ///
685    /// See also [`Self::close_all`] if you want to close any / all currently open popups.
686    pub fn close_id(ctx: &Context, popup_id: Id) {
687        ctx.memory_mut(|mem| mem.close_popup(popup_id));
688    }
689
690    /// Get the position for this popup, if it is open.
691    pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
692        ctx.memory(|mem| mem.popup_position(popup_id))
693    }
694}