egui/containers/
menu.rs

1//! Popup menus, context menus and menu bars.
2//!
3//! Show menus via
4//! - [`Popup::menu`] and [`Popup::context_menu`]
5//! - [`Ui::menu_button`], [`MenuButton`] and [`SubMenuButton`]
6//! - [`MenuBar`]
7//! - [`Response::context_menu`]
8//!
9//! See [`MenuBar`] for an example.
10
11use crate::style::StyleModifier;
12use crate::{
13    Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
14    PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
15};
16use emath::{Align, RectAlign, Vec2, vec2};
17use epaint::Stroke;
18
19/// Apply a menu style to the [`Style`].
20///
21/// Mainly removes the background stroke and the inactive background fill.
22pub fn menu_style(style: &mut Style) {
23    style.spacing.button_padding = vec2(2.0, 0.0);
24    style.visuals.widgets.active.bg_stroke = Stroke::NONE;
25    style.visuals.widgets.open.bg_stroke = Stroke::NONE;
26    style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
27    style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
28    style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
29}
30
31/// Find the root [`UiStack`] of the menu.
32pub fn find_menu_root(ui: &Ui) -> &UiStack {
33    ui.stack()
34        .iter()
35        .find(|stack| {
36            stack.is_root_ui()
37                || [Some(UiKind::Popup), Some(UiKind::Menu)].contains(&stack.kind())
38                || stack.info.tags.contains(MenuConfig::MENU_CONFIG_TAG)
39        })
40        .expect("We should always find the root")
41}
42
43/// Is this Ui part of a menu?
44///
45/// Returns `false` if this is a menu bar.
46/// Should be used to determine if we should show a menu button or submenu button.
47pub fn is_in_menu(ui: &Ui) -> bool {
48    for stack in ui.stack().iter() {
49        if let Some(config) = stack
50            .info
51            .tags
52            .get_downcast::<MenuConfig>(MenuConfig::MENU_CONFIG_TAG)
53        {
54            return !config.bar;
55        }
56        if [Some(UiKind::Popup), Some(UiKind::Menu)].contains(&stack.kind()) {
57            return true;
58        }
59    }
60    false
61}
62
63/// Configuration and style for menus.
64#[derive(Clone, Debug)]
65pub struct MenuConfig {
66    /// Is this a menu bar?
67    bar: bool,
68
69    /// If the user clicks, should we close the menu?
70    pub close_behavior: PopupCloseBehavior,
71
72    /// Override the menu style.
73    ///
74    /// Default is [`menu_style`].
75    pub style: StyleModifier,
76}
77
78impl Default for MenuConfig {
79    fn default() -> Self {
80        Self {
81            close_behavior: PopupCloseBehavior::default(),
82            bar: false,
83            style: menu_style.into(),
84        }
85    }
86}
87
88impl MenuConfig {
89    /// The tag used to store the menu config in the [`UiStack`].
90    pub const MENU_CONFIG_TAG: &'static str = "egui_menu_config";
91
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// If the user clicks, should we close the menu?
97    #[inline]
98    pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
99        self.close_behavior = close_behavior;
100        self
101    }
102
103    /// Override the menu style.
104    ///
105    /// Default is [`menu_style`].
106    #[inline]
107    pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
108        self.style = style.into();
109        self
110    }
111
112    fn from_stack(stack: &UiStack) -> Self {
113        stack
114            .info
115            .tags
116            .get_downcast(Self::MENU_CONFIG_TAG)
117            .cloned()
118            .unwrap_or_default()
119    }
120
121    /// Find the config for the current menu.
122    ///
123    /// Returns the default config if no config is found.
124    pub fn find(ui: &Ui) -> Self {
125        find_menu_root(ui)
126            .info
127            .tags
128            .get_downcast(Self::MENU_CONFIG_TAG)
129            .cloned()
130            .unwrap_or_default()
131    }
132}
133
134/// Holds the state of the menu.
135#[derive(Clone)]
136pub struct MenuState {
137    /// The currently open sub menu in this menu.
138    pub open_item: Option<Id>,
139    last_visible_pass: u64,
140}
141
142impl MenuState {
143    pub const ID: &'static str = "menu_state";
144
145    /// Find the root of the menu and get the state
146    pub fn from_ui<R>(ui: &Ui, f: impl FnOnce(&mut Self, &UiStack) -> R) -> R {
147        let stack = find_menu_root(ui);
148        Self::from_id(ui.ctx(), stack.id, |state| f(state, stack))
149    }
150
151    /// Get the state via the menus root [`Ui`] id
152    pub fn from_id<R>(ctx: &Context, id: Id, f: impl FnOnce(&mut Self) -> R) -> R {
153        let pass_nr = ctx.cumulative_pass_nr();
154        ctx.data_mut(|data| {
155            let state_id = id.with(Self::ID);
156            let mut state = data.get_temp(state_id).unwrap_or(Self {
157                open_item: None,
158                last_visible_pass: pass_nr,
159            });
160            // If the menu was closed for at least a frame, reset the open item
161            if state.last_visible_pass + 1 < pass_nr {
162                state.open_item = None;
163            }
164            if let Some(item) = state.open_item {
165                if data
166                    .get_temp(item.with(Self::ID))
167                    .is_none_or(|item: Self| item.last_visible_pass + 1 < pass_nr)
168                {
169                    // If the open item wasn't shown for at least a frame, reset the open item
170                    state.open_item = None;
171                }
172            }
173            let r = f(&mut state);
174            data.insert_temp(state_id, state);
175            r
176        })
177    }
178
179    pub fn mark_shown(ctx: &Context, id: Id) {
180        let pass_nr = ctx.cumulative_pass_nr();
181        Self::from_id(ctx, id, |state| {
182            state.last_visible_pass = pass_nr;
183        });
184    }
185
186    /// Is the menu with this id the deepest sub menu? (-> no child sub menu is open)
187    ///
188    /// Note: This only returns correct results if called after the menu contents were shown.
189    pub fn is_deepest_open_sub_menu(ctx: &Context, id: Id) -> bool {
190        let pass_nr = ctx.cumulative_pass_nr();
191        let open_item = Self::from_id(ctx, id, |state| state.open_item);
192        // If we have some open item, check if that was actually shown this frame
193        open_item.is_none_or(|submenu_id| {
194            Self::from_id(ctx, submenu_id, |state| state.last_visible_pass != pass_nr)
195        })
196    }
197}
198
199/// Horizontal menu bar where you can add [`MenuButton`]s.
200///
201/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
202/// but can also be placed in a [`crate::Window`].
203/// In the latter case you may want to wrap it in [`Frame`].
204///
205/// ### Example:
206/// ```
207/// # egui::__run_test_ui(|ui| {
208/// egui::MenuBar::new().ui(ui, |ui| {
209///     ui.menu_button("File", |ui| {
210///         if ui.button("Quit").clicked() {
211///             ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
212///         }
213///     });
214/// });
215/// # });
216/// ```
217#[derive(Clone, Debug)]
218pub struct MenuBar {
219    config: MenuConfig,
220    style: StyleModifier,
221}
222
223#[deprecated = "Renamed to `egui::MenuBar`"]
224pub type Bar = MenuBar;
225
226impl Default for MenuBar {
227    fn default() -> Self {
228        Self {
229            config: MenuConfig::default(),
230            style: menu_style.into(),
231        }
232    }
233}
234
235impl MenuBar {
236    pub fn new() -> Self {
237        Self::default()
238    }
239
240    /// Set the style for buttons in the menu bar.
241    ///
242    /// Doesn't affect the style of submenus, use [`MenuConfig::style`] for that.
243    /// Default is [`menu_style`].
244    #[inline]
245    pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
246        self.style = style.into();
247        self
248    }
249
250    /// Set the config for submenus.
251    ///
252    /// Note: The config will only be passed when using [`MenuButton`], not via [`Popup::menu`].
253    #[inline]
254    pub fn config(mut self, config: MenuConfig) -> Self {
255        self.config = config;
256        self
257    }
258
259    /// Show the menu bar.
260    #[inline]
261    pub fn ui<R>(self, ui: &mut Ui, content: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
262        let Self { mut config, style } = self;
263        config.bar = true;
264        // TODO(lucasmerlin): It'd be nice if we had a ui.horizontal_builder or something
265        // So we don't need the nested scope here
266        ui.horizontal(|ui| {
267            ui.scope_builder(
268                UiBuilder::new()
269                    .layout(Layout::left_to_right(Align::Center))
270                    .ui_stack_info(
271                        UiStackInfo::new(UiKind::Menu)
272                            .with_tag_value(MenuConfig::MENU_CONFIG_TAG, config),
273                    ),
274                |ui| {
275                    style.apply(ui.style_mut());
276
277                    // Take full width and fixed height:
278                    let height = ui.spacing().interact_size.y;
279                    ui.set_min_size(vec2(ui.available_width(), height));
280
281                    content(ui)
282                },
283            )
284            .inner
285        })
286    }
287}
288
289/// A thin wrapper around a [`Button`] that shows a [`Popup::menu`] when clicked.
290///
291/// The only thing this does is search for the current menu config (if set via [`MenuBar`]).
292/// If your menu button is not in a [`MenuBar`] it's fine to use [`Ui::button`] and [`Popup::menu`]
293/// directly.
294pub struct MenuButton<'a> {
295    pub button: Button<'a>,
296    pub config: Option<MenuConfig>,
297}
298
299impl<'a> MenuButton<'a> {
300    pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
301        Self::from_button(Button::new(atoms.into_atoms()))
302    }
303
304    /// Set the config for the menu.
305    #[inline]
306    pub fn config(mut self, config: MenuConfig) -> Self {
307        self.config = Some(config);
308        self
309    }
310
311    /// Create a new menu button from a [`Button`].
312    #[inline]
313    pub fn from_button(button: Button<'a>) -> Self {
314        Self {
315            button,
316            config: None,
317        }
318    }
319
320    /// Show the menu button.
321    pub fn ui<R>(
322        self,
323        ui: &mut Ui,
324        content: impl FnOnce(&mut Ui) -> R,
325    ) -> (Response, Option<InnerResponse<R>>) {
326        let response = self.button.ui(ui);
327        let mut config = self.config.unwrap_or_else(|| MenuConfig::find(ui));
328        config.bar = false;
329        let inner = Popup::menu(&response)
330            .close_behavior(config.close_behavior)
331            .style(config.style.clone())
332            .info(
333                UiStackInfo::new(UiKind::Menu).with_tag_value(MenuConfig::MENU_CONFIG_TAG, config),
334            )
335            .show(content);
336        (response, inner)
337    }
338}
339
340/// A submenu button that shows a [`SubMenu`] if a [`Button`] is hovered.
341pub struct SubMenuButton<'a> {
342    pub button: Button<'a>,
343    pub sub_menu: SubMenu,
344}
345
346impl<'a> SubMenuButton<'a> {
347    /// The default right arrow symbol: `"⏵"`
348    pub const RIGHT_ARROW: &'static str = "⏵";
349
350    pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
351        Self::from_button(Button::new(atoms.into_atoms()).right_text("⏵"))
352    }
353
354    /// Create a new submenu button from a [`Button`].
355    ///
356    /// Use [`Button::right_text`] and [`SubMenuButton::RIGHT_ARROW`] to add the default right
357    /// arrow symbol.
358    pub fn from_button(button: Button<'a>) -> Self {
359        Self {
360            button,
361            sub_menu: SubMenu::default(),
362        }
363    }
364
365    /// Set the config for the submenu.
366    ///
367    /// The close behavior will not affect the current button, but the buttons in the submenu.
368    #[inline]
369    pub fn config(mut self, config: MenuConfig) -> Self {
370        self.sub_menu.config = Some(config);
371        self
372    }
373
374    /// Show the submenu button.
375    pub fn ui<R>(
376        self,
377        ui: &mut Ui,
378        content: impl FnOnce(&mut Ui) -> R,
379    ) -> (Response, Option<InnerResponse<R>>) {
380        let my_id = ui.next_auto_id();
381        let open = MenuState::from_ui(ui, |state, _| {
382            state.open_item == Some(SubMenu::id_from_widget_id(my_id))
383        });
384        let inactive = ui.style().visuals.widgets.inactive;
385        // TODO(lucasmerlin) add `open` function to `Button`
386        if open {
387            ui.style_mut().visuals.widgets.inactive = ui.style().visuals.widgets.open;
388        }
389        let response = self.button.ui(ui);
390        ui.style_mut().visuals.widgets.inactive = inactive;
391
392        let popup_response = self.sub_menu.show(ui, &response, content);
393
394        (response, popup_response)
395    }
396}
397
398/// Show a submenu in a menu.
399///
400/// Useful if you want to make custom menu buttons.
401/// Usually, just use [`MenuButton`] or [`SubMenuButton`] instead.
402#[derive(Clone, Debug, Default)]
403pub struct SubMenu {
404    config: Option<MenuConfig>,
405}
406
407impl SubMenu {
408    pub fn new() -> Self {
409        Self::default()
410    }
411
412    /// Set the config for the submenu.
413    ///
414    /// The close behavior will not affect the current button, but the buttons in the submenu.
415    #[inline]
416    pub fn config(mut self, config: MenuConfig) -> Self {
417        self.config = Some(config);
418        self
419    }
420
421    /// Get the id for the submenu from the widget/response id.
422    pub fn id_from_widget_id(widget_id: Id) -> Id {
423        widget_id.with("submenu")
424    }
425
426    /// Show the submenu.
427    ///
428    /// This does some heuristics to check if the `button_response` was the last thing in the
429    /// menu that was hovered/clicked, and if so, shows the submenu.
430    pub fn show<R>(
431        self,
432        ui: &Ui,
433        button_response: &Response,
434        content: impl FnOnce(&mut Ui) -> R,
435    ) -> Option<InnerResponse<R>> {
436        let frame = Frame::menu(ui.style());
437
438        let id = Self::id_from_widget_id(button_response.id);
439
440        // Get the state from the parent menu
441        let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| {
442            (state.open_item, stack.id, MenuConfig::from_stack(stack))
443        });
444
445        let mut menu_config = self.config.unwrap_or_else(|| parent_config.clone());
446        menu_config.bar = false;
447
448        let menu_root_response = ui
449            .ctx()
450            .read_response(menu_id)
451            // Since we are a child of that ui, this should always exist
452            .unwrap();
453
454        let hover_pos = ui.ctx().pointer_hover_pos();
455
456        // We don't care if the user is hovering over the border
457        let menu_rect = menu_root_response.rect - frame.total_margin();
458        let is_hovering_menu = hover_pos.is_some_and(|pos| {
459            ui.ctx().layer_id_at(pos) == Some(menu_root_response.layer_id)
460                && menu_rect.contains(pos)
461        });
462
463        let is_any_open = open_item.is_some();
464        let mut is_open = open_item == Some(id);
465        let mut set_open = None;
466
467        // We expand the button rect so there is no empty space where no menu is shown
468        // TODO(lucasmerlin): Instead, maybe make item_spacing.y 0.0?
469        let button_rect = button_response
470            .rect
471            .expand2(ui.style().spacing.item_spacing / 2.0);
472
473        // In theory some other widget could cover the button and this check would still pass
474        // But since we check if no other menu is open, nothing should be able to cover the button
475        let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos));
476
477        // The clicked handler is there for accessibility (keyboard navigation)
478        let should_open =
479            ui.is_enabled() && (button_response.clicked() || (is_hovered && !is_any_open));
480        if should_open {
481            set_open = Some(true);
482            is_open = true;
483            // Ensure that all other sub menus are closed when we open the menu
484            MenuState::from_id(ui.ctx(), menu_id, |state| {
485                state.open_item = None;
486            });
487        }
488
489        let gap = frame.total_margin().sum().x / 2.0 + 2.0;
490
491        let mut response = button_response.clone();
492        // Expand the button rect so that the button and the first item in the submenu are aligned
493        let expand = Vec2::new(0.0, frame.total_margin().sum().y / 2.0);
494        response.interact_rect = response.interact_rect.expand2(expand);
495
496        let popup_response = Popup::from_response(&response)
497            .id(id)
498            .open(is_open)
499            .align(RectAlign::RIGHT_START)
500            .layout(Layout::top_down_justified(Align::Min))
501            .gap(gap)
502            .style(menu_config.style.clone())
503            .frame(frame)
504            // The close behavior is handled by the menu (see below)
505            .close_behavior(PopupCloseBehavior::IgnoreClicks)
506            .info(
507                UiStackInfo::new(UiKind::Menu)
508                    .with_tag_value(MenuConfig::MENU_CONFIG_TAG, menu_config.clone()),
509            )
510            .show(|ui| {
511                // Ensure our layer stays on top when the button is clicked
512                if button_response.clicked() || button_response.is_pointer_button_down_on() {
513                    ui.ctx().move_to_top(ui.layer_id());
514                }
515                content(ui)
516            });
517
518        if let Some(popup_response) = &popup_response {
519            // If no child sub menu is open means we must be the deepest child sub menu.
520            let is_deepest_submenu = MenuState::is_deepest_open_sub_menu(ui.ctx(), id);
521
522            // If the user clicks and the cursor is not hovering over our menu rect, it's
523            // safe to assume they clicked outside the menu, so we close everything.
524            // If they were to hover some other parent submenu we wouldn't be open.
525            // Only edge case is the user hovering this submenu's button, so we also check
526            // if we clicked outside the parent menu (which we luckily have access to here).
527            let clicked_outside = is_deepest_submenu
528                && popup_response.response.clicked_elsewhere()
529                && menu_root_response.clicked_elsewhere();
530
531            // We never automatically close when a submenu button is clicked, (so menus work
532            // on touch devices)
533            // Luckily we will always be the deepest submenu when a submenu button is clicked,
534            // so the following check is enough.
535            let submenu_button_clicked = button_response.clicked();
536
537            let clicked_inside = is_deepest_submenu
538                && !submenu_button_clicked
539                && response.ctx.input(|i| i.pointer.any_click())
540                && hover_pos.is_some_and(|pos| popup_response.response.interact_rect.contains(pos));
541
542            let click_close = match menu_config.close_behavior {
543                PopupCloseBehavior::CloseOnClick => clicked_outside || clicked_inside,
544                PopupCloseBehavior::CloseOnClickOutside => clicked_outside,
545                PopupCloseBehavior::IgnoreClicks => false,
546            };
547
548            if click_close {
549                set_open = Some(false);
550                ui.close();
551            }
552
553            let is_moving_towards_rect = ui.input(|i| {
554                i.pointer
555                    .is_moving_towards_rect(&popup_response.response.rect)
556            });
557            if is_moving_towards_rect {
558                // We need to repaint while this is true, so we can detect when
559                // the pointer is no longer moving towards the rect
560                ui.ctx().request_repaint();
561            }
562            let hovering_other_menu_entry = is_open
563                && !is_hovered
564                && !popup_response.response.contains_pointer()
565                && !is_moving_towards_rect
566                && is_hovering_menu;
567
568            let close_called = popup_response.response.should_close();
569
570            // Close the parent ui to e.g. close the popup from where the submenu was opened
571            if close_called {
572                ui.close();
573            }
574
575            if hovering_other_menu_entry {
576                set_open = Some(false);
577            }
578
579            if ui.will_parent_close() {
580                ui.data_mut(|data| data.remove_by_type::<MenuState>());
581            }
582        }
583
584        if let Some(set_open) = set_open {
585            MenuState::from_id(ui.ctx(), menu_id, |state| {
586                state.open_item = set_open.then_some(id);
587            });
588        }
589
590        popup_response
591    }
592}