1#![expect(deprecated)] use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum PopupAnchor {
27    ParentRect(Rect),
29
30    Pointer,
32
33    PointerFixed,
35
36    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        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    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#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
79pub enum PopupCloseBehavior {
80    #[default]
84    CloseOnClick,
85
86    CloseOnClickOutside,
89
90    IgnoreClicks,
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum SetOpenCommand {
97    Bool(bool),
99
100    Toggle,
102}
103
104impl From<bool> for SetOpenCommand {
105    fn from(b: bool) -> Self {
106        Self::Bool(b)
107    }
108}
109
110enum OpenKind<'a> {
112    Open,
114
115    Closed,
117
118    Bool(&'a mut bool),
120
121    Memory { set: Option<SetOpenCommand> },
123}
124
125impl OpenKind<'_> {
126    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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub enum PopupKind {
140    Popup,
141    Tooltip,
142    Menu,
143}
144
145impl PopupKind {
146    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#[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: f32,
181
182    width: Option<f32>,
184    sense: Sense,
185    layout: Layout,
186    frame: Option<Frame>,
187    style: StyleModifier,
188}
189
190impl<'a> Popup<'a> {
191    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    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    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    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    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                Some(SetOpenCommand::Bool(false))
256            } else {
257                None
258            })
259            .at_pointer_fixed()
260    }
261
262    #[inline]
264    pub fn kind(mut self, kind: PopupKind) -> Self {
265        self.kind = kind;
266        self
267    }
268
269    #[inline]
271    pub fn info(mut self, info: UiStackInfo) -> Self {
272        self.info = Some(info);
273        self
274    }
275
276    #[inline]
280    pub fn align(mut self, position_align: RectAlign) -> Self {
281        self.rect_align = position_align;
282        self
283    }
284
285    #[inline]
289    pub fn align_alternatives(mut self, alternatives: &'a [RectAlign]) -> Self {
290        self.alternative_aligns = Some(alternatives);
291        self
292    }
293
294    #[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    #[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    #[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    #[inline]
326    pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
327        self.close_behavior = close_behavior;
328        self
329    }
330
331    #[inline]
333    pub fn at_pointer(mut self) -> Self {
334        self.anchor = PopupAnchor::Pointer;
335        self
336    }
337
338    #[inline]
341    pub fn at_pointer_fixed(mut self) -> Self {
342        self.anchor = PopupAnchor::PointerFixed;
343        self
344    }
345
346    #[inline]
348    pub fn at_position(mut self, position: Pos2) -> Self {
349        self.anchor = PopupAnchor::Position(position);
350        self
351    }
352
353    #[inline]
355    pub fn anchor(mut self, anchor: impl Into<PopupAnchor>) -> Self {
356        self.anchor = anchor.into();
357        self
358    }
359
360    #[inline]
362    pub fn gap(mut self, gap: f32) -> Self {
363        self.gap = gap;
364        self
365    }
366
367    #[inline]
369    pub fn frame(mut self, frame: Frame) -> Self {
370        self.frame = Some(frame);
371        self
372    }
373
374    #[inline]
376    pub fn sense(mut self, sense: Sense) -> Self {
377        self.sense = sense;
378        self
379    }
380
381    #[inline]
383    pub fn layout(mut self, layout: Layout) -> Self {
384        self.layout = layout;
385        self
386    }
387
388    #[inline]
390    pub fn width(mut self, width: f32) -> Self {
391        self.width = Some(width);
392        self
393    }
394
395    #[inline]
397    pub fn id(mut self, id: Id) -> Self {
398        self.id = id;
399        self
400    }
401
402    #[inline]
408    pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
409        self.style = style.into();
410        self
411    }
412
413    pub fn ctx(&self) -> &Context {
415        &self.ctx
416    }
417
418    pub fn get_anchor(&self) -> PopupAnchor {
420        self.anchor
421    }
422
423    pub fn get_anchor_rect(&self) -> Option<Rect> {
427        self.anchor.rect(self.id, &self.ctx)
428    }
429
430    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    pub fn get_id(&self) -> Id {
446        self.id
447    }
448
449    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    pub fn get_expected_size(&self) -> Option<Vec2> {
461        AreaState::load(&self.ctx, self.id).and_then(|area| area.size)
462    }
463
464    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                    .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    pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
501        let id = self.id;
502        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        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        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
636impl Popup<'_> {
638    pub fn default_response_id(response: &Response) -> Id {
640        response.id.with("popup")
641    }
642
643    pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
654        ctx.memory(|mem| mem.is_popup_open(popup_id))
655    }
656
657    pub fn is_any_open(ctx: &Context) -> bool {
661        ctx.memory(|mem| mem.any_popup_open())
662    }
663
664    pub fn open_id(ctx: &Context, popup_id: Id) {
670        ctx.memory_mut(|mem| mem.open_popup(popup_id));
671    }
672
673    pub fn toggle_id(ctx: &Context, popup_id: Id) {
677        ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
678    }
679
680    pub fn close_all(ctx: &Context) {
682        ctx.memory_mut(|mem| mem.close_all_popups());
683    }
684
685    pub fn close_id(ctx: &Context, popup_id: Id) {
689        ctx.memory_mut(|mem| mem.close_popup(popup_id));
690    }
691
692    pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
694        ctx.memory(|mem| mem.popup_position(popup_id))
695    }
696}