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

Port menu to AtomUi

This commit is contained in:
Lucas Meurer
2026-08-20 18:10:10 +02:00
parent 97b38be791
commit 610c459076
3 changed files with 244 additions and 36 deletions

View File

@@ -1,9 +1,11 @@
use crate::{ use crate::{
Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Button, Context, Id, InnerResponse, IntoAtoms, Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Button, ClosableTag, Context, Id,
Layout, Response, Sense, Spacing, Style, Ui, UiBuilder, Visuals, WidgetRect, InnerResponse, IntoAtoms, LayerId, Layout, Response, Sense, Spacing, Style, Ui, UiBuilder,
UiStack, Visuals, WidgetRect,
}; };
use emath::{Align, Pos2, Rect, Vec2}; use emath::{Align, Pos2, Rect, Vec2};
use epaint::Direction; use epaint::Direction;
use std::sync::Arc;
pub fn atom() -> Atom<'static> { pub fn atom() -> Atom<'static> {
Atom::default() Atom::default()
@@ -62,6 +64,22 @@ macro_rules! impl_widget_for_atom_widget {
pub trait IsAtomWidgetContext { pub trait IsAtomWidgetContext {
fn ctx(&self) -> &crate::Context; fn ctx(&self) -> &crate::Context;
/// Where we are in the [`UiStack`].
///
/// Widgets built from atoms don't need this, but things that also open a container do:
/// [`crate::SubMenuButton`] needs it to find the menu it sits in.
fn stack(&self) -> &Arc<UiStack>;
/// The [`LayerId`] the widgets are drawn on.
fn layer_id(&self) -> LayerId;
/// The [`Id`] the next widget will get, without claiming it.
///
/// Use this when a widget's state has to be read before the widget is added, like
/// [`crate::SubMenuButton`] reading whether its submenu is open.
fn next_auto_id(&self) -> Id;
fn make_auto_id(&mut self) -> Id; fn make_auto_id(&mut self) -> Id;
fn is_enabled(&self) -> bool; fn is_enabled(&self) -> bool;
@@ -86,6 +104,36 @@ pub trait IsAtomWidgetContext {
fn read_response(&self, id: Id) -> Response; fn read_response(&self, id: Id) -> Response;
fn child_ui(&mut self, builder: UiBuilder) -> Ui; fn child_ui(&mut self, builder: UiBuilder) -> Ui;
/// Close the closest closable container, e.g. the menu we are in.
///
/// See [`Ui::close`].
fn close(&self) {
let tag = self.stack().iter().find_map(|stack| {
stack
.info
.tags
.get_downcast::<ClosableTag>(ClosableTag::NAME)
});
if let Some(tag) = tag {
tag.set_close();
} else {
log::warn!("Called close() on something that has no closable parent.");
}
}
/// Will any closable container we are in close this frame?
///
/// See [`Ui::will_parent_close`].
fn will_parent_close(&self) -> bool {
self.stack().iter().any(|stack| {
stack
.info
.tags
.get_downcast::<ClosableTag>(ClosableTag::NAME)
.is_some_and(|tag| tag.should_close())
})
}
} }
pub type AtomWidgetContext = dyn IsAtomWidgetContext; pub type AtomWidgetContext = dyn IsAtomWidgetContext;
@@ -95,8 +143,20 @@ impl IsAtomWidgetContext for Ui {
self.ctx() self.ctx()
} }
fn stack(&self) -> &Arc<UiStack> {
self.stack()
}
fn layer_id(&self) -> LayerId {
self.layer_id()
}
fn next_auto_id(&self) -> Id {
self.next_auto_id()
}
fn make_auto_id(&mut self) -> Id { fn make_auto_id(&mut self) -> Id {
let id = self.next_auto_id(); let id = IsAtomWidgetContext::next_auto_id(self);
self.skip_ahead_auto_ids(1); self.skip_ahead_auto_ids(1);
id id
} }
@@ -133,6 +193,18 @@ impl<'ui, 'layout> AtomUi<'ui, 'layout> {
Self { ctx, layout } Self { ctx, layout }
} }
/// The context the widgets are built in.
///
/// Needed by things that also open a container, like [`crate::SubMenuButton`].
pub fn context(&self) -> &AtomWidgetContext {
self.ctx
}
/// The [`Id`] the next widget added here will get, without claiming it.
pub fn next_auto_id(&self) -> Id {
self.ctx.next_auto_id()
}
/// The [`Style`] the widgets built here will use. /// The [`Style`] the widgets built here will use.
pub fn style(&self) -> &Style { pub fn style(&self) -> &Style {
self.ctx.style() self.ctx.style()
@@ -311,3 +383,40 @@ fn read_or_default_response(ui: &Ui, id: Id, sense: Sense) -> Response {
}) })
}) })
} }
pub trait AnyUi<'a> {
fn add(&mut self, w: impl AtomWidget<'a>) -> Response;
fn style(&self) -> &Style;
fn style_mut(&mut self) -> &mut Style;
}
impl<'a> AnyUi<'a> for AtomUi<'_, 'a> {
fn add(&mut self, w: impl AtomWidget<'a>) -> Response {
self.add(atom(), w)
}
fn style(&self) -> &Style {
self.style()
}
fn style_mut(&mut self) -> &mut Style {
self.style_mut()
}
}
impl<'a> AnyUi<'a> for Ui {
fn add(&mut self, w: impl AtomWidget<'a>) -> Response {
let layout = w.show_for(self).0;
self.add(layout)
}
fn style(&self) -> &Style {
self.style()
}
fn style_mut(&mut self) -> &mut Style {
self.style_mut()
}
}

