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}