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)?.size
462 }
463
464 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 .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 pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
502 let id = self.id;
503 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 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 MenuState::mark_shown(&ctx, id);
610
611 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
640impl Popup<'_> {
642 pub fn default_response_id(response: &Response) -> Id {
644 response.id.with("popup")
645 }
646
647 pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
658 ctx.memory(|mem| mem.is_popup_open(popup_id))
659 }
660
661 pub fn is_any_open(ctx: &Context) -> bool {
665 ctx.memory(|mem| mem.any_popup_open())
666 }
667
668 pub fn open_id(ctx: &Context, popup_id: Id) {
674 ctx.memory_mut(|mem| mem.open_popup(popup_id));
675 }
676
677 pub fn toggle_id(ctx: &Context, popup_id: Id) {
681 ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
682 }
683
684 pub fn close_all(ctx: &Context) {
686 ctx.memory_mut(|mem| mem.close_all_popups());
687 }
688
689 pub fn close_id(ctx: &Context, popup_id: Id) {
693 ctx.memory_mut(|mem| mem.close_popup(popup_id));
694 }
695
696 pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
698 ctx.memory(|mem| mem.popup_position(popup_id))
699 }
700}