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:
@@ -1,9 +1,11 @@
|
||||
use crate::{
|
||||
Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Button, Context, Id, InnerResponse, IntoAtoms,
|
||||
Layout, Response, Sense, Spacing, Style, Ui, UiBuilder, Visuals, WidgetRect,
|
||||
Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Button, ClosableTag, Context, Id,
|
||||
InnerResponse, IntoAtoms, LayerId, Layout, Response, Sense, Spacing, Style, Ui, UiBuilder,
|
||||
UiStack, Visuals, WidgetRect,
|
||||
};
|
||||
use emath::{Align, Pos2, Rect, Vec2};
|
||||
use epaint::Direction;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn atom() -> Atom<'static> {
|
||||
Atom::default()
|
||||
@@ -62,6 +64,22 @@ macro_rules! impl_widget_for_atom_widget {
|
||||
|
||||
pub trait IsAtomWidgetContext {
|
||||
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 is_enabled(&self) -> bool;
|
||||
@@ -86,6 +104,36 @@ pub trait IsAtomWidgetContext {
|
||||
fn read_response(&self, id: Id) -> Response;
|
||||
|
||||
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;
|
||||
@@ -95,8 +143,20 @@ impl IsAtomWidgetContext for Ui {
|
||||
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 {
|
||||
let id = self.next_auto_id();
|
||||
let id = IsAtomWidgetContext::next_auto_id(self);
|
||||
self.skip_ahead_auto_ids(1);
|
||||
id
|
||||
}
|
||||
@@ -133,6 +193,18 @@ impl<'ui, 'layout> AtomUi<'ui, '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.
|
||||
pub fn style(&self) -> &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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,7 +658,13 @@ impl Area {
|
||||
let sized = layout.measure(&content_ui, prepared.available_size);
|
||||
let size = sized.outer_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);
|
||||
InnerResponse { inner, response }
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
|
||||
use crate::style::StyleModifier;
|
||||
use crate::{
|
||||
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup,
|
||||
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
|
||||
AnyUi, Atom, AtomUi, AtomWidgetContext, Button, Color32, Context, Frame, Id, InnerResponse,
|
||||
IntoAtoms, Layout, PointerButton, Popup, PopupCloseBehavior, Response, Style, Ui, UiBuilder,
|
||||
UiKind, UiStack, UiStackInfo, Widget as _,
|
||||
};
|
||||
use emath::{Align, RectAlign, Vec2, vec2};
|
||||
use epaint::Stroke;
|
||||
@@ -30,7 +31,12 @@ pub fn menu_style(style: &mut Style) {
|
||||
|
||||
/// Find the root [`UiStack`] of the menu.
|
||||
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()
|
||||
.find(|stack| {
|
||||
stack.is_root_ui()
|
||||
@@ -144,8 +150,17 @@ impl MenuState {
|
||||
|
||||
/// 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 {
|
||||
let stack = find_menu_root(ui);
|
||||
Self::from_id(ui.ctx(), stack.id, |state| f(state, stack))
|
||||
Self::from_stack(ui.ctx(), ui.stack(), f)
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -373,22 +388,62 @@ impl<'a> SubMenuButton<'a> {
|
||||
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;
|
||||
// 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 Self { button, sub_menu } = self;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -428,15 +483,51 @@ impl SubMenu {
|
||||
ui: &Ui,
|
||||
button_response: &Response,
|
||||
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>> {
|
||||
let frame = Frame::menu(ui.style());
|
||||
|
||||
let id = Self::id_from_widget_id(button_response.id);
|
||||
|
||||
// Get the state from the parent menu
|
||||
let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| {
|
||||
(state.open_item, stack.id, MenuConfig::from_stack(stack))
|
||||
});
|
||||
let (open_item, menu_id, parent_config) =
|
||||
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());
|
||||
menu_config.bar = false;
|
||||
@@ -499,7 +590,7 @@ impl SubMenu {
|
||||
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)
|
||||
let popup = Popup::from_response(&response)
|
||||
.id(id)
|
||||
.open(is_open)
|
||||
.align(RectAlign::RIGHT_START)
|
||||
@@ -512,14 +603,9 @@ impl SubMenu {
|
||||
.info(
|
||||
UiStackInfo::new(UiKind::Menu)
|
||||
.with_tag_value(MenuConfig::MENU_CONFIG_TAG, menu_config.clone()),
|
||||
)
|
||||
.show(|ui| {
|
||||
// Ensure our layer stays on top when the button is clicked
|
||||
if button_response.clicked() || button_response.is_pointer_button_down_on() {
|
||||
ui.ctx().move_to_top(ui.layer_id());
|
||||
}
|
||||
content(ui)
|
||||
});
|
||||
);
|
||||
|
||||
let popup_response = show_popup(popup);
|
||||
|
||||
if let Some(popup_response) = &popup_response {
|
||||
// If no child sub menu is open means we must be the deepest child sub menu.
|
||||
@@ -556,14 +642,14 @@ impl SubMenu {
|
||||
ui.close();
|
||||
}
|
||||
|
||||
let is_moving_towards_rect = ui.input(|i| {
|
||||
let is_moving_towards_rect = ui.ctx().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.request_repaint();
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
let hovering_other_menu_entry = is_open
|
||||
&& !is_hovered
|
||||
@@ -583,7 +669,7 @@ impl SubMenu {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user