Skip to content

Commit

Permalink
New menu based on egui::Popup
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasmerlin committed Feb 13, 2025
1 parent bc5a750 commit 3a71bd5
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 9 deletions.
148 changes: 148 additions & 0 deletions crates/egui/src/containers/menu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use crate::{
Button, Color32, Frame, Id, InnerResponse, Layout, PointerState, Popup, Response, Style, Ui,
UiKind, UiStack, Widget,
};
use emath::{vec2, Align, Align4};
use epaint::Stroke;
use std::sync::Arc;

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.hovered.bg_stroke = Stroke::NONE;
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
}

pub fn global_menu_state_id() -> Id {
Id::new("global_menu_state")
}

pub fn find_sub_menu_root(ui: &Ui) -> &UiStack {
ui.stack()
.iter()
.find(|stack| {
// TODO: Add a MenuContainer widget that allows one to create a submenu from anywhere using UiStack::tags
stack.is_root_ui() || [Some(UiKind::Popup), Some(UiKind::Menu)].contains(&stack.kind())
})
// It's fine to unwrap since we should always find the root
.unwrap()
}

pub struct GlobalMenuState {
popup_id: Option<Id>,
}

#[derive(Default, Clone)]
pub struct MenuState {
pub open_item: Option<Id>,
}

impl MenuState {
pub fn from_ui<R>(ui: &Ui, f: impl FnOnce(&mut Self, &UiStack) -> R) -> R {
let stack = find_sub_menu_root(ui);
ui.data_mut(|data| {
let state = data.get_temp_mut_or_default(stack.id);
f(state, stack)
})
}

pub fn from_id<R>(ui: &Ui, id: Id, f: impl FnOnce(&mut Self) -> R) -> R {
ui.data_mut(|data| {
let state = data.get_temp_mut_or_default(id);
f(state)
})
}
}

pub struct Menu<'a> {
popup: Popup<'a>,
}

pub struct SubMenuButton {}
impl SubMenuButton {
pub fn new() -> Self {
Self {}
}

pub fn ui<R>(
self,
ui: &mut Ui,
content: impl FnOnce(&mut Ui) -> R,
) -> (Response, Option<InnerResponse<R>>) {
let frame = Frame::menu(ui.style());

let response = Button::new("Menu")
.shortcut_text("⏵") // TODO: Somehow set a color for the shortcut text
.ui(ui);

let id = response.id.with("submenu");

let (open_item, menu_id) =
MenuState::from_ui(ui, |state, stack| (state.open_item, stack.id));

let mut menu_root_response = ui
.ctx()
.read_response(menu_id)
// Since we are a child of that ui, this should always exist
.unwrap();

let hover_pos = ui.ctx().pointer_hover_pos();

// We don't care if the users is hovering over the border
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 = response.rect.expand2(ui.style().spacing.item_spacing / 2.0);
let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos));

if !is_any_open && is_hovered {
set_open = Some(true);
is_open = true;
}

let gap = frame.total_margin().sum().x / 2.0;

let popup_response = Popup::from_response(&response)
.open(is_open)
.position(Align4::RIGHT_START)
.layout(Layout::top_down_justified(Align::Min))
.gap(gap)
.style(menu_style)
.show(ui.ctx(), content);

if let Some(popup_response) = &popup_response {
let is_moving_towards_rect = ui.input(|i| {
i.pointer
.is_moving_towards_rect(&popup_response.response.rect)
});
if is_moving_towards_rect {
// We need to repaint while this is true, so we can detect when
// the pointer is no longer moving towards the rect
ui.ctx().request_repaint();
}
if is_open
&& !is_hovered
&& !popup_response.response.contains_pointer()
&& !is_moving_towards_rect
&& is_hovering_menu
{
set_open = Some(false);
}
}

if let Some(set_open) = set_open {
MenuState::from_id(ui, menu_id, |state| {
state.open_item = set_open.then_some(id);
});
}

(response, popup_response)
}
}
1 change: 1 addition & 0 deletions crates/egui/src/containers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) mod area;
pub mod collapsing_header;
mod combo_box;
pub mod frame;
pub mod menu;
pub mod modal;
pub mod old_popup;
pub mod panel;
Expand Down
20 changes: 18 additions & 2 deletions crates/egui/src/containers/popup.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::containers::menu::menu_style;
use crate::{
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
Sense, Ui, UiKind,
Sense, Style, Ui, UiKind,
};
use emath::{vec2, Align, Align4, Pos2, Rect};
use std::iter::once;
Expand Down Expand Up @@ -160,6 +161,7 @@ pub struct Popup<'a> {
sense: Sense,
layout: Layout,
frame: Option<Frame>,
style: Option<fn(&mut Style)>,
}

