use crate::style::StyleModifier;
use crate::{
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
};
use emath::{Align, RectAlign, Vec2, vec2};
use epaint::Stroke;
pub fn menu_style(style: &mut Style) {
style.spacing.button_padding = vec2(2.0, 0.0);
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
style.visuals.widgets.open.bg_stroke = Stroke::NONE;
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
}
pub fn find_menu_root(ui: &Ui) -> &UiStack {
ui.stack()
.iter()
.find(|stack| {
stack.is_root_ui()
|| [Some(UiKind::Popup), Some(UiKind::Menu)].contains(&stack.kind())
|| stack.info.tags.contains(MenuConfig::MENU_CONFIG_TAG)
})
.expect("We should always find the root")
}
pub fn is_in_menu(ui: &Ui) -> bool {
for stack in ui.stack().iter() {
if let Some(config) = stack
.info
.tags
.get_downcast::<MenuConfig>(MenuConfig::MENU_CONFIG_TAG)
{
return !config.bar;
}
if [Some(UiKind::Popup), Some(UiKind::Menu)].contains(&stack.kind()) {
return true;
}
}
false
}
#[derive(Clone, Debug)]
pub struct MenuConfig {
bar: bool,
pub close_behavior: PopupCloseBehavior,
pub style: StyleModifier,
}
impl Default for MenuConfig {
fn default() -> Self {
Self {
close_behavior: PopupCloseBehavior::default(),
bar: false,
style: menu_style.into(),
}
}
}
impl MenuConfig {
pub const MENU_CONFIG_TAG: &'static str = "egui_menu_config";
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
self.close_behavior = close_behavior;
self
}
#[inline]
pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
self.style = style.into();
self
}
fn from_stack(stack: &UiStack) -> Self {
stack
.info
.tags
.get_downcast(Self::MENU_CONFIG_TAG)
.cloned()
.unwrap_or_default()
}
pub fn find(ui: &Ui) -> Self {
find_menu_root(ui)
.info
.tags
.get_downcast(Self::MENU_CONFIG_TAG)
.cloned()
.unwrap_or_default()
}
}
#[derive(Clone)]
pub struct MenuState {
pub open_item: Option<Id>,
last_visible_pass: u64,
}
impl MenuState {
pub const ID: &'static str = "menu_state";
pub fn from_ui<R>(ui: &Ui, f: impl FnOnce(&mut Self, &UiStack) -> R) -> R {
let stack = find_menu_root(ui);
Self::from_id(ui.ctx(), stack.id, |state| f(state, stack))
}
pub fn from_id<R>(ctx: &Context, id: Id, f: impl FnOnce(&mut Self) -> R) -> R {
let pass_nr = ctx.cumulative_pass_nr();
ctx.data_mut(|data| {
let state_id = id.with(Self::ID);
let mut state = data.get_temp(state_id).unwrap_or(Self {
open_item: None,
last_visible_pass: pass_nr,
});
if state.last_visible_pass + 1 < pass_nr {
state.open_item = None;
}
if let Some(item) = state.open_item {
if data
.get_temp(item.with(Self::ID))
.is_none_or(|item: Self| item.last_visible_pass + 1 < pass_nr)
{
state.open_item = None;
}
}
let r = f(&mut state);
data.insert_temp(state_id, state);
r
})
}
pub fn mark_shown(ctx: &Context, id: Id) {
let pass_nr = ctx.cumulative_pass_nr();
Self::from_id(ctx, id, |state| {
state.last_visible_pass = pass_nr;
});
}
pub fn is_deepest_open_sub_menu(ctx: &Context, id: Id) -> bool {
let pass_nr = ctx.cumulative_pass_nr();
let open_item = Self::from_id(ctx, id, |state| state.open_item);
open_item.is_none_or(|submenu_id| {
Self::from_id(ctx, submenu_id, |state| state.last_visible_pass != pass_nr)
})
}
}
#[derive(Clone, Debug)]
pub struct MenuBar {
config: MenuConfig,
style: StyleModifier,
}
#[deprecated = "Renamed to `egui::MenuBar`"]
pub type Bar = MenuBar;
impl Default for MenuBar {
fn default() -> Self {
Self {
config: MenuConfig::default(),
style: menu_style.into(),
}
}
}
impl MenuBar {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
self.style = style.into();
self
}
#[inline]
pub fn config(mut self, config: MenuConfig) -> Self {
self.config = config;
self
}
#[inline]
pub fn ui<R>(self, ui: &mut Ui, content: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
let Self { mut config, style } = self;
config.bar = true;
ui.horizontal(|ui| {
ui.scope_builder(
UiBuilder::new()
.layout(Layout::left_to_right(Align::Center))
.ui_stack_info(
UiStackInfo::new(UiKind::Menu)
.with_tag_value(MenuConfig::MENU_CONFIG_TAG, config),
),
|ui| {
style.apply(ui.style_mut());
let height = ui.spacing().interact_size.y;
ui.set_min_size(vec2(ui.available_width(), height));
content(ui)
},
)
.inner
})
}
}
pub struct MenuButton<'a> {
pub button: Button<'a>,
pub config: Option<MenuConfig>,
}
impl<'a> MenuButton<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self::from_button(Button::new(atoms.into_atoms()))
}
#[inline]
pub fn config(mut self, config: MenuConfig) -> Self {
self.config = Some(config);
self
}
#[inline]
pub fn from_button(button: Button<'a>) -> Self {
Self {
button,
config: None,
}
}
pub fn ui<R>(
self,
ui: &mut Ui,
content: impl FnOnce(&mut Ui) -> R,
) -> (Response, Option<InnerResponse<R>>) {
let response = self.button.ui(ui);
let mut config = self.config.unwrap_or_else(|| MenuConfig::find(ui));
config.bar = false;
let inner = Popup::menu(&response)
.close_behavior(config.close_behavior)
.style(config.style.clone())
.info(
UiStackInfo::new(UiKind::Menu).with_tag_value(MenuConfig::MENU_CONFIG_TAG, config),
)
.show(content);
(response, inner)
}
}
pub struct SubMenuButton<'a> {
pub button: Button<'a>,
pub sub_menu: SubMenu,
}
impl<'a> SubMenuButton<'a> {
pub const RIGHT_ARROW: &'static str = "⏵";
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self::from_button(Button::new(atoms.into_atoms()).right_text("⏵"))
}
pub fn from_button(button: Button<'a>) -> Self {
Self {
button,
sub_menu: SubMenu::default(),
}
}
#[inline]
pub fn config(mut self, config: MenuConfig) -> Self {
self.sub_menu.config = Some(config);
self
}
pub fn ui<R>(
self,
ui: &mut Ui,
content: impl FnOnce(&mut Ui) -> R,
) -> (Response, Option<InnerResponse<R>>) {
let my_id = ui.next_auto_id();
let open = MenuState::from_ui(ui, |state, _| {
state.open_item == Some(SubMenu::id_from_widget_id(my_id))
});
let inactive = ui.style().visuals.widgets.inactive;
if open {
ui.style_mut().visuals.widgets.inactive = ui.style().visuals.widgets.open;
}
let response = self.button.ui(ui);
ui.style_mut().visuals.widgets.inactive = inactive;
let popup_response = self.sub_menu.show(ui, &response, content);
(response, popup_response)
}
}
#[derive(Clone, Debug, Default)]
pub struct SubMenu {
config: Option<MenuConfig>,
}
impl SubMenu {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn config(mut self, config: MenuConfig) -> Self {
self.config = Some(config);
self
}
pub fn id_from_widget_id(widget_id: Id) -> Id {
widget_id.with("submenu")
}
pub fn show<R>(
self,
ui: &Ui,
button_response: &Response,
content: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
let frame = Frame::menu(ui.style());
let id = Self::id_from_widget_id(button_response.id);
let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| {
(state.open_item, stack.id, MenuConfig::from_stack(stack))
});
let mut menu_config = self.config.unwrap_or_else(|| parent_config.clone());
menu_config.bar = false;
let menu_root_response = ui
.ctx()
.read_response(menu_id)
.unwrap();
let hover_pos = ui.ctx().pointer_hover_pos();
let menu_rect = menu_root_response.rect - frame.total_margin();
let is_hovering_menu = hover_pos.is_some_and(|pos| {
ui.ctx().layer_id_at(pos) == Some(menu_root_response.layer_id)
&& menu_rect.contains(pos)
});
let is_any_open = open_item.is_some();
let mut is_open = open_item == Some(id);
let mut set_open = None;
let button_rect = button_response
.rect
.expand2(ui.style().spacing.item_spacing / 2.0);
let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos));
let should_open =
ui.is_enabled() && (button_response.clicked() || (is_hovered && !is_any_open));
if should_open {
set_open = Some(true);
is_open = true;
MenuState::from_id(ui.ctx(), menu_id, |state| {
state.open_item = None;
});
}
let gap = frame.total_margin().sum().x / 2.0 + 2.0;
let mut response = button_response.clone();
let expand = Vec2::new(0.0, frame.total_margin().sum().y / 2.0);
response.interact_rect = response.interact_rect.expand2(expand);
let popup_response = Popup::from_response(&response)
.id(id)
.open(is_open)
.align(RectAlign::RIGHT_START)
.layout(Layout::top_down_justified(Align::Min))
.gap(gap)
.style(menu_config.style.clone())
.frame(frame)
.close_behavior(PopupCloseBehavior::IgnoreClicks)
.info(
UiStackInfo::new(UiKind::Menu)
.with_tag_value(MenuConfig::MENU_CONFIG_TAG, menu_config.clone()),
)
.show(|ui| {
if button_response.clicked() || button_response.is_pointer_button_down_on() {
ui.ctx().move_to_top(ui.layer_id());
}
content(ui)
});
if let Some(popup_response) = &popup_response {
let is_deepest_submenu = MenuState::is_deepest_open_sub_menu(ui.ctx(), id);
let clicked_outside = is_deepest_submenu
&& popup_response.response.clicked_elsewhere()
&& menu_root_response.clicked_elsewhere();
let submenu_button_clicked = button_response.clicked();
let clicked_inside = is_deepest_submenu
&& !submenu_button_clicked
&& response.ctx.input(|i| i.pointer.any_click())
&& hover_pos.is_some_and(|pos| popup_response.response.interact_rect.contains(pos));
let click_close = match menu_config.close_behavior {
PopupCloseBehavior::CloseOnClick => clicked_outside || clicked_inside,
PopupCloseBehavior::CloseOnClickOutside => clicked_outside,
PopupCloseBehavior::IgnoreClicks => false,
};
if click_close {
set_open = Some(false);
ui.close();
}
let is_moving_towards_rect = ui.input(|i| {
i.pointer
.is_moving_towards_rect(&popup_response.response.rect)
});
if is_moving_towards_rect {
ui.ctx().request_repaint();
}
let hovering_other_menu_entry = is_open
&& !is_hovered
&& !popup_response.response.contains_pointer()
&& !is_moving_towards_rect
&& is_hovering_menu;
let close_called = popup_response.response.should_close();
if close_called {
ui.close();
}
if hovering_other_menu_entry {
set_open = Some(false);
}
if ui.will_parent_close() {
ui.data_mut(|data| data.remove_by_type::<MenuState>());
}
}
if let Some(set_open) = set_open {
MenuState::from_id(ui.ctx(), menu_id, |state| {
state.open_item = set_open.then_some(id);
});
}
popup_response
}
}