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 widget_clicked_elsewhere: bool,
184
185 width: Option<f32>,
187 sense: Sense,
188 layout: Layout,
189 frame: Option<Frame>,
190 style: StyleModifier,
191}
192
193impl<'a> Popup<'a> {
194 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 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 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 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 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 Some(SetOpenCommand::Bool(false))
262 } else {
263 None
264 })
265 .at_pointer_fixed()
266 }
267
268 #[inline]
270 pub fn kind(mut self, kind: PopupKind) -> Self {
271 self.kind = kind;
272 self
273 }
274
275 #[inline]
277 pub fn info(mut self, info: UiStackInfo) -> Self {
278 self.info = Some(info);
279 self
280 }
281
282 #[inline]
286 pub fn align(mut self, position_align: RectAlign) -> Self {
287 self.rect_align = position_align;
288 self
289 }
290
291 #[inline]
295 pub fn align_alternatives(mut self, alternatives: &'a [RectAlign]) -> Self {
296 self.alternative_aligns = Some(alternatives);
297 self
298 }
299
300 #[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 #[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 #[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 #[inline]
332 pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
333 self.close_behavior = close_behavior;
334 self
335 }
336
337 #[inline]
339 pub fn at_pointer(mut self) -> Self {
340 self.anchor = PopupAnchor::Pointer;
341 self
342 }
343
344 #[inline]
347 pub fn at_pointer_fixed(mut self) -> Self {
348 self.anchor = PopupAnchor::PointerFixed;
349 self
350 }
351
352 #[inline]
354 pub fn at_position(mut self, position: Pos2) -> Self {
355 self.anchor = PopupAnchor::Position(position);
356 self
357 }
358
359 #[inline]
361 pub fn anchor(mut self, anchor: impl Into<PopupAnchor>) -> Self {
362 self.anchor = anchor.into();
363 self
364 }
365
366 #[inline]
368 pub fn gap(mut self, gap: f32) -> Self {
369 self.gap = gap;
370 self
371 }
372
373 #[inline]
375 pub fn frame(mut self, frame: Frame) -> Self {
376 self.frame = Some(frame);
377 self
378 }
379
380 #[inline]
382 pub fn sense(mut self, sense: Sense) -> Self {
383 self.sense = sense;
384 self
385 }
386
387 #[inline]
389 pub fn layout(mut self, layout: Layout) -> Self {
390 self.layout = layout;
391 self
392 }
393
394 #[inline]
396 pub fn width(mut self, width: f32) -> Self {
397 self.width = Some(width);
398 self
399 }
400
401 #[inline]
403 pub fn id(mut self, id: Id) -> Self {
404 self.id = id;
405 self
406 }
407
408 #[inline]
414 pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
415 self.style = style.into();
416 self
417 }
418
419 pub fn ctx(&self) -> &Context {
421 &self.ctx
422 }
423
424 pub fn get_anchor(&self) -> PopupAnchor {
426 self.anchor
427 }
428
429 pub fn get_anchor_rect(&self) -> Option<Rect> {
433 self.anchor.rect(self.id, &self.ctx)
434 }
435
436 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 pub fn get_id(&self) -> Id {
452 self.id
453 }
454
455 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 pub fn get_expected_size(&self) -> Option<Vec2> {
467 AreaState::load(&self.ctx, self.id).and_then(|area| area.size)
468 }
469
470 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 .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 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 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
634impl Popup<'_> {
636 pub fn default_response_id(response: &Response) -> Id {
638 response.id.with("popup")
639 }
640
641 pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
652 ctx.memory(|mem| mem.is_popup_open(popup_id))
653 }
654
655 pub fn is_any_open(ctx: &Context) -> bool {
659 ctx.memory(|mem| mem.any_popup_open())
660 }
661
662 pub fn open_id(ctx: &Context, popup_id: Id) {
668 ctx.memory_mut(|mem| mem.open_popup(popup_id));
669 }
670
671 pub fn toggle_id(ctx: &Context, popup_id: Id) {
675 ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
676 }
677
678 pub fn close_all(ctx: &Context) {
680 ctx.memory_mut(|mem| mem.close_all_popups());
681 }
682
683 pub fn close_id(ctx: &Context, popup_id: Id) {
687 ctx.memory_mut(|mem| mem.close_popup(popup_id));
688 }
689
690 pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
692 ctx.memory(|mem| mem.popup_position(popup_id))
693 }
694}