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