1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

⚠️ Improved menu based on egui::Popup (#5716)

Continuation of #5713

**Silently breaking changes:**
- Menus now close on click by default, this is configurable via
`PopupCloseBehavior`

**Additional additions:**
- `Button::right_text`
- `StyleModifier`

This is a rewrite of the egui menus, with the following goals:
- submenu buttons should work everywhere, in a popup, context menu,
area, in some random Ui
- remove the menu state from Ui
- should work just like the previous menu
- ~proper support for keyboard navigation~
  - It's better now but requires further work to be perfect
- support `PopupCloseBehavior`

* Closes #4607 
* [x] I have followed the instructions in the PR template
This commit is contained in:
lucasmerlin
2025-03-03 14:56:30 +01:00
committed by GitHub
parent 6e3575b4c7
commit cd22517280
27 changed files with 1289 additions and 368 deletions

View File

@@ -1,94 +0,0 @@
use egui::{ComboBox, Popup};
#[derive(Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ContextMenus {}
impl crate::Demo for ContextMenus {
fn name(&self) -> &'static str {
"☰ Context Menus"
}
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
use crate::View;
egui::Window::new(self.name())
.vscroll(false)
.resizable(false)
.open(open)
.show(ctx, |ui| self.ui(ui));
}
}
impl crate::View for ContextMenus {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.menu_button("Click for menu", Self::nested_menus);
ui.button("Right-click for menu")
.context_menu(Self::nested_menus);
if ui.ctx().is_context_menu_open() {
ui.label("Context menu is open");
} else {
ui.label("Context menu is closed");
}
});
ui.horizontal(|ui| {
let response = ui.button("New menu");
Popup::menu(&response).show(Self::nested_menus);
let response = ui.button("New context menu");
Popup::context_menu(&response).show(Self::nested_menus);
ComboBox::new("Hi", "Hi").show_ui(ui, |ui| {
_ = ui.selectable_label(false, "I have some long text that should be wrapped");
_ = ui.selectable_label(false, "Short");
_ = ui.selectable_label(false, "Medium length");
});
});
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
}
}
impl ContextMenus {
fn nested_menus(ui: &mut egui::Ui) {
ui.set_max_width(200.0); // To make sure we wrap long text
if ui.button("Open…").clicked() {
ui.close();
}
ui.menu_button("SubMenu", |ui| {
ui.menu_button("SubMenu", |ui| {
if ui.button("Open…").clicked() {
ui.close();
}
let _ = ui.button("Item");
ui.menu_button("Recursive", Self::nested_menus)
});
ui.menu_button("SubMenu", |ui| {
if ui.button("Open…").clicked() {
ui.close();
}
let _ = ui.button("Item");
});
let _ = ui.button("Item");
if ui.button("Open…").clicked() {
ui.close();
}
});
ui.menu_button("SubMenu", |ui| {
let _ = ui.button("Item1");
let _ = ui.button("Item2");
let _ = ui.button("Item3");
let _ = ui.button("Item4");
if ui.button("Open…").clicked() {
ui.close();
}
});
let _ = ui.button("Very long text for this item that should be wrapped");
}
}

View File

@@ -1,12 +1,12 @@
use std::collections::BTreeSet;
use egui::{Context, Modifiers, ScrollArea, Ui};
use super::About;
use crate::is_mobile;
use crate::Demo;
use crate::View;
use egui::containers::menu;
use egui::style::StyleModifier;
use egui::{Context, Modifiers, ScrollArea, Ui};
// ----------------------------------------------------------------------------
struct DemoGroup {
@@ -65,7 +65,6 @@ impl Default for DemoGroups {
Box::<super::paint_bezier::PaintBezier>::default(),
Box::<super::code_editor::CodeEditor>::default(),
Box::<super::code_example::CodeExample>::default(),
Box::<super::context_menu::ContextMenus>::default(),
Box::<super::dancing_strings::DancingStrings>::default(),
Box::<super::drag_and_drop::DragAndDropDemo>::default(),
Box::<super::extra_viewport::ExtraViewport>::default(),
@@ -227,29 +226,27 @@ impl DemoWindows {
fn mobile_top_bar(&mut self, ctx: &Context) {
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
let font_size = 16.5;
menu::Bar::new()
.config(menu::MenuConfig::new().style(StyleModifier::default()))
.ui(ui, |ui| {
let font_size = 16.5;
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
self.demo_list_ui(ui);
if ui.ui_contains_pointer() && ui.input(|i| i.pointer.any_click()) {
ui.close();
}
});
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
self.demo_list_ui(ui);
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
use egui::special_emojis::GITHUB;
ui.hyperlink_to(
egui::RichText::new("🦋").size(font_size),
"https://bsky.app/profile/ernerfeldt.bsky.social",
);
ui.hyperlink_to(
egui::RichText::new(GITHUB).size(font_size),
"https://github.com/emilk/egui",
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
use egui::special_emojis::GITHUB;
ui.hyperlink_to(
egui::RichText::new("🦋").size(font_size),
"https://bsky.app/profile/ernerfeldt.bsky.social",
);
ui.hyperlink_to(
egui::RichText::new(GITHUB).size(font_size),
"https://github.com/emilk/egui",
);
});
});
});
});
}
@@ -282,7 +279,7 @@ impl DemoWindows {
});
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
menu::Bar::new().ui(ui, |ui| {
file_menu_button(ui);
});
});
@@ -345,7 +342,6 @@ fn file_menu_button(ui: &mut Ui) {
.clicked()
{
ui.ctx().memory_mut(|mem| mem.reset_areas());
ui.close();
}
if ui
@@ -357,7 +353,6 @@ fn file_menu_button(ui: &mut Ui) {
.clicked()
{
ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close();
}
});
}

