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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum PopupAnchor {
25 ParentRect(Rect),
27
28 Pointer,
30
31 PointerFixed,
33
34 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 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 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#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
77pub enum PopupCloseBehavior {
78 #[default]
82 CloseOnClick,
83
84 CloseOnClickOutside,
87
88 IgnoreClicks,
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum SetOpenCommand {
95 Bool(bool),
97
98 Toggle,
100}
101
102impl From<bool> for SetOpenCommand {
103 fn from(b: bool) -> Self {
104 Self::Bool(b)
105 }
106}
107
108enum OpenKind<'a> {
110 Open,
112
113 Closed,
115
116 Bool(&'a mut bool),
118
119 Memory { set: Option<SetOpenCommand> },
121}
122
123impl OpenKind<'_> {
124 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum PopupKind {
138 Popup,
139 Tooltip,
140 Menu,
141}
142
143impl PopupKind {
144 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#[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: f32,
179
180 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 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 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]
378 pub fn interactable(mut self, interactable: bool) -> Self {
379 self.interactable = interactable;
380 self
381 }
382
383 #[inline]
385 pub fn sense(mut self, sense: Sense) -> Self {
386 self.sense = sense;
387 self
388 }
389
390 #[inline]
392 pub fn layout(mut self, layout: Layout) -> Self {
393 self.layout = layout;
394 self
395 }
396
397 #[inline]
399 pub fn width(mut self, width: f32) -> Self {
400 self.width = Some(width);
401 self
402 }
403
404 #[inline]
406 pub fn id(mut self, id: Id) -> Self {
407 self.id = id;
408 self
409 }
410
411 #[inline]
417 pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
418 self.style = style.into();
419 self
420 }
421
422 pub fn ctx(&self) -> &Context {
424 &self.ctx
425 }
426
427 pub fn get_anchor(&self) -> PopupAnchor {
429 self.anchor
430 }
431
432 pub fn get_anchor_rect(&self) -> Option<Rect> {
436 self.anchor.rect(self.id, &self.ctx)
437 }
438
439 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 pub fn get_id(&self) -> Id {
455 self.id
456 }
457
458 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 pub fn get_expected_size(&self) -> Option<Vec2> {
470 AreaState::load(&self.ctx, self.id)?.size
471 }
472
473 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 .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 pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
509 let id = self.id;
510 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 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 MenuState::mark_shown(&ctx, id);
620
621 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
650impl Popup<'_> {
652 pub fn default_response_id(response: &Response) -> Id {
654 response.id.with("popup")
655 }
656
657 pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
668 ctx.memory(|mem| mem.is_popup_open(popup_id))
669 }
670
671 pub fn is_any_open(ctx: &Context) -> bool {
675 ctx.memory(|mem| mem.any_popup_open())
676 }
677
678 pub fn open_id(ctx: &Context, popup_id: Id) {
680 ctx.memory_mut(|mem| mem.open_popup(popup_id));
681 }
682
683 pub fn toggle_id(ctx: &Context, popup_id: Id) {
687 ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
688 }
689
690 pub fn close_all(ctx: &Context) {
692 ctx.memory_mut(|mem| mem.close_all_popups());
693 }
694
695 pub fn close_id(ctx: &Context, popup_id: Id) {
699 ctx.memory_mut(|mem| mem.close_popup(popup_id));
700 }
701
702 pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
704 ctx.memory(|mem| mem.popup_position(popup_id))
705 }
706}