1use crate::style::StyleModifier;
12use crate::{
13 Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup,
14 PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
15};
16use emath::{Align, RectAlign, Vec2, vec2};
17use epaint::Stroke;
18
19pub 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
31pub 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
43pub 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#[derive(Clone, Debug)]
65pub struct MenuConfig {
66 bar: bool,
68
69 pub close_behavior: PopupCloseBehavior,
71
72 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 pub const MENU_CONFIG_TAG: &'static str = "egui_menu_config";
91
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 #[inline]
98 pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
99 self.close_behavior = close_behavior;
100 self
101 }
102
103 #[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 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#[derive(Clone)]
136pub struct MenuState {
137 pub open_item: Option<Id>,
139 last_visible_pass: u64,
140}
141
142impl MenuState {
143 pub const ID: &'static str = "menu_state";
144
145 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 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 state.last_visible_pass + 1 < pass_nr {
162 state.open_item = None;
163 }
164 if let Some(item) = state.open_item
165 && data
166 .get_temp(item.with(Self::ID))
167 .is_none_or(|item: Self| item.last_visible_pass + 1 < pass_nr)
168 {
169 state.open_item = None;
171 }
172 let r = f(&mut state);
173 data.insert_temp(state_id, state);
174 r
175 })
176 }
177
178 pub fn mark_shown(ctx: &Context, id: Id) {
179 let pass_nr = ctx.cumulative_pass_nr();
180 Self::from_id(ctx, id, |state| {
181 state.last_visible_pass = pass_nr;
182 });
183 }
184
185 pub fn is_deepest_open_sub_menu(ctx: &Context, id: Id) -> bool {
189 let pass_nr = ctx.cumulative_pass_nr();
190 let open_item = Self::from_id(ctx, id, |state| state.open_item);
191 open_item.is_none_or(|submenu_id| {
193 Self::from_id(ctx, submenu_id, |state| state.last_visible_pass != pass_nr)
194 })
195 }
196}
197
198#[derive(Clone, Debug)]
217pub struct MenuBar {
218 config: MenuConfig,
219 style: StyleModifier,
220}
221
222#[deprecated = "Renamed to `egui::MenuBar`"]
223pub type Bar = MenuBar;
224
225impl Default for MenuBar {
226 fn default() -> Self {
227 Self {
228 config: MenuConfig::default(),
229 style: menu_style.into(),
230 }
231 }
232}
233
234impl MenuBar {
235 pub fn new() -> Self {
236 Self::default()
237 }
238
239 #[inline]
244 pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
245 self.style = style.into();
246 self
247 }
248
249 #[inline]
253 pub fn config(mut self, config: MenuConfig) -> Self {
254 self.config = config;
255 self
256 }
257
258 #[inline]
260 pub fn ui<R>(self, ui: &mut Ui, content: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
261 let Self { mut config, style } = self;
262 config.bar = true;
263 ui.horizontal(|ui| {
266 ui.scope_builder(
267 UiBuilder::new()
268 .layout(Layout::left_to_right(Align::Center))
269 .ui_stack_info(
270 UiStackInfo::new(UiKind::Menu)
271 .with_tag_value(MenuConfig::MENU_CONFIG_TAG, config),
272 ),
273 |ui| {
274 style.apply(ui.style_mut());
275
276 let height = ui.spacing().interact_size.y;
278 ui.set_min_size(vec2(ui.available_width(), height));
279
280 content(ui)
281 },
282 )
283 .inner
284 })
285 }
286}
287
288pub struct MenuButton<'a> {
294 pub button: Button<'a>,
295 pub config: Option<MenuConfig>,
296}
297
298impl<'a> MenuButton<'a> {
299 pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
300 Self::from_button(Button::new(atoms.into_atoms()))
301 }
302
303 #[inline]
305 pub fn config(mut self, config: MenuConfig) -> Self {
306 self.config = Some(config);
307 self
308 }
309
310 #[inline]
312 pub fn from_button(button: Button<'a>) -> Self {
313 Self {
314 button,
315 config: None,
316 }
317 }
318
319 pub fn ui<R>(
321 self,
322 ui: &mut Ui,
323 content: impl FnOnce(&mut Ui) -> R,
324 ) -> (Response, Option<InnerResponse<R>>) {
325 let response = self.button.ui(ui);
326 let mut config = self.config.unwrap_or_else(|| MenuConfig::find(ui));
327 config.bar = false;
328 let inner = Popup::menu(&response)
329 .close_behavior(config.close_behavior)
330 .style(config.style.clone())
331 .info(
332 UiStackInfo::new(UiKind::Menu).with_tag_value(MenuConfig::MENU_CONFIG_TAG, config),
333 )
334 .show(content);
335 (response, inner)
336 }
337}
338
339pub struct SubMenuButton<'a> {
341 pub button: Button<'a>,
342 pub sub_menu: SubMenu,
343}
344
345impl<'a> SubMenuButton<'a> {
346 pub const RIGHT_ARROW: &'static str = "⏵";
348
349 pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
350 Self::from_button(Button::new(atoms.into_atoms()).right_text("⏵"))
351 }
352
353 pub fn from_button(button: Button<'a>) -> Self {
358 Self {
359 button,
360 sub_menu: SubMenu::default(),
361 }
362 }
363
364 #[inline]
368 pub fn config(mut self, config: MenuConfig) -> Self {
369 self.sub_menu.config = Some(config);
370 self
371 }
372
373 pub fn ui<R>(
375 self,
376 ui: &mut Ui,
377 content: impl FnOnce(&mut Ui) -> R,
378 ) -> (Response, Option<InnerResponse<R>>) {
379 let my_id = ui.next_auto_id();
380 let open = MenuState::from_ui(ui, |state, _| {
381 state.open_item == Some(SubMenu::id_from_widget_id(my_id))
382 });
383 let inactive = ui.style().visuals.widgets.inactive;
384 if open {
386 ui.style_mut().visuals.widgets.inactive = ui.style().visuals.widgets.open;
387 }
388 let response = self.button.ui(ui);
389 ui.style_mut().visuals.widgets.inactive = inactive;
390
391 let popup_response = self.sub_menu.show(ui, &response, content);
392
393 (response, popup_response)
394 }
395}
396
397#[derive(Clone, Debug, Default)]
402pub struct SubMenu {
403 config: Option<MenuConfig>,
404}
405
406impl SubMenu {
407 pub fn new() -> Self {
408 Self::default()
409 }
410
411 #[inline]
415 pub fn config(mut self, config: MenuConfig) -> Self {
416 self.config = Some(config);
417 self
418 }
419
420 pub fn id_from_widget_id(widget_id: Id) -> Id {
422 widget_id.with("submenu")
423 }
424
425 pub fn show<R>(
430 self,
431 ui: &Ui,
432 button_response: &Response,
433 content: impl FnOnce(&mut Ui) -> R,
434 ) -> Option<InnerResponse<R>> {
435 let frame = Frame::menu(ui.style());
436
437 let id = Self::id_from_widget_id(button_response.id);
438
439 let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| {
441 (state.open_item, stack.id, MenuConfig::from_stack(stack))
442 });
443
444 let mut menu_config = self.config.unwrap_or_else(|| parent_config.clone());
445 menu_config.bar = false;
446
447 #[expect(clippy::unwrap_used)] let menu_root_response = ui.ctx().read_response(menu_id).unwrap();
449
450 let hover_pos = ui.ctx().pointer_hover_pos();
451
452 let menu_rect = menu_root_response.rect - frame.total_margin();
454 let is_hovering_menu = hover_pos.is_some_and(|pos| {
455 ui.ctx().layer_id_at(pos) == Some(menu_root_response.layer_id)
456 && menu_rect.contains(pos)
457 });
458
459 let is_any_open = open_item.is_some();
460 let mut is_open = open_item == Some(id);
461 let was_open = is_open;
462 let mut set_open = None;
463
464 let button_rect = button_response
467 .rect
468 .expand2(ui.style().spacing.item_spacing / 2.0);
469
470 let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos));
473
474 let clicked = button_response.clicked();
478 let clicked_by_pointer = button_response.clicked_by(PointerButton::Primary);
479 let clicked_by_keyboard_or_access = clicked && !clicked_by_pointer;
480
481 if ui.is_enabled() && is_open && clicked_by_keyboard_or_access {
482 set_open = Some(false);
483 is_open = false;
484 }
485
486 let should_open =
488 ui.is_enabled() && ((!was_open && clicked) || (is_hovered && !is_any_open));
489 if should_open {
490 set_open = Some(true);
491 is_open = true;
492 MenuState::from_id(ui.ctx(), menu_id, |state| {
494 state.open_item = None;
495 });
496 }
497
498 let gap = frame.total_margin().sum().x / 2.0 + 2.0;
499
500 let mut response = button_response.clone();
501 let expand = Vec2::new(0.0, frame.total_margin().sum().y / 2.0);
503 response.interact_rect = response.interact_rect.expand2(expand);
504
505 let popup_response = Popup::from_response(&response)
506 .id(id)
507 .open(is_open)
508 .align(RectAlign::RIGHT_START)
509 .layout(Layout::top_down_justified(Align::Min))
510 .gap(gap)
511 .style(menu_config.style.clone())
512 .frame(frame)
513 .close_behavior(PopupCloseBehavior::IgnoreClicks)
515 .info(
516 UiStackInfo::new(UiKind::Menu)
517 .with_tag_value(MenuConfig::MENU_CONFIG_TAG, menu_config.clone()),
518 )
519 .show(|ui| {
520 if button_response.clicked() || button_response.is_pointer_button_down_on() {
522 ui.ctx().move_to_top(ui.layer_id());
523 }
524 content(ui)
525 });
526
527 if let Some(popup_response) = &popup_response {
528 let is_deepest_submenu = MenuState::is_deepest_open_sub_menu(ui.ctx(), id);
530
531 let clicked_outside = is_deepest_submenu
537 && popup_response.response.clicked_elsewhere()
538 && menu_root_response.clicked_elsewhere();
539
540 let submenu_button_clicked = button_response.clicked();
545
546 let clicked_inside = is_deepest_submenu
547 && !submenu_button_clicked
548 && response.ctx.input(|i| i.pointer.any_click())
549 && hover_pos.is_some_and(|pos| popup_response.response.interact_rect.contains(pos));
550
551 let click_close = match menu_config.close_behavior {
552 PopupCloseBehavior::CloseOnClick => clicked_outside || clicked_inside,
553 PopupCloseBehavior::CloseOnClickOutside => clicked_outside,
554 PopupCloseBehavior::IgnoreClicks => false,
555 };
556
557 if click_close {
558 set_open = Some(false);
559 ui.close();
560 }
561
562 let is_moving_towards_rect = ui.input(|i| {
563 i.pointer
564 .is_moving_towards_rect(&popup_response.response.rect)
565 });
566 if is_moving_towards_rect {
567 ui.request_repaint();
570 }
571 let hovering_other_menu_entry = is_open
572 && !is_hovered
573 && !popup_response.response.contains_pointer()
574 && !is_moving_towards_rect
575 && is_hovering_menu;
576
577 let close_called = popup_response.response.should_close();
578
579 if close_called {
581 ui.close();
582 }
583
584 if hovering_other_menu_entry {
585 set_open = Some(false);
586 }
587
588 if ui.will_parent_close() {
589 ui.data_mut(|data| data.remove_by_type::<MenuState>());
590 }
591 }
592
593 if let Some(set_open) = set_open {
594 MenuState::from_id(ui.ctx(), menu_id, |state| {
595 state.open_item = set_open.then_some(id);
596 });
597 }
598
599 popup_response
600 }
601}