diff --git a/crates/egui/src/atomics/atom_widget.rs b/crates/egui/src/atomics/atom_widget.rs index 3fa56a12f..125ba03e0 100644 --- a/crates/egui/src/atomics/atom_widget.rs +++ b/crates/egui/src/atomics/atom_widget.rs @@ -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; + + /// 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::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::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 { + 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() + } +} diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index cdc5c7ec8..f10dc46a4 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -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 } diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index d4c95b298..d050ddf5f 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -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(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( + 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>) { - 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>) + 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> { + 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> { + 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( + self, + ui: &AtomWidgetContext, + button_response: &Response, + show_popup: impl FnOnce(Popup<'_>) -> Option>, ) -> Option> { 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::()); + ui.ctx().data_mut(|data| data.remove_by_type::()); } } @@ -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()); + } +}