View File

@@ -658,7 +658,13 @@ impl Area {
let sized = layout.measure(&content_ui, prepared.available_size); let sized = layout.measure(&content_ui, prepared.available_size);
let size = sized.outer_size; let size = sized.outer_size;
prepared.resize(ctx, size); prepared.resize(ctx, size);
sized.show_at(&content_ui, prepared.state.rect()); let rect = prepared.state.rect();
// Atoms are painted, not allocated, so the `Ui` never learns where it ended up.
// Tell it, so `Ui::response` is right for whoever reads it (menus do).
content_ui.force_set_min_rect(rect);
sized.show_at(&content_ui, rect);
let response = prepared.end_with_size(ctx, content_ui, size); let response = prepared.end_with_size(ctx, content_ui, size);
InnerResponse { inner, response } InnerResponse { inner, response }

View File

@@ -10,8 +10,9 @@
use crate::style::StyleModifier; use crate::style::StyleModifier;
use crate::{ use crate::{
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup, AnyUi, Atom, AtomUi, AtomWidgetContext, Button, Color32, Context, Frame, Id, InnerResponse,
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _, IntoAtoms, Layout, PointerButton, Popup, PopupCloseBehavior, Response, Style, Ui, UiBuilder,
UiKind, UiStack, UiStackInfo, Widget as _,
}; };
use emath::{Align, RectAlign, Vec2, vec2}; use emath::{Align, RectAlign, Vec2, vec2};
use epaint::Stroke; use epaint::Stroke;
@@ -30,7 +31,12 @@ pub fn menu_style(style: &mut Style) {
/// Find the root [`UiStack`] of the menu. /// Find the root [`UiStack`] of the menu.
pub fn find_menu_root(ui: &Ui) -> &UiStack { pub fn find_menu_root(ui: &Ui) -> &UiStack {
ui.stack() find_menu_root_in_stack(ui.stack())
}
/// Find the root [`UiStack`] of the menu, starting from `stack`.
pub fn find_menu_root_in_stack(stack: &UiStack) -> &UiStack {
stack
.iter() .iter()
.find(|stack| { .find(|stack| {
stack.is_root_ui() stack.is_root_ui()
@@ -144,8 +150,17 @@ impl MenuState {
/// Find the root of the menu and get the state /// Find the root of the menu and get the state
pub fn from_ui<R>(ui: &Ui, f: impl FnOnce(&mut Self, &UiStack) -> R) -> R { pub fn from_ui<R>(ui: &Ui, f: impl FnOnce(&mut Self, &UiStack) -> R) -> R {
let stack = find_menu_root(ui); Self::from_stack(ui.ctx(), ui.stack(), f)
Self::from_id(ui.ctx(), stack.id, |state| f(state, stack)) }
/// Find the root of the menu and get the state, from a [`UiStack`].
pub fn from_stack<R>(
ctx: &Context,
stack: &UiStack,
f: impl FnOnce(&mut Self, &UiStack) -> R,
) -> R {
let root = find_menu_root_in_stack(stack);
Self::from_id(ctx, root.id, |state| f(state, root))
} }
/// Get the state via the menus root [`Ui`] id /// Get the state via the menus root [`Ui`] id
@@ -373,22 +388,62 @@ impl<'a> SubMenuButton<'a> {
ui: &mut Ui, ui: &mut Ui,
content: impl FnOnce(&mut Ui) -> R, content: impl FnOnce(&mut Ui) -> R,
) -> (Response, Option<InnerResponse<R>>) { ) -> (Response, Option<InnerResponse<R>>) {
let my_id = ui.next_auto_id(); let Self { button, sub_menu } = self;
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;
// TODO(lucasmerlin) add `open` function to `Button`
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); let open = is_sub_menu_open(ui, ui.next_auto_id());
let response = with_open_style(ui, open, |ui| button.ui(ui));
let popup_response = sub_menu.show(ui, &response, content);
(response, popup_response) (response, popup_response)
} }
/// Show the submenu button in an [`AtomUi`], with [`crate::Atom`]-based submenu contents.
///
/// Like [`Self::ui`], but both the button and the submenu are built from atoms, so the
/// submenu needs no sizing pass. See [`Popup::show_atom`].
pub fn atom_ui<'l, R>(
self,
ui: &mut AtomUi<'_, 'l>,
content: impl FnOnce(&mut AtomUi<'_, 'l>) -> R,
) -> (Response, Option<InnerResponse<R>>)
where
'a: 'l,
{
let Self { button, sub_menu } = self;
let open = is_sub_menu_open(ui.context(), ui.next_auto_id());
let response = with_open_style(ui, open, |ui| ui.add(Atom::default(), button));
let popup_response = sub_menu.show_atom(ui.context(), &response, content);
(response, popup_response)
}
}
/// Is the submenu belonging to the widget with this id open?
fn is_sub_menu_open(ui: &AtomWidgetContext, widget_id: Id) -> bool {
MenuState::from_stack(ui.ctx(), ui.stack(), |state, _| {
state.open_item == Some(SubMenu::id_from_widget_id(widget_id))
})
}
/// Run `add_button` with the `open` widget visuals in place of the inactive ones, so an open
/// submenu's button looks open.
// TODO(lucasmerlin) add `open` function to `Button`
fn with_open_style<'a, UI: AnyUi<'a>, R>(
ui: &mut UI,
open: bool,
add_button: impl FnOnce(&mut UI) -> R,
) -> R {
if !open {
return add_button(ui);
}
let inactive = ui.style().visuals.widgets.inactive;
ui.style_mut().visuals.widgets.inactive = ui.style().visuals.widgets.open;
let response = add_button(ui);
ui.style_mut().visuals.widgets.inactive = inactive;
response
} }
/// Show a submenu in a menu. /// Show a submenu in a menu.
@@ -428,15 +483,51 @@ impl SubMenu {
ui: &Ui, ui: &Ui,
button_response: &Response, button_response: &Response,
content: impl FnOnce(&mut Ui) -> R, content: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
self.show_impl(ui, button_response, |popup| {
popup.show(|ui| {
keep_menu_on_top(ui, button_response);
content(ui)
})
})
}
/// Show the submenu, with [`crate::Atom`]-based contents.
///
/// Like [`Self::show`], but the submenu is measured before it is painted, so it is
/// correctly sized and placed on the frame it opens. See [`Popup::show_atom`].
pub fn show_atom<'l, R>(
self,
ui: &AtomWidgetContext,
button_response: &Response,
content: impl FnOnce(&mut AtomUi<'_, 'l>) -> R,
) -> Option<InnerResponse<R>> {
self.show_impl(ui, button_response, |popup| {
popup.show_atom(|atom_ui| {
keep_menu_on_top(atom_ui.context(), button_response);
content(atom_ui)
})
})
}
/// Everything [`Self::show`] and [`Self::show_atom`] have in common: deciding whether the
/// submenu should be open, configuring the [`Popup`], and handling the close behavior.
/// `show_popup` shows the contents.
fn show_impl<R>(
self,
ui: &AtomWidgetContext,
button_response: &Response,
show_popup: impl FnOnce(Popup<'_>) -> Option<InnerResponse<R>>,
) -> Option<InnerResponse<R>> { ) -> Option<InnerResponse<R>> {
let frame = Frame::menu(ui.style()); let frame = Frame::menu(ui.style());
let id = Self::id_from_widget_id(button_response.id); let id = Self::id_from_widget_id(button_response.id);
// Get the state from the parent menu // Get the state from the parent menu
let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| { let (open_item, menu_id, parent_config) =
(state.open_item, stack.id, MenuConfig::from_stack(stack)) MenuState::from_stack(ui.ctx(), ui.stack(), |state, stack| {
}); (state.open_item, stack.id, MenuConfig::from_stack(stack))
});
let mut menu_config = self.config.unwrap_or_else(|| parent_config.clone()); let mut menu_config = self.config.unwrap_or_else(|| parent_config.clone());
menu_config.bar = false; menu_config.bar = false;
@@ -499,7 +590,7 @@ impl SubMenu {
let expand = Vec2::new(0.0, frame.total_margin().sum().y / 2.0); let expand = Vec2::new(0.0, frame.total_margin().sum().y / 2.0);
response.interact_rect = response.interact_rect.expand2(expand); response.interact_rect = response.interact_rect.expand2(expand);
let popup_response = Popup::from_response(&response) let popup = Popup::from_response(&response)
.id(id) .id(id)
.open(is_open) .open(is_open)
.align(RectAlign::RIGHT_START) .align(RectAlign::RIGHT_START)
@@ -512,14 +603,9 @@ impl SubMenu {
.info( .info(
UiStackInfo::new(UiKind::Menu) UiStackInfo::new(UiKind::Menu)
.with_tag_value(MenuConfig::MENU_CONFIG_TAG, menu_config.clone()), .with_tag_value(MenuConfig::MENU_CONFIG_TAG, menu_config.clone()),
) );
.show(|ui| {
// Ensure our layer stays on top when the button is clicked let popup_response = show_popup(popup);
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 { if let Some(popup_response) = &popup_response {
// If no child sub menu is open means we must be the deepest child sub menu. // If no child sub menu is open means we must be the deepest child sub menu.
@@ -556,14 +642,14 @@ impl SubMenu {
ui.close(); ui.close();
} }
let is_moving_towards_rect = ui.input(|i| { let is_moving_towards_rect = ui.ctx().input(|i| {
i.pointer i.pointer
.is_moving_towards_rect(&popup_response.response.rect) .is_moving_towards_rect(&popup_response.response.rect)
}); });
if is_moving_towards_rect { if is_moving_towards_rect {
// We need to repaint while this is true, so we can detect when // We need to repaint while this is true, so we can detect when
// the pointer is no longer moving towards the rect // the pointer is no longer moving towards the rect
ui.request_repaint(); ui.ctx().request_repaint();
} }
let hovering_other_menu_entry = is_open let hovering_other_menu_entry = is_open
&& !is_hovered && !is_hovered
@@ -583,7 +669,7 @@ impl SubMenu {
} }
if ui.will_parent_close() { if ui.will_parent_close() {
ui.data_mut(|data| data.remove_by_type::<MenuState>()); ui.ctx().data_mut(|data| data.remove_by_type::<MenuState>());
} }
} }
@@ -596,3 +682,10 @@ impl SubMenu {
popup_response popup_response
} }
} }
/// Ensure the submenu's layer stays on top while its button is being clicked.
fn keep_menu_on_top(ui: &AtomWidgetContext, button_response: &Response) {
if button_response.clicked() || button_response.is_pointer_button_down_on() {
ui.ctx().move_to_top(ui.layer_id());
}
}