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