View File

@@ -7,7 +7,6 @@
pub mod about;
pub mod code_editor;
pub mod code_example;
pub mod context_menu;
pub mod dancing_strings;
pub mod demo_app_windows;
pub mod drag_and_drop;

View File

@@ -1,4 +1,9 @@
use egui::{vec2, Align2, ComboBox, Frame, Id, Popup, PopupCloseBehavior, RectAlign, Tooltip, Ui};
use crate::rust_view_ui;
use egui::containers::menu::{MenuConfig, SubMenuButton};
use egui::{
include_image, Align, Align2, ComboBox, Frame, Id, Layout, Popup, PopupCloseBehavior,
RectAlign, Tooltip, Ui, UiBuilder,
};
/// Showcase [`Popup`].
#[derive(Clone, PartialEq)]
@@ -10,6 +15,7 @@ pub struct PopupsDemo {
#[cfg_attr(feature = "serde", serde(skip))]
close_behavior: PopupCloseBehavior,
popup_open: bool,
checked: bool,
}
impl PopupsDemo {
@@ -28,6 +34,7 @@ impl Default for PopupsDemo {
gap: 4.0,
close_behavior: PopupCloseBehavior::CloseOnClick,
popup_open: false,
checked: false,
}
}
}
@@ -50,120 +57,70 @@ impl crate::Demo for PopupsDemo {
}
}
fn nested_menus(ui: &mut egui::Ui, checked: &mut bool) {
ui.set_max_width(200.0); // To make sure we wrap long text
if ui.button("Open…").clicked() {
ui.close();
}
ui.menu_button("Popups can have submenus", |ui| {
ui.menu_button("SubMenu", |ui| {
if ui.button("Open…").clicked() {
ui.close();
}
let _ = ui.button("Item");
ui.menu_button("Recursive", |ui| nested_menus(ui, checked));
});
ui.menu_button("SubMenu", |ui| {
if ui.button("Open…").clicked() {
ui.close();
}
let _ = ui.button("Item");
});
let _ = ui.button("Item");
if ui.button("Open…").clicked() {
ui.close();
}
});
ui.menu_image_text_button(
include_image!("../../data/icon.png"),
"I have an icon!",
|ui| {
let _ = ui.button("Item1");
let _ = ui.button("Item2");
let _ = ui.button("Item3");
let _ = ui.button("Item4");
if ui.button("Open…").clicked() {
ui.close();
}
},
);
let _ = ui.button("Very long text for this item that should be wrapped");
SubMenuButton::new("Always CloseOnClickOutside")
.config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside))
.ui(ui, |ui| {
ui.checkbox(checked, "Checkbox");
if ui.button("Open…").clicked() {
ui.close();
}
});
}
impl crate::View for PopupsDemo {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.style_mut().spacing.item_spacing.x = 0.0;
let align_combobox = |ui: &mut Ui, label: &str, align: &mut Align2| {
let aligns = [
(Align2::LEFT_TOP, "Left top"),
(Align2::LEFT_CENTER, "Left center"),
(Align2::LEFT_BOTTOM, "Left bottom"),
(Align2::CENTER_TOP, "Center top"),
(Align2::CENTER_CENTER, "Center center"),
(Align2::CENTER_BOTTOM, "Center bottom"),
(Align2::RIGHT_TOP, "Right top"),
(Align2::RIGHT_CENTER, "Right center"),
(Align2::RIGHT_BOTTOM, "Right bottom"),
];
ui.label(label);
ComboBox::new(label, "")
.selected_text(aligns.iter().find(|(a, _)| a == align).unwrap().1)
.show_ui(ui, |ui| {
for (align2, name) in &aligns {
ui.selectable_value(align, *align2, *name);
}
});
};
ui.label("Align4(");
align_combobox(ui, "parent: ", &mut self.align4.parent);
ui.label(", ");
align_combobox(ui, "child: ", &mut self.align4.child);
ui.label(") ");
let presets = [
(RectAlign::TOP_START, "Top start"),
(RectAlign::TOP, "Top"),
(RectAlign::TOP_END, "Top end"),
(RectAlign::RIGHT_START, "Right start"),
(RectAlign::RIGHT, "Right Center"),
(RectAlign::RIGHT_END, "Right end"),
(RectAlign::BOTTOM_START, "Bottom start"),
(RectAlign::BOTTOM, "Bottom"),
(RectAlign::BOTTOM_END, "Bottom end"),
(RectAlign::LEFT_START, "Left start"),
(RectAlign::LEFT, "Left"),
(RectAlign::LEFT_END, "Left end"),
];
ui.label(" Presets: ");
ComboBox::new("Preset", "")
.selected_text(
presets
.iter()
.find(|(a, _)| a == &self.align4)
.map_or("Select", |(_, name)| *name),
)
.show_ui(ui, |ui| {
for (align4, name) in &presets {
ui.selectable_value(&mut self.align4, *align4, *name);
}
});
});
ui.horizontal(|ui| {
ui.label("Gap:");
ui.add(egui::DragValue::new(&mut self.gap));
});
ui.horizontal(|ui| {
ui.label("Close behavior:");
ui.selectable_value(
&mut self.close_behavior,
PopupCloseBehavior::CloseOnClick,
"Close on click",
)
.on_hover_text("Closes when the user clicks anywhere (inside or outside)");
ui.selectable_value(
&mut self.close_behavior,
PopupCloseBehavior::CloseOnClickOutside,
"Close on click outside",
)
.on_hover_text("Closes when the user clicks outside the popup");
ui.selectable_value(
&mut self.close_behavior,
PopupCloseBehavior::IgnoreClicks,
"Ignore clicks",
)
.on_hover_text("Close only when the button is clicked again");
});
ui.checkbox(&mut self.popup_open, "Show popup");
let response = Frame::group(ui.style())
.inner_margin(vec2(0.0, 25.0))
.show(ui, |ui| {
ui.set_width(ui.available_width());
ui.vertical_centered(|ui| ui.button("Click, right-click and hover me!"))
.inner
})
.inner;
self.apply_options(Popup::menu(&response).id(Id::new("menu")))
.show(|ui| {
_ = ui.button("Menu item 1");
_ = ui.button("Menu item 2");
if ui.button("I always close the menu").clicked() {
ui.close();
}
});
.show(|ui| nested_menus(ui, &mut self.checked));
self.apply_options(Popup::context_menu(&response).id(Id::new("context_menu")))
.show(|ui| {
_ = ui.button("Context menu item 1");
_ = ui.button("Context menu item 2");
});
.show(|ui| nested_menus(ui, &mut self.checked));
if self.popup_open {
self.apply_options(Popup::from_response(&response).id(Id::new("popup")))
@@ -178,6 +135,148 @@ impl crate::View for PopupsDemo {
ui.label("Tooltips are popups, too!");
});
Frame::canvas(ui.style()).show(ui, |ui| {
let mut reset_btn_ui = ui.new_child(
UiBuilder::new()
.max_rect(ui.max_rect())
.layout(Layout::right_to_left(Align::Min)),
);
if reset_btn_ui
.button("")
.on_hover_text("Reset to defaults")
.clicked()
{
*self = Self::default();
}
ui.set_width(ui.available_width());
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.spacing_mut().item_spacing.x = 0.0;
let align_combobox = |ui: &mut Ui, label: &str, align: &mut Align2| {
let aligns = [
(Align2::LEFT_TOP, "LEFT_TOP"),
(Align2::LEFT_CENTER, "LEFT_CENTER"),
(Align2::LEFT_BOTTOM, "LEFT_BOTTOM"),
(Align2::CENTER_TOP, "CENTER_TOP"),
(Align2::CENTER_CENTER, "CENTER_CENTER"),
(Align2::CENTER_BOTTOM, "CENTER_BOTTOM"),
(Align2::RIGHT_TOP, "RIGHT_TOP"),
(Align2::RIGHT_CENTER, "RIGHT_CENTER"),
(Align2::RIGHT_BOTTOM, "RIGHT_BOTTOM"),
];
ComboBox::new(label, "")
.selected_text(aligns.iter().find(|(a, _)| a == align).unwrap().1)
.show_ui(ui, |ui| {
for (align2, name) in &aligns {
ui.selectable_value(align, *align2, *name);
}
});
};
rust_view_ui(ui, "let align = RectAlign {");
ui.horizontal(|ui| {
rust_view_ui(ui, " parent: Align2::");
align_combobox(ui, "parent", &mut self.align4.parent);
rust_view_ui(ui, ",");
});
ui.horizontal(|ui| {
rust_view_ui(ui, " child: Align2::");
align_combobox(ui, "child", &mut self.align4.child);
rust_view_ui(ui, ",");
});
rust_view_ui(ui, "};");
ui.horizontal(|ui| {
rust_view_ui(ui, "let align = RectAlign::");
let presets = [
(RectAlign::TOP_START, "TOP_START"),
(RectAlign::TOP, "TOP"),
(RectAlign::TOP_END, "TOP_END"),
(RectAlign::RIGHT_START, "RIGHT_START"),
(RectAlign::RIGHT, "RIGHT"),
(RectAlign::RIGHT_END, "RIGHT_END"),
(RectAlign::BOTTOM_START, "BOTTOM_START"),
(RectAlign::BOTTOM, "BOTTOM"),
(RectAlign::BOTTOM_END, "BOTTOM_END"),
(RectAlign::LEFT_START, "LEFT_START"),
(RectAlign::LEFT, "LEFT"),
(RectAlign::LEFT_END, "LEFT_END"),
];
ComboBox::new("Preset", "")
.selected_text(
presets
.iter()
.find(|(a, _)| a == &self.align4)
.map_or("<Select Preset>", |(_, name)| *name),
)
.show_ui(ui, |ui| {
for (align4, name) in &presets {
ui.selectable_value(&mut self.align4, *align4, *name);
}
});
rust_view_ui(ui, ";");
});
ui.horizontal(|ui| {
rust_view_ui(ui, "let gap = ");
ui.add(egui::DragValue::new(&mut self.gap));
rust_view_ui(ui, ";");
});
rust_view_ui(ui, "let close_behavior");
ui.horizontal(|ui| {
rust_view_ui(ui, " = PopupCloseBehavior::");
let close_behaviors = [
(
PopupCloseBehavior::CloseOnClick,
"CloseOnClick",
"Closes when the user clicks anywhere (inside or outside)",
),
(
PopupCloseBehavior::CloseOnClickOutside,
"CloseOnClickOutside",
"Closes when the user clicks outside the popup",
),
(
PopupCloseBehavior::IgnoreClicks,
"IgnoreClicks",
"Close only when the button is clicked again",
),
];
ComboBox::new("Close behavior", "")
.selected_text(
close_behaviors
.iter()
.find_map(|(behavior, text, _)| {
(behavior == &self.close_behavior).then_some(*text)
})
.unwrap(),
)
.show_ui(ui, |ui| {
for (close_behavior, name, tooltip) in &close_behaviors {
ui.selectable_value(&mut self.close_behavior, *close_behavior, *name)
.on_hover_text(*tooltip);
}
});
rust_view_ui(ui, ";");
});
ui.horizontal(|ui| {
rust_view_ui(ui, "let popup_open = ");
ui.checkbox(&mut self.popup_open, "");
rust_view_ui(ui, ";");
});
ui.monospace("");
rust_view_ui(ui, "let response = ui.button(\"Click me!\");");
rust_view_ui(ui, "Popup::menu(&response)");
rust_view_ui(ui, " .gap(gap).align(align)");
rust_view_ui(ui, " .close_behavior(close_behavior)");
rust_view_ui(ui, " .show(|ui| { /* menu contents */ });");
});
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4806984f9c801a054cea80b89664293680abaa57cf0a95cf9682f111e3794fc1
size 25080
oid sha256:17dc2a2f98d4cc52f6c6337dcc2e40f22d7310a933de91bb60576b893926193c
size 58674