1use super::{
19 style::WidgetVisuals, Align, Context, Id, InnerResponse, PointerState, Pos2, Rect, Response,
20 Sense, TextStyle, Ui, Vec2,
21};
22use crate::{
23 epaint, vec2,
24 widgets::{Button, ImageButton},
25 Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt, Order, Stroke, Style, TextWrapMode,
26 UiKind, WidgetText,
27};
28use epaint::mutex::RwLock;
29use std::sync::Arc;
30
31#[derive(Clone, Default)]
33pub struct BarState {
34 open_menu: MenuRootManager,
35}
36
37impl BarState {
38 pub fn load(ctx: &Context, bar_id: Id) -> Self {
39 ctx.data_mut(|d| d.get_temp::<Self>(bar_id).unwrap_or_default())
40 }
41
42 pub fn store(self, ctx: &Context, bar_id: Id) {
43 ctx.data_mut(|d| d.insert_temp(bar_id, self));
44 }
45
46 pub fn bar_menu<R>(
50 &mut self,
51 button: &Response,
52 add_contents: impl FnOnce(&mut Ui) -> R,
53 ) -> Option<InnerResponse<R>> {
54 MenuRoot::stationary_click_interaction(button, &mut self.open_menu);
55 self.open_menu.show(button, add_contents)
56 }
57
58 pub(crate) fn has_root(&self) -> bool {
59 self.open_menu.inner.is_some()
60 }
61}
62
63impl std::ops::Deref for BarState {
64 type Target = MenuRootManager;
65
66 fn deref(&self) -> &Self::Target {
67 &self.open_menu
68 }
69}
70
71impl std::ops::DerefMut for BarState {
72 fn deref_mut(&mut self) -> &mut Self::Target {
73 &mut self.open_menu
74 }
75}
76
77fn set_menu_style(style: &mut Style) {
78 style.spacing.button_padding = vec2(2.0, 0.0);
79 style.visuals.widgets.active.bg_stroke = Stroke::NONE;
80 style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
81 style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
82 style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
83}
84
85pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
89 ui.horizontal(|ui| {
90 set_menu_style(ui.style_mut());
91
92 let height = ui.spacing().interact_size.y;
94 ui.set_min_size(vec2(ui.available_width(), height));
95
96 add_contents(ui)
97 })
98}
99
100pub fn menu_button<R>(
106 ui: &mut Ui,
107 title: impl Into<WidgetText>,
108 add_contents: impl FnOnce(&mut Ui) -> R,
109) -> InnerResponse<Option<R>> {
110 stationary_menu_impl(ui, title, Box::new(add_contents))
111}
112
113pub fn menu_custom_button<R>(
119 ui: &mut Ui,
120 button: Button<'_>,
121 add_contents: impl FnOnce(&mut Ui) -> R,
122) -> InnerResponse<Option<R>> {
123 stationary_menu_button_impl(ui, button, Box::new(add_contents))
124}
125
126#[deprecated = "Use `menu_custom_button` instead"]
132pub fn menu_image_button<R>(
133 ui: &mut Ui,
134 image_button: ImageButton<'_>,
135 add_contents: impl FnOnce(&mut Ui) -> R,
136) -> InnerResponse<Option<R>> {
137 stationary_menu_button_impl(
138 ui,
139 Button::image(image_button.image),
140 Box::new(add_contents),
141 )
142}
143
144pub(crate) fn submenu_button<R>(
150 ui: &mut Ui,
151 parent_state: Arc<RwLock<MenuState>>,
152 title: impl Into<WidgetText>,
153 add_contents: impl FnOnce(&mut Ui) -> R,
154) -> InnerResponse<Option<R>> {
155 SubMenu::new(parent_state, title).show(ui, add_contents)
156}
157
158fn menu_popup<'c, R>(
160 ctx: &Context,
161 parent_layer: LayerId,
162 menu_state_arc: &Arc<RwLock<MenuState>>,
163 menu_id: Id,
164 add_contents: impl FnOnce(&mut Ui) -> R + 'c,
165) -> InnerResponse<R> {
166 let pos = {
167 let mut menu_state = menu_state_arc.write();
168 menu_state.entry_count = 0;
169 menu_state.rect.min
170 };
171
172 let area_id = menu_id.with("__menu");
173
174 ctx.pass_state_mut(|fs| {
175 fs.layers
176 .entry(parent_layer)
177 .or_default()
178 .open_popups
179 .insert(area_id)
180 });
181
182 let area = Area::new(area_id)
183 .kind(UiKind::Menu)
184 .order(Order::Foreground)
185 .fixed_pos(pos)
186 .default_width(ctx.style().spacing.menu_width)
187 .sense(Sense::hover());
188
189 let mut sizing_pass = false;
190
191 let area_response = area.show(ctx, |ui| {
192 sizing_pass = ui.is_sizing_pass();
193
194 set_menu_style(ui.style_mut());
195
196 Frame::menu(ui.style())
197 .show(ui, |ui| {
198 ui.set_menu_state(Some(menu_state_arc.clone()));
199 ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents)
200 .inner
201 })
202 .inner
203 });
204
205 let area_rect = area_response.response.rect;
206
207 menu_state_arc.write().rect = if sizing_pass {
208 Rect::from_min_size(pos, area_rect.size())
212 } else {
213 area_rect
216 };
217
218 area_response
219}
220
221fn stationary_menu_impl<'c, R>(
225 ui: &mut Ui,
226 title: impl Into<WidgetText>,
227 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
228) -> InnerResponse<Option<R>> {
229 let title = title.into();
230 let bar_id = ui.id();
231 let menu_id = bar_id.with(title.text());
232
233 let mut bar_state = BarState::load(ui.ctx(), bar_id);
234
235 let mut button = Button::new(title);
236
237 if bar_state.open_menu.is_menu_open(menu_id) {
238 button = button.fill(ui.visuals().widgets.open.weak_bg_fill);
239 button = button.stroke(ui.visuals().widgets.open.bg_stroke);
240 }
241
242 let button_response = ui.add(button);
243 let inner = bar_state.bar_menu(&button_response, add_contents);
244
245 bar_state.store(ui.ctx(), bar_id);
246 InnerResponse::new(inner.map(|r| r.inner), button_response)
247}
248
249fn stationary_menu_button_impl<'c, R>(
253 ui: &mut Ui,
254 button: Button<'_>,
255 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
256) -> InnerResponse<Option<R>> {
257 let bar_id = ui.id();
258
259 let mut bar_state = BarState::load(ui.ctx(), bar_id);
260 let button_response = ui.add(button);
261 let inner = bar_state.bar_menu(&button_response, add_contents);
262
263 bar_state.store(ui.ctx(), bar_id);
264 InnerResponse::new(inner.map(|r| r.inner), button_response)
265}
266
267pub(crate) const CONTEXT_MENU_ID_STR: &str = "__egui::context_menu";
268
269pub(crate) fn context_menu(
271 response: &Response,
272 add_contents: impl FnOnce(&mut Ui),
273) -> Option<InnerResponse<()>> {
274 let menu_id = Id::new(CONTEXT_MENU_ID_STR);
275 let mut bar_state = BarState::load(&response.ctx, menu_id);
276
277 MenuRoot::context_click_interaction(response, &mut bar_state);
278 let inner_response = bar_state.show(response, add_contents);
279
280 bar_state.store(&response.ctx, menu_id);
281 inner_response
282}
283
284pub(crate) fn context_menu_opened(response: &Response) -> bool {
286 let menu_id = Id::new(CONTEXT_MENU_ID_STR);
287 let bar_state = BarState::load(&response.ctx, menu_id);
288 bar_state.is_menu_open(response.id)
289}
290
291#[derive(Clone, Default)]
293pub struct MenuRootManager {
294 inner: Option<MenuRoot>,
295}
296
297impl MenuRootManager {
298 pub fn show<R>(
302 &mut self,
303 button: &Response,
304 add_contents: impl FnOnce(&mut Ui) -> R,
305 ) -> Option<InnerResponse<R>> {
306 if let Some(root) = self.inner.as_mut() {
307 let (menu_response, inner_response) = root.show(button, add_contents);
308 if menu_response.is_close() {
309 self.inner = None;
310 }
311 inner_response
312 } else {
313 None
314 }
315 }
316
317 fn is_menu_open(&self, id: Id) -> bool {
318 self.inner.as_ref().map(|m| m.id) == Some(id)
319 }
320}
321
322impl std::ops::Deref for MenuRootManager {
323 type Target = Option<MenuRoot>;
324
325 fn deref(&self) -> &Self::Target {
326 &self.inner
327 }
328}
329
330impl std::ops::DerefMut for MenuRootManager {
331 fn deref_mut(&mut self) -> &mut Self::Target {
332 &mut self.inner
333 }
334}
335
336#[derive(Clone)]
338pub struct MenuRoot {
339 pub menu_state: Arc<RwLock<MenuState>>,
340 pub id: Id,
341}
342
343impl MenuRoot {
344 pub fn new(position: Pos2, id: Id) -> Self {
345 Self {
346 menu_state: Arc::new(RwLock::new(MenuState::new(position))),
347 id,
348 }
349 }
350
351 pub fn show<R>(
352 &self,
353 button: &Response,
354 add_contents: impl FnOnce(&mut Ui) -> R,
355 ) -> (MenuResponse, Option<InnerResponse<R>>) {
356 if self.id == button.id {
357 let inner_response = menu_popup(
358 &button.ctx,
359 button.layer_id,
360 &self.menu_state,
361 self.id,
362 add_contents,
363 );
364 let menu_state = self.menu_state.read();
365
366 let escape_pressed = button.ctx.input(|i| i.key_pressed(Key::Escape));
367 if menu_state.response.is_close() || escape_pressed {
368 return (MenuResponse::Close, Some(inner_response));
369 }
370 }
371 (MenuResponse::Stay, None)
372 }
373
374 fn stationary_interaction(button: &Response, root: &mut MenuRootManager) -> MenuResponse {
378 let id = button.id;
379
380 if (button.clicked() && root.is_menu_open(id))
381 || button.ctx.input(|i| i.key_pressed(Key::Escape))
382 {
383 return MenuResponse::Close;
385 } else if (button.clicked() && !root.is_menu_open(id))
386 || (button.hovered() && root.is_some())
387 {
388 let mut pos = button.rect.left_bottom();
391
392 let menu_frame = Frame::menu(&button.ctx.style());
393 pos.x -= menu_frame.total_margin().left; pos.y += button.ctx.style().spacing.menu_spacing;
395
396 if let Some(root) = root.inner.as_mut() {
397 let menu_rect = root.menu_state.read().rect;
398 let screen_rect = button.ctx.input(|i| i.screen_rect);
399
400 if pos.y + menu_rect.height() > screen_rect.max.y {
401 pos.y = screen_rect.max.y - menu_rect.height() - button.rect.height();
402 }
403
404 if pos.x + menu_rect.width() > screen_rect.max.x {
405 pos.x = screen_rect.max.x - menu_rect.width();
406 }
407 }
408
409 if let Some(to_global) = button.ctx.layer_transform_to_global(button.layer_id) {
410 pos = to_global * pos;
411 }
412
413 return MenuResponse::Create(pos, id);
414 } else if button
415 .ctx
416 .input(|i| i.pointer.any_pressed() && i.pointer.primary_down())
417 {
418 if let Some(pos) = button.ctx.input(|i| i.pointer.interact_pos()) {
419 if let Some(root) = root.inner.as_mut() {
420 if root.id == id {
421 let in_menu = root.menu_state.read().area_contains(pos);
423 if !in_menu {
424 return MenuResponse::Close;
425 }
426 }
427 }
428 }
429 }
430 MenuResponse::Stay
431 }
432
433 pub fn context_interaction(response: &Response, root: &mut Option<Self>) -> MenuResponse {
435 let response = response.interact(Sense::click());
436 let hovered = response.hovered();
437 let secondary_clicked = response.secondary_clicked();
438
439 response.ctx.input(|input| {
440 let pointer = &input.pointer;
441 if let Some(pos) = pointer.interact_pos() {
442 let mut in_old_menu = false;
443 let mut destroy = false;
444 if let Some(root) = root {
445 in_old_menu = root.menu_state.read().area_contains(pos);
446 destroy = !in_old_menu && pointer.any_pressed() && root.id == response.id;
447 }
448 if !in_old_menu {
449 if hovered && secondary_clicked {
450 return MenuResponse::Create(pos, response.id);
451 } else if destroy || hovered && pointer.primary_down() {
452 return MenuResponse::Close;
453 }
454 }
455 }
456 MenuResponse::Stay
457 })
458 }
459
460 pub fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
461 match menu_response {
462 MenuResponse::Create(pos, id) => {
463 root.inner = Some(Self::new(pos, id));
464 }
465 MenuResponse::Close => root.inner = None,
466 MenuResponse::Stay => {}
467 }
468 }
469
470 pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager) {
472 let menu_response = Self::context_interaction(response, root);
473 Self::handle_menu_response(root, menu_response);
474 }
475
476 pub fn stationary_click_interaction(button: &Response, root: &mut MenuRootManager) {
478 let menu_response = Self::stationary_interaction(button, root);
479 Self::handle_menu_response(root, menu_response);
480 }
481}
482
483#[derive(Copy, Clone, PartialEq, Eq)]
484pub enum MenuResponse {
485 Close,
486 Stay,
487 Create(Pos2, Id),
488}
489
490impl MenuResponse {
491 pub fn is_close(&self) -> bool {
492 *self == Self::Close
493 }
494}
495
496pub struct SubMenuButton {
497 text: WidgetText,
498 icon: WidgetText,
499 index: usize,
500}
501
502impl SubMenuButton {
503 fn new(text: impl Into<WidgetText>, icon: impl Into<WidgetText>, index: usize) -> Self {
505 Self {
506 text: text.into(),
507 icon: icon.into(),
508 index,
509 }
510 }
511
512 fn visuals<'a>(
513 ui: &'a Ui,
514 response: &Response,
515 menu_state: &MenuState,
516 sub_id: Id,
517 ) -> &'a WidgetVisuals {
518 if menu_state.is_open(sub_id) && !response.hovered() {
519 &ui.style().visuals.widgets.open
520 } else {
521 ui.style().interact(response)
522 }
523 }
524
525 #[inline]
526 pub fn icon(mut self, icon: impl Into<WidgetText>) -> Self {
527 self.icon = icon.into();
528 self
529 }
530
531 pub(crate) fn show(self, ui: &mut Ui, menu_state: &MenuState, sub_id: Id) -> Response {
532 let Self { text, icon, .. } = self;
533
534 let text_style = TextStyle::Button;
535 let sense = Sense::click();
536
537 let text_icon_gap = ui.spacing().item_spacing.x;
538 let button_padding = ui.spacing().button_padding;
539 let total_extra = button_padding + button_padding;
540 let text_available_width = ui.available_width() - total_extra.x;
541 let text_galley = text.into_galley(
542 ui,
543 Some(TextWrapMode::Wrap),
544 text_available_width,
545 text_style.clone(),
546 );
547
548 let icon_available_width = text_available_width - text_galley.size().x;
549 let icon_galley = icon.into_galley(
550 ui,
551 Some(TextWrapMode::Wrap),
552 icon_available_width,
553 text_style,
554 );
555 let text_and_icon_size = Vec2::new(
556 text_galley.size().x + text_icon_gap + icon_galley.size().x,
557 text_galley.size().y.max(icon_galley.size().y),
558 );
559 let mut desired_size = text_and_icon_size + 2.0 * button_padding;
560 desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
561
562 let (rect, response) = ui.allocate_at_least(desired_size, sense);
563 response.widget_info(|| {
564 crate::WidgetInfo::labeled(
565 crate::WidgetType::Button,
566 ui.is_enabled(),
567 text_galley.text(),
568 )
569 });
570
571 if ui.is_rect_visible(rect) {
572 let visuals = Self::visuals(ui, &response, menu_state, sub_id);
573 let text_pos = Align2::LEFT_CENTER
574 .align_size_within_rect(text_galley.size(), rect.shrink2(button_padding))
575 .min;
576 let icon_pos = Align2::RIGHT_CENTER
577 .align_size_within_rect(icon_galley.size(), rect.shrink2(button_padding))
578 .min;
579
580 if ui.visuals().button_frame {
581 ui.painter().rect_filled(
582 rect.expand(visuals.expansion),
583 visuals.corner_radius,
584 visuals.weak_bg_fill,
585 );
586 }
587
588 let text_color = visuals.text_color();
589 ui.painter().galley(text_pos, text_galley, text_color);
590 ui.painter().galley(icon_pos, icon_galley, text_color);
591 }
592 response
593 }
594}
595
596pub struct SubMenu {
597 button: SubMenuButton,
598 parent_state: Arc<RwLock<MenuState>>,
599}
600
601impl SubMenu {
602 fn new(parent_state: Arc<RwLock<MenuState>>, text: impl Into<WidgetText>) -> Self {
603 let index = parent_state.write().next_entry_index();
604 Self {
605 button: SubMenuButton::new(text, "⏵", index),
606 parent_state,
607 }
608 }
609
610 pub fn show<R>(
611 self,
612 ui: &mut Ui,
613 add_contents: impl FnOnce(&mut Ui) -> R,
614 ) -> InnerResponse<Option<R>> {
615 let sub_id = ui.id().with(self.button.index);
616 let response = self.button.show(ui, &self.parent_state.read(), sub_id);
617 self.parent_state
618 .write()
619 .submenu_button_interaction(ui, sub_id, &response);
620 let inner =
621 self.parent_state
622 .write()
623 .show_submenu(ui.ctx(), ui.layer_id(), sub_id, add_contents);
624 InnerResponse::new(inner, response)
625 }
626}
627
628pub struct MenuState {
632 sub_menu: Option<(Id, Arc<RwLock<MenuState>>)>,
634
635 pub rect: Rect,
638
639 pub response: MenuResponse,
641
642 entry_count: usize,
644}
645
646impl MenuState {
647 pub fn new(position: Pos2) -> Self {
648 Self {
649 rect: Rect::from_min_size(position, Vec2::ZERO),
650 sub_menu: None,
651 response: MenuResponse::Stay,
652 entry_count: 0,
653 }
654 }
655
656 pub fn close(&mut self) {
658 self.response = MenuResponse::Close;
659 }
660
661 fn show_submenu<R>(
662 &mut self,
663 ctx: &Context,
664 parent_layer: LayerId,
665 id: Id,
666 add_contents: impl FnOnce(&mut Ui) -> R,
667 ) -> Option<R> {
668 let (sub_response, response) = self.submenu(id).map(|sub| {
669 let inner_response = menu_popup(ctx, parent_layer, sub, id, add_contents);
670 (sub.read().response, inner_response.inner)
671 })?;
672 self.cascade_close_response(sub_response);
673 Some(response)
674 }
675
676 pub fn area_contains(&self, pos: Pos2) -> bool {
678 self.rect.contains(pos)
679 || self
680 .sub_menu
681 .as_ref()
682 .is_some_and(|(_, sub)| sub.read().area_contains(pos))
683 }
684
685 fn next_entry_index(&mut self) -> usize {
686 self.entry_count += 1;
687 self.entry_count - 1
688 }
689
690 fn submenu_button_interaction(&mut self, ui: &Ui, sub_id: Id, button: &Response) {
692 let pointer = ui.input(|i| i.pointer.clone());
693 let open = self.is_open(sub_id);
694 if self.moving_towards_current_submenu(&pointer) {
695 ui.ctx().request_repaint();
698 } else if !open && button.hovered() {
699 let mut pos = button.rect.right_top();
701 pos.x = self.rect.right() + ui.spacing().menu_spacing;
702 pos.y -= Frame::menu(ui.style()).total_margin().top; self.open_submenu(sub_id, pos);
705 } else if open
706 && ui.response().contains_pointer()
707 && !button.hovered()
708 && !self.hovering_current_submenu(&pointer)
709 {
710 self.close_submenu();
712 }
713 }
714
715 fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool {
717 if pointer.is_still() {
718 return false;
719 }
720
721 if let Some(sub_menu) = self.current_submenu() {
722 if let Some(pos) = pointer.hover_pos() {
723 let rect = sub_menu.read().rect;
724 return rect.intersects_ray(pos, pointer.direction().normalized());
725 }
726 }
727 false
728 }
729
730 fn hovering_current_submenu(&self, pointer: &PointerState) -> bool {
732 if let Some(sub_menu) = self.current_submenu() {
733 if let Some(pos) = pointer.hover_pos() {
734 return sub_menu.read().area_contains(pos);
735 }
736 }
737 false
738 }
739
740 fn cascade_close_response(&mut self, response: MenuResponse) {
742 if response.is_close() {
743 self.response = response;
744 }
745 }
746
747 fn is_open(&self, id: Id) -> bool {
748 self.sub_id() == Some(id)
749 }
750
751 fn sub_id(&self) -> Option<Id> {
752 self.sub_menu.as_ref().map(|(id, _)| *id)
753 }
754
755 fn current_submenu(&self) -> Option<&Arc<RwLock<Self>>> {
756 self.sub_menu.as_ref().map(|(_, sub)| sub)
757 }
758
759 fn submenu(&self, id: Id) -> Option<&Arc<RwLock<Self>>> {
760 self.sub_menu
761 .as_ref()
762 .and_then(|(k, sub)| if id == *k { Some(sub) } else { None })
763 }
764
765 fn open_submenu(&mut self, id: Id, pos: Pos2) {
767 if !self.is_open(id) {
768 self.sub_menu = Some((id, Arc::new(RwLock::new(Self::new(pos)))));
769 }
770 }
771
772 fn close_submenu(&mut self) {
773 self.sub_menu = None;
774 }
775}