impl<'a> Popup<'a> {
Expand All @@ -179,6 +181,7 @@ impl<'a> Popup<'a> {
sense: Sense::click(),
layout: Layout::default(),
frame: None,
style: None,
}
}

Expand Down Expand Up @@ -230,6 +233,7 @@ impl<'a> Popup<'a> {
PopupCloseBehavior::CloseOnClick,
)
.layout(Layout::top_down_justified(Align::Min))
.style(menu_style)
}

/// Show a context menu when the widget was secondary clicked.
Expand All @@ -243,6 +247,7 @@ impl<'a> Popup<'a> {
)
.layout(Layout::top_down_justified(Align::Min))
.at_pointer_fixed()
.style(menu_style)
}

/// Force the popup to be open or closed.
Expand Down Expand Up @@ -357,6 +362,11 @@ impl<'a> Popup<'a> {
self
}

pub fn style(mut self, style: fn(&mut Style)) -> Self {
self.style = Some(style);
self
}

/// Is the popup open?
pub fn is_open(&self, ctx: &Context) -> bool {
match &self.open_kind {
Expand Down Expand Up @@ -422,6 +432,7 @@ impl<'a> Popup<'a> {
sense,
layout,
frame,
style,
} = self;

let hover_pos = ctx.pointer_hover_pos();
Expand Down Expand Up @@ -484,7 +495,12 @@ impl<'a> Popup<'a> {

let frame = frame.unwrap_or_else(|| Frame::popup(&ctx.style()));

let response = area.show(ctx, |ui| frame.show(ui, content).inner);
let response = area.show(ctx, |ui| {
if let Some(style) = style {
style(&mut ui.style_mut());
}
frame.show(ui, content).inner
});

let should_close = |close_behavior| {
let should_close = match close_behavior {
Expand Down
18 changes: 12 additions & 6 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1200,12 +1200,18 @@ impl Context {
pub fn read_response(&self, id: Id) -> Option<Response> {
self.write(|ctx| {
let viewport = ctx.viewport();
viewport
.this_pass
.widgets
.get(id)
.or_else(|| viewport.prev_pass.widgets.get(id))
.copied()
let rect = viewport.this_pass.widgets.get(id);
if let Some(rect) = rect {
if rect.rect != Rect::NOTHING {
Some(*rect)
} else if let Some(prev_rect) = viewport.prev_pass.widgets.get(id) {
Some(*prev_rect)
} else {
Some(*rect)
}
} else {
None
}
})
.map(|widget_rect| self.get_response(widget_rect))
}
Expand Down
12 changes: 12 additions & 0 deletions crates/egui/src/input_state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1325,6 +1325,18 @@ impl PointerState {
pub fn middle_down(&self) -> bool {
self.button_down(PointerButton::Middle)
}

/// Is the mouse moving in the direction of the given rect?
pub fn is_moving_towards_rect(&self, rect: &Rect) -> bool {
if self.is_still() {
return false;
}

if let Some(pos) = self.hover_pos() {
return rect.intersects_ray(pos, self.direction());
}
false
}
}

impl InputState {
Expand Down
21 changes: 20 additions & 1 deletion crates/egui_demo_lib/src/demo/popups.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use egui::{vec2, Align2, Align4, ComboBox, Frame, Id, Popup, PopupCloseBehavior, Tooltip, Ui};
use egui::containers::menu::SubMenuButton;
use egui::{
vec2, Align2, Align4, ComboBox, Frame, Id, Popup, PopupCloseBehavior, Pos2, Tooltip, Ui,
};

/// Showcase [`Popup`].
#[derive(Clone, PartialEq)]
Expand Down Expand Up @@ -153,6 +156,8 @@ impl crate::View for PopupsDemo {
.show(ui.ctx(), |ui| {
_ = ui.button("Menu item 1");
_ = ui.button("Menu item 2");

submenus(ui);
});

self.apply_options(Popup::context_menu(&response).id(Id::new("context_menu")))
Expand All @@ -179,3 +184,17 @@ impl crate::View for PopupsDemo {
});
}
}

fn submenus(ui: &mut Ui) {
SubMenuButton::new().ui(ui, |ui| {
_ = ui.button("Item 1");
_ = ui.button("Item 2");

submenus(ui);
});
SubMenuButton::new().ui(ui, |ui| {
submenus(ui);
_ = ui.button("Item 1");
_ = ui.button("Item 2");
});
}

0 comments on commit 3a71bd5

Please sign in to comment.