⚠️ Improved menu based on `egui::Popup` (#5716)
Continuation of #5713 **Silently breaking changes:** - Menus now close on click by default, this is configurable via `PopupCloseBehavior` **Additional additions:** - `Button::right_text` - `StyleModifier` This is a rewrite of the egui menus, with the following goals: - submenu buttons should work everywhere, in a popup, context menu, area, in some random Ui - remove the menu state from Ui - should work just like the previous menu - ~proper support for keyboard navigation~ - It's better now but requires further work to be perfect - support `PopupCloseBehavior` * Closes #4607 * [x] I have followed the instructions in the PR template
This commit is contained in:
parent
6e3575b4c7
commit
cd22517280
|
|
@ -1393,6 +1393,7 @@ dependencies = [
|
|||
"eframe",
|
||||
"egui",
|
||||
"egui-wgpu",
|
||||
"egui_extras",
|
||||
"image",
|
||||
"kittest",
|
||||
"pollster 0.4.0",
|
||||
|
|
|
|||
|
|
@ -103,10 +103,10 @@ impl AreaState {
|
|||
///
|
||||
/// The previous rectangle used by this area can be obtained through [`crate::Memory::area_rect()`].
|
||||
#[must_use = "You should call .show()"]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Area {
|
||||
pub(crate) id: Id,
|
||||
kind: UiKind,
|
||||
info: UiStackInfo,
|
||||
sense: Option<Sense>,
|
||||
movable: bool,
|
||||
interactable: bool,
|
||||
|
|
@ -132,7 +132,7 @@ impl Area {
|
|||
pub fn new(id: Id) -> Self {
|
||||
Self {
|
||||
id,
|
||||
kind: UiKind::GenericArea,
|
||||
info: UiStackInfo::new(UiKind::GenericArea),
|
||||
sense: None,
|
||||
movable: true,
|
||||
interactable: true,
|
||||
|
|
@ -164,7 +164,16 @@ impl Area {
|
|||
/// Default to [`UiKind::GenericArea`].
|
||||
#[inline]
|
||||
pub fn kind(mut self, kind: UiKind) -> Self {
|
||||
self.kind = kind;
|
||||
self.info = UiStackInfo::new(kind);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`UiStackInfo`] of the area's [`Ui`].
|
||||
///
|
||||
/// Default to [`UiStackInfo::new(UiKind::GenericArea)`].
|
||||
#[inline]
|
||||
pub fn info(mut self, info: UiStackInfo) -> Self {
|
||||
self.info = info;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -351,7 +360,7 @@ impl Area {
|
|||
}
|
||||
|
||||
pub(crate) struct Prepared {
|
||||
kind: UiKind,
|
||||
info: Option<UiStackInfo>,
|
||||
layer_id: LayerId,
|
||||
state: AreaState,
|
||||
move_response: Response,
|
||||
|
|
@ -376,7 +385,7 @@ impl Area {
|
|||
ctx: &Context,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let prepared = self.begin(ctx);
|
||||
let mut prepared = self.begin(ctx);
|
||||
let mut content_ui = prepared.content_ui(ctx);
|
||||
let inner = add_contents(&mut content_ui);
|
||||
let response = prepared.end(ctx, content_ui);
|
||||
|
|
@ -386,7 +395,7 @@ impl Area {
|
|||
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
|
||||
let Self {
|
||||
id,
|
||||
kind,
|
||||
info,
|
||||
sense,
|
||||
movable,
|
||||
order,
|
||||
|
|
@ -518,7 +527,7 @@ impl Area {
|
|||
move_response.interact_rect = state.rect();
|
||||
|
||||
Prepared {
|
||||
kind,
|
||||
info: Some(info),
|
||||
layer_id,
|
||||
state,
|
||||
move_response,
|
||||
|
|
@ -549,11 +558,11 @@ impl Prepared {
|
|||
self.constrain_rect
|
||||
}
|
||||
|
||||
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
|
||||
pub(crate) fn content_ui(&mut self, ctx: &Context) -> Ui {
|
||||
let max_rect = self.state.rect();
|
||||
|
||||
let mut ui_builder = UiBuilder::new()
|
||||
.ui_stack_info(UiStackInfo::new(self.kind))
|
||||
.ui_stack_info(self.info.take().unwrap_or_default())
|
||||
.layer_id(self.layer_id)
|
||||
.max_rect(max_rect)
|
||||
.layout(self.layout)
|
||||
|
|
@ -596,7 +605,7 @@ impl Prepared {
|
|||
#[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
|
||||
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
|
||||
let Self {
|
||||
kind: _,
|
||||
info: _,
|
||||
layer_id,
|
||||
mut state,
|
||||
move_response: mut response,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use epaint::Shape;
|
||||
|
||||
use crate::{
|
||||
epaint, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, NumExt, Painter, Popup,
|
||||
PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui,
|
||||
UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||
epaint, style::StyleModifier, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse,
|
||||
NumExt, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
|
||||
TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||
};
|
||||
|
||||
#[allow(unused_imports)] // Documentation
|
||||
|
|
@ -374,6 +374,7 @@ fn combo_box_dyn<'c, R>(
|
|||
|
||||
let inner = Popup::menu(&button_response)
|
||||
.id(popup_id)
|
||||
.style(StyleModifier::default())
|
||||
.width(button_response.rect.width())
|
||||
.close_behavior(close_behavior)
|
||||
.show(|ui| {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,528 @@
|
|||
use crate::style::StyleModifier;
|
||||
use crate::{
|
||||
Button, Color32, Context, Frame, Id, InnerResponse, Layout, Popup, PopupCloseBehavior,
|
||||
Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget, WidgetText,
|
||||
};
|
||||
use emath::{vec2, Align, RectAlign, Vec2};
|
||||
use epaint::Stroke;
|
||||
|
||||
/// Apply a menu style to the [`Style`].
|
||||
///
|
||||
/// Mainly removes the background stroke and the inactive background fill.
|
||||
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;
|
||||
}
|
||||
|
||||
/// Find the root [`UiStack`] of the menu.
|
||||
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")
|
||||
}
|
||||
|
||||
/// Is this Ui part of a menu?
|
||||
///
|
||||
/// Returns `false` if this is a menu bar.
|
||||
/// Should be used to determine if we should show a menu button or submenu button.
|
||||
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 {
|
||||
/// Is this a menu bar?
|
||||
bar: bool,
|
||||
|
||||
/// If the user clicks, should we close the menu?
|
||||
pub close_behavior: PopupCloseBehavior,
|
||||
|
||||
/// Override the menu style.
|
||||
///
|
||||
/// Default is [`menu_style`].
|
||||
pub style: StyleModifier,
|
||||
}
|
||||
|
||||
impl Default for MenuConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
close_behavior: PopupCloseBehavior::default(),
|
||||
bar: false,
|
||||
style: menu_style.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MenuConfig {
|
||||
/// The tag used to store the menu config in the [`UiStack`].
|
||||
pub const MENU_CONFIG_TAG: &'static str = "egui_menu_config";
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// If the user clicks, should we close the menu?
|
||||
#[inline]
|
||||
pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
|
||||
self.close_behavior = close_behavior;
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the menu style.
|
||||
///
|
||||
/// Default is [`menu_style`].
|
||||
#[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()
|
||||
}
|
||||
|
||||
/// Find the config for the current menu.
|
||||
///
|
||||
/// Returns the default config if no config is found.
|
||||
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";
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
/// Get the state via the menus root [`Ui`] id
|
||||
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 = data.get_temp_mut_or_insert_with(id.with(Self::ID), || Self {
|
||||
open_item: None,
|
||||
last_visible_pass: pass_nr,
|
||||
});
|
||||
// If the menu was closed for at least a frame, reset the open item
|
||||
if state.last_visible_pass + 1 < pass_nr {
|
||||
state.open_item = None;
|
||||
}
|
||||
state.last_visible_pass = pass_nr;
|
||||
f(state)
|
||||
})
|
||||
}
|
||||
|
||||
/// Is the menu with this id the deepest sub menu? (-> no child sub menu is open)
|
||||
pub fn is_deepest_sub_menu(ctx: &Context, id: Id) -> bool {
|
||||
Self::from_id(ctx, id, |state| state.open_item.is_none())
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizontal menu bar where you can add [`MenuButton`]s.
|
||||
|
||||
/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
|
||||
/// but can also be placed in a [`crate::Window`].
|
||||
/// In the latter case you may want to wrap it in [`Frame`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Bar {
|
||||
config: MenuConfig,
|
||||
style: StyleModifier,
|
||||
}
|
||||
|
||||
impl Default for Bar {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: MenuConfig::default(),
|
||||
style: menu_style.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Bar {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the style for buttons in the menu bar.
|
||||
///
|
||||
/// Doesn't affect the style of submenus, use [`MenuConfig::style`] for that.
|
||||
/// Default is [`menu_style`].
|
||||
#[inline]
|
||||
pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the config for submenus.
|
||||
///
|
||||
/// Note: The config will only be passed when using [`MenuButton`], not via [`Popup::menu`].
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.config = config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the menu bar.
|
||||
#[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;
|
||||
// TODO(lucasmerlin): It'd be nice if we had a ui.horizontal_builder or something
|
||||
// So we don't need the nested scope here
|
||||
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());
|
||||
|
||||
// Take full width and fixed height:
|
||||
let height = ui.spacing().interact_size.y;
|
||||
ui.set_min_size(vec2(ui.available_width(), height));
|
||||
|
||||
content(ui)
|
||||
},
|
||||
)
|
||||
.inner
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A thin wrapper around a [`Button`] that shows a [`Popup::menu`] when clicked.
|
||||
///
|
||||
/// The only thing this does is search for the current menu config (if set via [`Bar`]).
|
||||
/// If your menu button is not in a [`Bar`] it's fine to use [`Ui::button`] and [`Popup::menu`]
|
||||
/// directly.
|
||||
pub struct MenuButton<'a> {
|
||||
pub button: Button<'a>,
|
||||
pub config: Option<MenuConfig>,
|
||||
}
|
||||
|
||||
impl<'a> MenuButton<'a> {
|
||||
pub fn new(text: impl Into<WidgetText>) -> Self {
|
||||
Self::from_button(Button::new(text))
|
||||
}
|
||||
|
||||
/// Set the config for the menu.
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.config = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new menu button from a [`Button`].
|
||||
#[inline]
|
||||
pub fn from_button(button: Button<'a>) -> Self {
|
||||
Self {
|
||||
button,
|
||||
config: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the menu button.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// A submenu button that shows a [`SubMenu`] if a [`Button`] is hovered.
|
||||
pub struct SubMenuButton<'a> {
|
||||
pub button: Button<'a>,
|
||||
pub sub_menu: SubMenu,
|
||||
}
|
||||
|
||||
impl<'a> SubMenuButton<'a> {
|
||||
/// The default right arrow symbol: `"⏵"`
|
||||
pub const RIGHT_ARROW: &'static str = "⏵";
|
||||
|
||||
pub fn new(text: impl Into<WidgetText>) -> Self {
|
||||
Self::from_button(Button::new(text).right_text("⏵"))
|
||||
}
|
||||
|
||||
/// Create a new submenu button from a [`Button`].
|
||||
///
|
||||
/// Use [`Button::right_text`] and [`SubMenuButton::RIGHT_ARROW`] to add the default right
|
||||
/// arrow symbol.
|
||||
pub fn from_button(button: Button<'a>) -> Self {
|
||||
Self {
|
||||
button,
|
||||
sub_menu: SubMenu::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the config for the submenu.
|
||||
///
|
||||
/// The close behavior will not affect the current button, but the buttons in the submenu.
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.sub_menu.config = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the submenu button.
|
||||
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;
|
||||
// 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);
|
||||
|
||||
(response, popup_response)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SubMenu {
|
||||
config: Option<MenuConfig>,
|
||||
}
|
||||
|
||||
impl SubMenu {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the config for the submenu.
|
||||
///
|
||||
/// The close behavior will not affect the current button, but the buttons in the submenu.
|
||||
#[inline]
|
||||
pub fn config(mut self, config: MenuConfig) -> Self {
|
||||
self.config = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the id for the submenu from the widget/response id.
|
||||
pub fn id_from_widget_id(widget_id: Id) -> Id {
|
||||
widget_id.with("submenu")
|
||||
}
|
||||
|
||||
/// Show the 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)
|
||||
// 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 user 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;
|
||||
|
||||
// We expand the button rect so there is no empty space where no menu is shown
|
||||
// TODO(lucasmerlin): Instead, maybe make item_spacing.y 0.0?
|
||||
let button_rect = button_response
|
||||
.rect
|
||||
.expand2(ui.style().spacing.item_spacing / 2.0);
|
||||
|
||||
// In theory some other widget could cover the button and this check would still pass
|
||||
// But since we check if no other menu is open, nothing should be able to cover the button
|
||||
let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos));
|
||||
|
||||
// The clicked handler is there for accessibility (keyboard navigation)
|
||||
if (!is_any_open && is_hovered) || button_response.clicked() {
|
||||
set_open = Some(true);
|
||||
is_open = true;
|
||||
// Ensure that all other sub menus are closed when we open the menu
|
||||
MenuState::from_id(ui.ctx(), id, |state| {
|
||||
state.open_item = None;
|
||||
});
|
||||
}
|
||||
|
||||
let gap = frame.total_margin().sum().x / 2.0 + 2.0;
|
||||
|
||||
let mut response = button_response.clone();
|
||||
// Expand the button rect so that the button and the first item in the submenu are aligned
|
||||
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)
|
||||
// The close behavior is handled by the menu (see below)
|
||||
.close_behavior(PopupCloseBehavior::IgnoreClicks)
|
||||
.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)
|
||||
});
|
||||
|
||||
if let Some(popup_response) = &popup_response {
|
||||
// If no child sub menu is open means we must be the deepest child sub menu.
|
||||
let is_deepest_submenu = MenuState::is_deepest_sub_menu(ui.ctx(), id);
|
||||
|
||||
// If the user clicks and the cursor is not hovering over our menu rect, it's
|
||||
// safe to assume they clicked outside the menu, so we close everything.
|
||||
// If they were to hover some other parent submenu we wouldn't be open.
|
||||
// Only edge case is the user hovering this submenu's button, so we also check
|
||||
// if we clicked outside the parent menu (which we luckily have access to here).
|
||||
let clicked_outside = is_deepest_submenu
|
||||
&& popup_response.response.clicked_elsewhere()
|
||||
&& menu_root_response.clicked_elsewhere();
|
||||
|
||||
// We never automatically close when a submenu button is clicked, (so menus work
|
||||
// on touch devices)
|
||||
// Luckily we will always be the deepest submenu when a submenu button is clicked,
|
||||
// so the following check is enough.
|
||||
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 {
|
||||
// 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();
|
||||
}
|
||||
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();
|
||||
|
||||
// Close the parent ui to e.g. close the popup from where the submenu was opened
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ pub mod close_tag;
|
|||
pub mod collapsing_header;
|
||||
mod combo_box;
|
||||
pub mod frame;
|
||||
pub mod menu;
|
||||
pub mod modal;
|
||||
pub mod old_popup;
|
||||
pub mod panel;
|
||||
|
|
|
|||
|
|
@ -196,7 +196,8 @@ pub fn popup_above_or_below_widget<R>(
|
|||
) -> Option<R> {
|
||||
let response = Popup::from_response(widget_response)
|
||||
.layout(Layout::top_down_justified(Align::LEFT))
|
||||
.open_memory(None, close_behavior)
|
||||
.open_memory(None)
|
||||
.close_behavior(close_behavior)
|
||||
.id(popup_id)
|
||||
.align(match above_or_below {
|
||||
AboveOrBelow::Above => RectAlign::TOP_START,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use crate::containers::menu::{menu_style, MenuConfig, MenuState};
|
||||
use crate::style::StyleModifier;
|
||||
use crate::{
|
||||
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
|
||||
Sense, Ui, UiKind,
|
||||
Sense, Ui, UiKind, UiStackInfo,
|
||||
};
|
||||
use emath::{vec2, Align, Pos2, Rect, RectAlign, Vec2};
|
||||
use std::iter::once;
|
||||
|
|
@ -44,7 +46,8 @@ impl From<Pos2> for PopupAnchor {
|
|||
|
||||
impl From<&Response> for PopupAnchor {
|
||||
fn from(response: &Response) -> Self {
|
||||
let mut widget_rect = response.rect;
|
||||
// We use interact_rect so we don't show the popup relative to some clipped point
|
||||
let mut widget_rect = response.interact_rect;
|
||||
if let Some(to_global) = response.ctx.layer_transform_to_global(response.layer_id) {
|
||||
widget_rect = to_global * widget_rect;
|
||||
}
|
||||
|
|
@ -69,11 +72,12 @@ impl PopupAnchor {
|
|||
}
|
||||
|
||||
/// Determines popup's close behavior
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
|
||||
pub enum PopupCloseBehavior {
|
||||
/// Popup will be closed on click anywhere, inside or outside the popup.
|
||||
///
|
||||
/// It is used in [`crate::ComboBox`].
|
||||
/// It is used in [`crate::ComboBox`] and in [`crate::containers::menu`]s.
|
||||
#[default]
|
||||
CloseOnClick,
|
||||
|
||||
/// Popup will be closed if the click happened somewhere else
|
||||
|
|
@ -109,13 +113,10 @@ enum OpenKind<'a> {
|
|||
Closed,
|
||||
|
||||
/// Open if the bool is true
|
||||
Bool(&'a mut bool, PopupCloseBehavior),
|
||||
Bool(&'a mut bool),
|
||||
|
||||
/// Store the open state via [`crate::Memory`]
|
||||
Memory {
|
||||
set: Option<SetOpenCommand>,
|
||||
close_behavior: PopupCloseBehavior,
|
||||
},
|
||||
Memory { set: Option<SetOpenCommand> },
|
||||
}
|
||||
|
||||
impl OpenKind<'_> {
|
||||
|
|
@ -124,7 +125,7 @@ impl OpenKind<'_> {
|
|||
match self {
|
||||
OpenKind::Open => true,
|
||||
OpenKind::Closed => false,
|
||||
OpenKind::Bool(open, _) => **open,
|
||||
OpenKind::Bool(open) => **open,
|
||||
OpenKind::Memory { .. } => ctx.memory(|mem| mem.is_popup_open(id)),
|
||||
}
|
||||
}
|
||||
|
|
@ -138,6 +139,26 @@ pub enum PopupKind {
|
|||
Menu,
|
||||
}
|
||||
|
||||
impl PopupKind {
|
||||
/// Returns the order to be used with this kind.
|
||||
pub fn order(self) -> Order {
|
||||
match self {
|
||||
Self::Tooltip => Order::Tooltip,
|
||||
Self::Menu | Self::Popup => Order::Foreground,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PopupKind> for UiKind {
|
||||
fn from(kind: PopupKind) -> Self {
|
||||
match kind {
|
||||
PopupKind::Popup => Self::Popup,
|
||||
PopupKind::Tooltip => Self::Tooltip,
|
||||
PopupKind::Menu => Self::Menu,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Popup<'a> {
|
||||
id: Id,
|
||||
ctx: Context,
|
||||
|
|
@ -146,6 +167,8 @@ pub struct Popup<'a> {
|
|||
alternative_aligns: Option<&'a [RectAlign]>,
|
||||
layer_id: LayerId,
|
||||
open_kind: OpenKind<'a>,
|
||||
close_behavior: PopupCloseBehavior,
|
||||
info: Option<UiStackInfo>,
|
||||
kind: PopupKind,
|
||||
|
||||
/// Gap between the anchor and the popup
|
||||
|
|
@ -159,6 +182,7 @@ pub struct Popup<'a> {
|
|||
sense: Sense,
|
||||
layout: Layout,
|
||||
frame: Option<Frame>,
|
||||
style: StyleModifier,
|
||||
}
|
||||
|
||||
impl<'a> Popup<'a> {
|
||||
|
|
@ -169,6 +193,8 @@ impl<'a> Popup<'a> {
|
|||
ctx,
|
||||
anchor: anchor.into(),
|
||||
open_kind: OpenKind::Open,
|
||||
close_behavior: PopupCloseBehavior::default(),
|
||||
info: None,
|
||||
kind: PopupKind::Popup,
|
||||
layer_id,
|
||||
rect_align: RectAlign::BOTTOM_START,
|
||||
|
|
@ -179,6 +205,7 @@ impl<'a> Popup<'a> {
|
|||
sense: Sense::click(),
|
||||
layout: Layout::default(),
|
||||
frame: None,
|
||||
style: StyleModifier::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +216,13 @@ impl<'a> Popup<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the [`UiStackInfo`] of the popup's [`Ui`].
|
||||
#[inline]
|
||||
pub fn info(mut self, info: UiStackInfo) -> Self {
|
||||
self.info = Some(info);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`RectAlign`] of the popup relative to the [`PopupAnchor`].
|
||||
/// This is the default position, and will be used if it fits.
|
||||
/// See [`Self::align_alternatives`] for more on this.
|
||||
|
|
@ -226,29 +260,23 @@ impl<'a> Popup<'a> {
|
|||
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
|
||||
pub fn menu(response: &Response) -> Self {
|
||||
Self::from_response(response)
|
||||
.open_memory(
|
||||
if response.clicked() {
|
||||
Some(SetOpenCommand::Toggle)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
PopupCloseBehavior::CloseOnClick,
|
||||
)
|
||||
.open_memory(response.clicked().then_some(SetOpenCommand::Toggle))
|
||||
.kind(PopupKind::Menu)
|
||||
.layout(Layout::top_down_justified(Align::Min))
|
||||
.style(menu_style)
|
||||
.gap(0.0)
|
||||
}
|
||||
|
||||
/// Show a context menu when the widget was secondary clicked.
|
||||
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
|
||||
/// In contrast to [`Self::menu`], this will open at the pointer position.
|
||||
pub fn context_menu(response: &Response) -> Self {
|
||||
Self::from_response(response)
|
||||
Self::menu(response)
|
||||
.open_memory(
|
||||
response
|
||||
.secondary_clicked()
|
||||
.then_some(SetOpenCommand::Bool(true)),
|
||||
PopupCloseBehavior::CloseOnClick,
|
||||
)
|
||||
.layout(Layout::top_down_justified(Align::Min))
|
||||
.at_pointer_fixed()
|
||||
}
|
||||
|
||||
|
|
@ -266,22 +294,17 @@ impl<'a> Popup<'a> {
|
|||
/// Store the open state via [`crate::Memory`].
|
||||
/// You can set the state via the first [`SetOpenCommand`] param.
|
||||
#[inline]
|
||||
pub fn open_memory(
|
||||
mut self,
|
||||
set_state: impl Into<Option<SetOpenCommand>>,
|
||||
close_behavior: PopupCloseBehavior,
|
||||
) -> Self {
|
||||
pub fn open_memory(mut self, set_state: impl Into<Option<SetOpenCommand>>) -> Self {
|
||||
self.open_kind = OpenKind::Memory {
|
||||
set: set_state.into(),
|
||||
close_behavior,
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
/// Store the open state via a mutable bool.
|
||||
#[inline]
|
||||
pub fn open_bool(mut self, open: &'a mut bool, close_behavior: PopupCloseBehavior) -> Self {
|
||||
self.open_kind = OpenKind::Bool(open, close_behavior);
|
||||
pub fn open_bool(mut self, open: &'a mut bool) -> Self {
|
||||
self.open_kind = OpenKind::Bool(open);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -290,16 +313,7 @@ impl<'a> Popup<'a> {
|
|||
/// This will do nothing if [`Popup::open`] was called.
|
||||
#[inline]
|
||||
pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
|
||||
match &mut self.open_kind {
|
||||
OpenKind::Memory {
|
||||
close_behavior: behavior,
|
||||
..
|
||||
}
|
||||
| OpenKind::Bool(_, behavior) => {
|
||||
*behavior = close_behavior;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.close_behavior = close_behavior;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -339,6 +353,13 @@ impl<'a> Popup<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the frame of the popup.
|
||||
#[inline]
|
||||
pub fn frame(mut self, frame: Frame) -> Self {
|
||||
self.frame = Some(frame);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sense of the popup.
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
|
|
@ -367,6 +388,17 @@ impl<'a> Popup<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the style for the popup contents.
|
||||
///
|
||||
/// Default:
|
||||
/// - is [`menu_style`] for [`Self::menu`] and [`Self::context_menu`]
|
||||
/// - is [`None`] otherwise
|
||||
#[inline]
|
||||
pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the [`Context`]
|
||||
pub fn ctx(&self) -> &Context {
|
||||
&self.ctx
|
||||
|
|
@ -408,11 +440,12 @@ impl<'a> Popup<'a> {
|
|||
match &self.open_kind {
|
||||
OpenKind::Open => true,
|
||||
OpenKind::Closed => false,
|
||||
OpenKind::Bool(open, _) => **open,
|
||||
OpenKind::Bool(open) => **open,
|
||||
OpenKind::Memory { .. } => self.ctx.memory(|mem| mem.is_popup_open(self.id)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the expected size of the popup.
|
||||
pub fn get_expected_size(&self) -> Option<Vec2> {
|
||||
AreaState::load(&self.ctx, self.id).and_then(|area| area.size)
|
||||
}
|
||||
|
|
@ -459,7 +492,9 @@ impl<'a> Popup<'a> {
|
|||
ctx,
|
||||
anchor,
|
||||
open_kind,
|
||||
close_behavior,
|
||||
kind,
|
||||
info,
|
||||
layer_id,
|
||||
rect_align: _,
|
||||
alternative_aligns: _,
|
||||
|
|
@ -469,6 +504,7 @@ impl<'a> Popup<'a> {
|
|||
sense,
|
||||
layout,
|
||||
frame,
|
||||
style,
|
||||
} = self;
|
||||
|
||||
let hover_pos = ctx.pointer_hover_pos();
|
||||
|
|
@ -497,13 +533,7 @@ impl<'a> Popup<'a> {
|
|||
return None;
|
||||
}
|
||||
|
||||
let (ui_kind, order) = match kind {
|
||||
PopupKind::Popup => (UiKind::Popup, Order::Foreground),
|
||||
PopupKind::Tooltip => (UiKind::Tooltip, Order::Tooltip),
|
||||
PopupKind::Menu => (UiKind::Menu, Order::Foreground),
|
||||
};
|
||||
|
||||
if kind == PopupKind::Popup {
|
||||
if kind != PopupKind::Tooltip {
|
||||
ctx.pass_state_mut(|fs| {
|
||||
fs.layers
|
||||
.entry(layer_id)
|
||||
|
|
@ -518,12 +548,17 @@ impl<'a> Popup<'a> {
|
|||
let (pivot, anchor) = best_align.pivot_pos(&anchor_rect, gap);
|
||||
|
||||
let mut area = Area::new(id)
|
||||
.order(order)
|
||||
.kind(ui_kind)
|
||||
.order(kind.order())
|
||||
.pivot(pivot)
|
||||
.fixed_pos(anchor)
|
||||
.sense(sense)
|
||||
.layout(layout);
|
||||
.layout(layout)
|
||||
.info(info.unwrap_or_else(|| {
|
||||
UiStackInfo::new(kind.into()).with_tag_value(
|
||||
MenuConfig::MENU_CONFIG_TAG,
|
||||
MenuConfig::new().close_behavior(close_behavior),
|
||||
)
|
||||
}));
|
||||
|
||||
if let Some(width) = width {
|
||||
area = area.default_width(width);
|
||||
|
|
@ -531,31 +566,39 @@ 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 mut response = area.show(&ctx, |ui| {
|
||||
style.apply(ui.style_mut());
|
||||
frame.show(ui, content).inner
|
||||
});
|
||||
|
||||
let should_close = |close_behavior| {
|
||||
let should_close = match close_behavior {
|
||||
PopupCloseBehavior::CloseOnClick => widget_clicked_elsewhere,
|
||||
PopupCloseBehavior::CloseOnClickOutside => {
|
||||
widget_clicked_elsewhere && response.response.clicked_elsewhere()
|
||||
}
|
||||
PopupCloseBehavior::IgnoreClicks => false,
|
||||
};
|
||||
|
||||
should_close
|
||||
|| ctx.input(|i| i.key_pressed(Key::Escape))
|
||||
|| response.response.should_close()
|
||||
let closed_by_click = match close_behavior {
|
||||
PopupCloseBehavior::CloseOnClick => widget_clicked_elsewhere,
|
||||
PopupCloseBehavior::CloseOnClickOutside => {
|
||||
widget_clicked_elsewhere && response.response.clicked_elsewhere()
|
||||
}
|
||||
PopupCloseBehavior::IgnoreClicks => false,
|
||||
};
|
||||
|
||||
// If a submenu is open, the CloseBehavior is handled there
|
||||
let is_any_submenu_open = !MenuState::is_deepest_sub_menu(&response.response.ctx, id);
|
||||
|
||||
let should_close = (!is_any_submenu_open && closed_by_click)
|
||||
|| ctx.input(|i| i.key_pressed(Key::Escape))
|
||||
|| response.response.should_close();
|
||||
|
||||
if should_close {
|
||||
response.response.set_close();
|
||||
}
|
||||
|
||||
match open_kind {
|
||||
OpenKind::Open | OpenKind::Closed => {}
|
||||
OpenKind::Bool(open, close_behavior) => {
|
||||
if should_close(close_behavior) {
|
||||
OpenKind::Bool(open) => {
|
||||
if should_close {
|
||||
*open = false;
|
||||
}
|
||||
}
|
||||
OpenKind::Memory { close_behavior, .. } => {
|
||||
if should_close(close_behavior) {
|
||||
OpenKind::Memory { .. } => {
|
||||
if should_close {
|
||||
ctx.memory_mut(|mem| mem.close_popup());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ use crate::{
|
|||
load,
|
||||
load::{Bytes, Loaders, SizedTexture},
|
||||
memory::{Options, Theme},
|
||||
menu,
|
||||
os::OperatingSystem,
|
||||
output::FullOutput,
|
||||
pass_state::PassState,
|
||||
|
|
@ -1197,15 +1196,27 @@ impl Context {
|
|||
/// This is because widget interaction happens at the start of the pass, using the widget rects from the previous pass.
|
||||
///
|
||||
/// If the widget was not visible the previous pass (or this pass), this will return `None`.
|
||||
///
|
||||
/// If you try to read a [`Ui`]'s response, while still inside, this will return the [`Rect`] from the previous frame.
|
||||
pub fn read_response(&self, id: Id) -> Option<Response> {
|
||||
self.write(|ctx| {
|
||||
let viewport = ctx.viewport();
|
||||
viewport
|
||||
let widget_rect = viewport
|
||||
.this_pass
|
||||
.widgets
|
||||
.get(id)
|
||||
.or_else(|| viewport.prev_pass.widgets.get(id))
|
||||
.copied()
|
||||
.copied();
|
||||
widget_rect.map(|mut rect| {
|
||||
// If the Rect is invalid the Ui hasn't registered its final Rect yet.
|
||||
// We return the Rect from last frame instead.
|
||||
if !(rect.rect.is_positive() && rect.rect.is_finite()) {
|
||||
if let Some(prev_rect) = viewport.prev_pass.widgets.get(id) {
|
||||
rect.rect = prev_rect.rect;
|
||||
}
|
||||
}
|
||||
rect
|
||||
})
|
||||
})
|
||||
.map(|widget_rect| self.get_response(widget_rect))
|
||||
}
|
||||
|
|
@ -2626,12 +2637,27 @@ impl Context {
|
|||
}
|
||||
|
||||
/// Is an egui context menu open?
|
||||
///
|
||||
/// This only works with the old, deprecated [`crate::menu`] API.
|
||||
#[allow(deprecated)]
|
||||
#[deprecated = "Use `is_popup_open` instead"]
|
||||
pub fn is_context_menu_open(&self) -> bool {
|
||||
self.data(|d| {
|
||||
d.get_temp::<crate::menu::BarState>(menu::CONTEXT_MENU_ID_STR.into())
|
||||
d.get_temp::<crate::menu::BarState>(crate::menu::CONTEXT_MENU_ID_STR.into())
|
||||
.is_some_and(|state| state.has_root())
|
||||
})
|
||||
}
|
||||
|
||||
/// Is a popup or (context) menu open?
|
||||
///
|
||||
/// Will return false for [`crate::Tooltip`]s (which are technically popups as well).
|
||||
pub fn is_popup_open(&self) -> bool {
|
||||
self.pass_state_mut(|fs| {
|
||||
fs.layers
|
||||
.values()
|
||||
.any(|layer| !layer.open_popups.is_empty())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Ergonomic methods to forward some calls often used in 'if let' without holding the borrow
|
||||
|
|
@ -2676,7 +2702,8 @@ impl Context {
|
|||
///
|
||||
/// Can be used to implement pan and zoom (see relevant demo).
|
||||
///
|
||||
/// For a temporary transform, use [`Self::transform_layer_shapes`] instead.
|
||||
/// For a temporary transform, use [`Self::transform_layer_shapes`] or
|
||||
/// [`Ui::with_visual_transform`].
|
||||
pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) {
|
||||
self.memory_mut(|m| {
|
||||
if transform == TSTransform::IDENTITY {
|
||||
|
|
@ -3166,13 +3193,14 @@ impl Context {
|
|||
}
|
||||
});
|
||||
|
||||
#[allow(deprecated)]
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!(
|
||||
"{} menu bars",
|
||||
self.data(|d| d.count::<menu::BarState>())
|
||||
self.data(|d| d.count::<crate::menu::BarState>())
|
||||
));
|
||||
if ui.button("Reset").clicked() {
|
||||
self.data_mut(|d| d.remove_by_type::<menu::BarState>());
|
||||
self.data_mut(|d| d.remove_by_type::<crate::menu::BarState>());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@ pub fn zoom_menu_buttons(ui: &mut Ui) {
|
|||
.clicked()
|
||||
{
|
||||
zoom_in(ui.ctx());
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if ui
|
||||
|
|
@ -99,7 +98,6 @@ pub fn zoom_menu_buttons(ui: &mut Ui) {
|
|||
.clicked()
|
||||
{
|
||||
zoom_out(ui.ctx());
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if ui
|
||||
|
|
@ -110,6 +108,5 @@ pub fn zoom_menu_buttons(ui: &mut Ui) {
|
|||
.clicked()
|
||||
{
|
||||
ui.ctx().set_zoom_factor(1.0);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1333,6 +1333,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 {
|
||||
|
|
|
|||
|
|
@ -422,6 +422,7 @@ pub mod layers;
|
|||
mod layout;
|
||||
pub mod load;
|
||||
mod memory;
|
||||
#[deprecated = "Use `egui::containers::menu` instead"]
|
||||
pub mod menu;
|
||||
pub mod os;
|
||||
mod painter;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(deprecated)]
|
||||
//! Menu bar functionality (very basic so far).
|
||||
//!
|
||||
//! Usage:
|
||||
|
|
@ -146,7 +147,7 @@ pub fn menu_image_button<R>(
|
|||
/// Opens on hover.
|
||||
///
|
||||
/// Returns `None` if the menu is not open.
|
||||
pub(crate) fn submenu_button<R>(
|
||||
pub fn submenu_button<R>(
|
||||
ui: &mut Ui,
|
||||
parent_state: Arc<RwLock<MenuState>>,
|
||||
title: impl Into<WidgetText>,
|
||||
|
|
@ -267,7 +268,7 @@ fn stationary_menu_button_impl<'c, R>(
|
|||
pub(crate) const CONTEXT_MENU_ID_STR: &str = "__egui::context_menu";
|
||||
|
||||
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
||||
pub(crate) fn context_menu(
|
||||
pub fn context_menu(
|
||||
response: &Response,
|
||||
add_contents: impl FnOnce(&mut Ui),
|
||||
) -> Option<InnerResponse<()>> {
|
||||
|
|
@ -282,7 +283,7 @@ pub(crate) fn context_menu(
|
|||
}
|
||||
|
||||
/// Returns `true` if the context menu is opened for this widget.
|
||||
pub(crate) fn context_menu_opened(response: &Response) -> bool {
|
||||
pub fn context_menu_opened(response: &Response) -> bool {
|
||||
let menu_id = Id::new(CONTEXT_MENU_ID_STR);
|
||||
let bar_state = BarState::load(&response.ctx, menu_id);
|
||||
bar_state.is_menu_open(response.id)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ use std::{any::Any, sync::Arc};
|
|||
|
||||
use crate::{
|
||||
emath::{Align, Pos2, Rect, Vec2},
|
||||
menu, pass_state, Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense,
|
||||
Tooltip, Ui, WidgetRect, WidgetText,
|
||||
pass_state, Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip,
|
||||
Ui, WidgetRect, WidgetText,
|
||||
};
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
|
|
@ -935,14 +935,14 @@ impl Response {
|
|||
///
|
||||
/// See also: [`Ui::menu_button`] and [`Ui::close`].
|
||||
pub fn context_menu(&self, add_contents: impl FnOnce(&mut Ui)) -> Option<InnerResponse<()>> {
|
||||
menu::context_menu(self, add_contents)
|
||||
Popup::context_menu(self).show(add_contents)
|
||||
}
|
||||
|
||||
/// Returns whether a context menu is currently open for this widget.
|
||||
///
|
||||
/// See [`Self::context_menu`].
|
||||
pub fn context_menu_opened(&self) -> bool {
|
||||
menu::context_menu_opened(self)
|
||||
Popup::context_menu(self).is_open()
|
||||
}
|
||||
|
||||
/// Draw a debug rectangle over the response displaying the response's id and whether it is
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
|
||||
#![allow(clippy::if_same_then_else)]
|
||||
|
||||
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
||||
|
||||
use emath::Align;
|
||||
use epaint::{text::FontTweak, CornerRadius, Shadow, Stroke};
|
||||
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
ecolor::Color32,
|
||||
|
|
@ -174,6 +173,49 @@ impl From<TextStyle> for FontSelection {
|
|||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Utility to modify a [`Style`] in some way.
|
||||
/// Constructed via [`StyleModifier::from`] from a `Fn(&mut Style)` or a [`Style`].
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StyleModifier(Option<Arc<dyn Fn(&mut Style) + Send + Sync>>);
|
||||
|
||||
impl std::fmt::Debug for StyleModifier {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("StyleModifier")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for StyleModifier
|
||||
where
|
||||
T: Fn(&mut Style) + Send + Sync + 'static,
|
||||
{
|
||||
fn from(f: T) -> Self {
|
||||
Self(Some(Arc::new(f)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Style> for StyleModifier {
|
||||
fn from(style: Style) -> Self {
|
||||
Self(Some(Arc::new(move |s| *s = style.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleModifier {
|
||||
/// Create a new [`StyleModifier`] from a function.
|
||||
pub fn new(f: impl Fn(&mut Style) + Send + Sync + 'static) -> Self {
|
||||
Self::from(f)
|
||||
}
|
||||
|
||||
/// Apply the modification to the given [`Style`].
|
||||
/// Usually used with [`Ui::style_mut`].
|
||||
pub fn apply(&self, style: &mut Style) {
|
||||
if let Some(f) = &self.0 {
|
||||
f(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Specifies the look and feel of egui.
|
||||
///
|
||||
/// You can change the visuals of a [`Ui`] with [`Ui::style_mut`]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use epaint::mutex::RwLock;
|
|||
use std::{any::Any, hash::Hash, sync::Arc};
|
||||
|
||||
use crate::close_tag::ClosableTag;
|
||||
use crate::containers::menu;
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::Stroke;
|
||||
use crate::{
|
||||
|
|
@ -15,8 +16,6 @@ use crate::{
|
|||
epaint::text::Fonts,
|
||||
grid,
|
||||
layout::{Direction, Layout},
|
||||
menu,
|
||||
menu::MenuState,
|
||||
pass_state,
|
||||
placer::Placer,
|
||||
pos2, style,
|
||||
|
|
@ -99,7 +98,8 @@ pub struct Ui {
|
|||
sizing_pass: bool,
|
||||
|
||||
/// Indicates whether this Ui belongs to a Menu.
|
||||
menu_state: Option<Arc<RwLock<MenuState>>>,
|
||||
#[allow(deprecated)]
|
||||
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
|
||||
|
||||
/// The [`UiStack`] for this [`Ui`].
|
||||
stack: Arc<UiStack>,
|
||||
|
|
@ -1187,6 +1187,14 @@ impl Ui {
|
|||
/// [`crate::Area`] e.g. will return true from it's [`Response::should_close`] method.
|
||||
///
|
||||
/// If you want to close a specific kind of container, use [`Ui::close_kind`] instead.
|
||||
///
|
||||
/// Also note that this won't bubble up across [`crate::Area`]s. If needed, you can check
|
||||
/// `response.should_close()` and close the parent manually. ([`menu`] does this for example).
|
||||
///
|
||||
/// See also:
|
||||
/// - [`Ui::close_kind`]
|
||||
/// - [`Ui::should_close`]
|
||||
/// - [`Ui::will_parent_close`]
|
||||
pub fn close(&self) {
|
||||
let tag = self.stack.iter().find_map(|stack| {
|
||||
stack
|
||||
|
|
@ -1207,6 +1215,11 @@ impl Ui {
|
|||
/// This is useful if you want to e.g. close a [`crate::Window`]. Since it contains a
|
||||
/// `Collapsible`, [`Ui::close`] would close the `Collapsible` instead.
|
||||
/// You can close the [`crate::Window`] by calling `ui.close_kind(UiKind::Window)`.
|
||||
///
|
||||
/// See also:
|
||||
/// - [`Ui::close`]
|
||||
/// - [`Ui::should_close`]
|
||||
/// - [`Ui::will_parent_close`]
|
||||
pub fn close_kind(&self, ui_kind: UiKind) {
|
||||
let tag = self
|
||||
.stack
|
||||
|
|
@ -1230,6 +1243,12 @@ impl Ui {
|
|||
/// Only works if the [`Ui`] was created with [`UiBuilder::closable`].
|
||||
///
|
||||
/// You can also check via this [`Ui`]'s [`Response::should_close`].
|
||||
///
|
||||
/// See also:
|
||||
/// - [`Ui::will_parent_close`]
|
||||
/// - [`Ui::close`]
|
||||
/// - [`Ui::close_kind`]
|
||||
/// - [`Response::should_close`]
|
||||
pub fn should_close(&self) -> bool {
|
||||
self.stack
|
||||
.info
|
||||
|
|
@ -1237,6 +1256,22 @@ impl Ui {
|
|||
.get_downcast(ClosableTag::NAME)
|
||||
.is_some_and(|tag: &ClosableTag| tag.should_close())
|
||||
}
|
||||
|
||||
/// Will this [`Ui`] or any of its parents close this frame?
|
||||
///
|
||||
/// See also
|
||||
/// - [`Ui::should_close`]
|
||||
/// - [`Ui::close`]
|
||||
/// - [`Ui::close_kind`]
|
||||
pub 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// # Allocating space: where do I put my widgets?
|
||||
|
|
@ -2977,7 +3012,11 @@ impl Ui {
|
|||
self.close_kind(UiKind::Menu);
|
||||
}
|
||||
|
||||
pub(crate) fn set_menu_state(&mut self, menu_state: Option<Arc<RwLock<MenuState>>>) {
|
||||
#[allow(deprecated)]
|
||||
pub(crate) fn set_menu_state(
|
||||
&mut self,
|
||||
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
|
||||
) {
|
||||
self.menu_state = menu_state;
|
||||
}
|
||||
|
||||
|
|
@ -3004,11 +3043,12 @@ impl Ui {
|
|||
title: impl Into<WidgetText>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
if let Some(menu_state) = self.menu_state.clone() {
|
||||
menu::submenu_button(self, menu_state, title, add_contents)
|
||||
let (response, inner) = if menu::is_in_menu(self) {
|
||||
menu::SubMenuButton::new(title).ui(self, add_contents)
|
||||
} else {
|
||||
menu::menu_button(self, title, add_contents)
|
||||
}
|
||||
menu::MenuButton::new(title).ui(self, add_contents)
|
||||
};
|
||||
InnerResponse::new(inner.map(|i| i.inner), response)
|
||||
}
|
||||
|
||||
/// Create a menu button with an image that when clicked will show the given menu.
|
||||
|
|
@ -3037,11 +3077,15 @@ impl Ui {
|
|||
image: impl Into<Image<'a>>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
if let Some(menu_state) = self.menu_state.clone() {
|
||||
menu::submenu_button(self, menu_state, String::new(), add_contents)
|
||||
let (response, inner) = if menu::is_in_menu(self) {
|
||||
menu::SubMenuButton::from_button(
|
||||
Button::image(image).right_text(menu::SubMenuButton::RIGHT_ARROW),
|
||||
)
|
||||
.ui(self, add_contents)
|
||||
} else {
|
||||
menu::menu_custom_button(self, Button::image(image), add_contents)
|
||||
}
|
||||
menu::MenuButton::from_button(Button::image(image)).ui(self, add_contents)
|
||||
};
|
||||
InnerResponse::new(inner.map(|i| i.inner), response)
|
||||
}
|
||||
|
||||
/// Create a menu button with an image and a text that when clicked will show the given menu.
|
||||
|
|
@ -3071,11 +3115,16 @@ impl Ui {
|
|||
title: impl Into<WidgetText>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
if let Some(menu_state) = self.menu_state.clone() {
|
||||
menu::submenu_button(self, menu_state, title, add_contents)
|
||||
let (response, inner) = if menu::is_in_menu(self) {
|
||||
menu::SubMenuButton::from_button(
|
||||
Button::image_and_text(image, title).right_text(menu::SubMenuButton::RIGHT_ARROW),
|
||||
)
|
||||
.ui(self, add_contents)
|
||||
} else {
|
||||
menu::menu_custom_button(self, Button::image_and_text(image, title), add_contents)
|
||||
}
|
||||
menu::MenuButton::from_button(Button::image_and_text(image, title))
|
||||
.ui(self, add_contents)
|
||||
};
|
||||
InnerResponse::new(inner.map(|i| i.inner), response)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use crate::{
|
|||
pub struct Button<'a> {
|
||||
image: Option<Image<'a>>,
|
||||
text: Option<WidgetText>,
|
||||
shortcut_text: WidgetText,
|
||||
right_text: WidgetText,
|
||||
wrap_mode: Option<TextWrapMode>,
|
||||
|
||||
/// None means default for interact
|
||||
|
|
@ -61,7 +61,7 @@ impl<'a> Button<'a> {
|
|||
Self {
|
||||
text,
|
||||
image,
|
||||
shortcut_text: Default::default(),
|
||||
right_text: Default::default(),
|
||||
wrap_mode: None,
|
||||
fill: None,
|
||||
stroke: None,
|
||||
|
|
@ -181,9 +181,18 @@ impl<'a> Button<'a> {
|
|||
/// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
|
||||
///
|
||||
/// The text can be created with [`crate::Context::format_shortcut`].
|
||||
///
|
||||
/// See also [`Self::right_text`].
|
||||
#[inline]
|
||||
pub fn shortcut_text(mut self, shortcut_text: impl Into<WidgetText>) -> Self {
|
||||
self.shortcut_text = shortcut_text.into();
|
||||
self.right_text = shortcut_text.into().weak();
|
||||
self
|
||||
}
|
||||
|
||||
/// Show some text on the right side of the button.
|
||||
#[inline]
|
||||
pub fn right_text(mut self, right_text: impl Into<WidgetText>) -> Self {
|
||||
self.right_text = right_text.into();
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +209,7 @@ impl Widget for Button<'_> {
|
|||
let Button {
|
||||
text,
|
||||
image,
|
||||
shortcut_text,
|
||||
right_text,
|
||||
wrap_mode,
|
||||
fill,
|
||||
stroke,
|
||||
|
|
@ -239,16 +248,16 @@ impl Widget for Button<'_> {
|
|||
Vec2::ZERO
|
||||
};
|
||||
|
||||
let gap_before_shortcut_text = ui.spacing().item_spacing.x;
|
||||
let gap_before_right_text = ui.spacing().item_spacing.x;
|
||||
|
||||
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
|
||||
if image.is_some() {
|
||||
text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
|
||||
}
|
||||
|
||||
// Note: we don't wrap the shortcut text
|
||||
let shortcut_galley = (!shortcut_text.is_empty()).then(|| {
|
||||
shortcut_text.into_galley(
|
||||
// Note: we don't wrap the right text
|
||||
let right_galley = (!right_text.is_empty()).then(|| {
|
||||
right_text.into_galley(
|
||||
ui,
|
||||
Some(TextWrapMode::Extend),
|
||||
f32::INFINITY,
|
||||
|
|
@ -256,9 +265,9 @@ impl Widget for Button<'_> {
|
|||
)
|
||||
});
|
||||
|
||||
if let Some(shortcut_galley) = &shortcut_galley {
|
||||
// Leave space for the shortcut text:
|
||||
text_wrap_width -= gap_before_shortcut_text + shortcut_galley.size().x;
|
||||
if let Some(right_galley) = &right_galley {
|
||||
// Leave space for the right text:
|
||||
text_wrap_width -= gap_before_right_text + right_galley.size().x;
|
||||
}
|
||||
|
||||
let galley =
|
||||
|
|
@ -276,9 +285,9 @@ impl Widget for Button<'_> {
|
|||
desired_size.x += galley.size().x;
|
||||
desired_size.y = desired_size.y.max(galley.size().y);
|
||||
}
|
||||
if let Some(shortcut_galley) = &shortcut_galley {
|
||||
desired_size.x += gap_before_shortcut_text + shortcut_galley.size().x;
|
||||
desired_size.y = desired_size.y.max(shortcut_galley.size().y);
|
||||
if let Some(right_galley) = &right_galley {
|
||||
desired_size.x += gap_before_right_text + right_galley.size().x;
|
||||
desired_size.y = desired_size.y.max(right_galley.size().y);
|
||||
}
|
||||
desired_size += 2.0 * button_padding;
|
||||
if !small {
|
||||
|
|
@ -335,7 +344,7 @@ impl Widget for Button<'_> {
|
|||
.layout()
|
||||
.align_size_within_rect(image_size, rect.shrink2(button_padding))
|
||||
.min;
|
||||
if galley.is_some() || shortcut_galley.is_some() {
|
||||
if galley.is_some() || right_galley.is_some() {
|
||||
image_pos.x = cursor_x;
|
||||
}
|
||||
let image_rect = Rect::from_min_size(image_pos, image_size);
|
||||
|
|
@ -369,27 +378,25 @@ impl Widget for Button<'_> {
|
|||
.layout()
|
||||
.align_size_within_rect(galley.size(), rect.shrink2(button_padding))
|
||||
.min;
|
||||
if image.is_some() || shortcut_galley.is_some() {
|
||||
if image.is_some() || right_galley.is_some() {
|
||||
text_pos.x = cursor_x;
|
||||
}
|
||||
ui.painter().galley(text_pos, galley, visuals.text_color());
|
||||
}
|
||||
|
||||
if let Some(shortcut_galley) = shortcut_galley {
|
||||
if let Some(right_galley) = right_galley {
|
||||
// Always align to the right
|
||||
let layout = if ui.layout().is_horizontal() {
|
||||
ui.layout().with_main_align(Align::Max)
|
||||
} else {
|
||||
ui.layout().with_cross_align(Align::Max)
|
||||
};
|
||||
let shortcut_text_pos = layout
|
||||
.align_size_within_rect(shortcut_galley.size(), rect.shrink2(button_padding))
|
||||
let right_text_pos = layout
|
||||
.align_size_within_rect(right_galley.size(), rect.shrink2(button_padding))
|
||||
.min;
|
||||
ui.painter().galley(
|
||||
shortcut_text_pos,
|
||||
shortcut_galley,
|
||||
ui.visuals().weak_text_color(),
|
||||
);
|
||||
|
||||
ui.painter()
|
||||
.galley(right_text_pos, right_galley, visuals.text_color());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
use egui::{ComboBox, Popup};
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ContextMenus {}
|
||||
|
||||
impl crate::Demo for ContextMenus {
|
||||
fn name(&self) -> &'static str {
|
||||
"☰ Context Menus"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use crate::View;
|
||||
egui::Window::new(self.name())
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.open(open)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::View for ContextMenus {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.menu_button("Click for menu", Self::nested_menus);
|
||||
|
||||
ui.button("Right-click for menu")
|
||||
.context_menu(Self::nested_menus);
|
||||
|
||||
if ui.ctx().is_context_menu_open() {
|
||||
ui.label("Context menu is open");
|
||||
} else {
|
||||
ui.label("Context menu is closed");
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let response = ui.button("New menu");
|
||||
Popup::menu(&response).show(Self::nested_menus);
|
||||
|
||||
let response = ui.button("New context menu");
|
||||
Popup::context_menu(&response).show(Self::nested_menus);
|
||||
|
||||
ComboBox::new("Hi", "Hi").show_ui(ui, |ui| {
|
||||
_ = ui.selectable_label(false, "I have some long text that should be wrapped");
|
||||
_ = ui.selectable_label(false, "Short");
|
||||
_ = ui.selectable_label(false, "Medium length");
|
||||
});
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenus {
|
||||
fn nested_menus(ui: &mut egui::Ui) {
|
||||
ui.set_max_width(200.0); // To make sure we wrap long text
|
||||
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
ui.menu_button("Recursive", Self::nested_menus)
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
let _ = ui.button("Item");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
let _ = ui.button("Item1");
|
||||
let _ = ui.button("Item2");
|
||||
let _ = ui.button("Item3");
|
||||
let _ = ui.button("Item4");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
let _ = ui.button("Very long text for this item that should be wrapped");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
use std::collections::BTreeSet;
|
||||
|
||||
use egui::{Context, Modifiers, ScrollArea, Ui};
|
||||
|
||||
use super::About;
|
||||
use crate::is_mobile;
|
||||
use crate::Demo;
|
||||
use crate::View;
|
||||
|
||||
use egui::containers::menu;
|
||||
use egui::style::StyleModifier;
|
||||
use egui::{Context, Modifiers, ScrollArea, Ui};
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct DemoGroup {
|
||||
|
|
@ -65,7 +65,6 @@ impl Default for DemoGroups {
|
|||
Box::<super::paint_bezier::PaintBezier>::default(),
|
||||
Box::<super::code_editor::CodeEditor>::default(),
|
||||
Box::<super::code_example::CodeExample>::default(),
|
||||
Box::<super::context_menu::ContextMenus>::default(),
|
||||
Box::<super::dancing_strings::DancingStrings>::default(),
|
||||
Box::<super::drag_and_drop::DragAndDropDemo>::default(),
|
||||
Box::<super::extra_viewport::ExtraViewport>::default(),
|
||||
|
|
@ -227,29 +226,27 @@ impl DemoWindows {
|
|||
|
||||
fn mobile_top_bar(&mut self, ctx: &Context) {
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
let font_size = 16.5;
|
||||
menu::Bar::new()
|
||||
.config(menu::MenuConfig::new().style(StyleModifier::default()))
|
||||
.ui(ui, |ui| {
|
||||
let font_size = 16.5;
|
||||
|
||||
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
|
||||
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
|
||||
self.demo_list_ui(ui);
|
||||
if ui.ui_contains_pointer() && ui.input(|i| i.pointer.any_click()) {
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
|
||||
self.demo_list_ui(ui);
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
use egui::special_emojis::GITHUB;
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new("🦋").size(font_size),
|
||||
"https://bsky.app/profile/ernerfeldt.bsky.social",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(GITHUB).size(font_size),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
use egui::special_emojis::GITHUB;
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new("🦋").size(font_size),
|
||||
"https://bsky.app/profile/ernerfeldt.bsky.social",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(GITHUB).size(font_size),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -282,7 +279,7 @@ impl DemoWindows {
|
|||
});
|
||||
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
menu::Bar::new().ui(ui, |ui| {
|
||||
file_menu_button(ui);
|
||||
});
|
||||
});
|
||||
|
|
@ -345,7 +342,6 @@ fn file_menu_button(ui: &mut Ui) {
|
|||
.clicked()
|
||||
{
|
||||
ui.ctx().memory_mut(|mem| mem.reset_areas());
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if ui
|
||||
|
|
@ -357,7 +353,6 @@ fn file_menu_button(ui: &mut Ui) {
|
|||
.clicked()
|
||||
{
|
||||
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
pub mod about;
|
||||
pub mod code_editor;
|
||||
pub mod code_example;
|
||||
pub mod context_menu;
|
||||
pub mod dancing_strings;
|
||||
pub mod demo_app_windows;
|
||||
pub mod drag_and_drop;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
use egui::{vec2, Align2, ComboBox, Frame, Id, Popup, PopupCloseBehavior, RectAlign, Tooltip, Ui};
|
||||
use crate::rust_view_ui;
|
||||
use egui::containers::menu::{MenuConfig, SubMenuButton};
|
||||
use egui::{
|
||||
include_image, Align, Align2, ComboBox, Frame, Id, Layout, Popup, PopupCloseBehavior,
|
||||
RectAlign, Tooltip, Ui, UiBuilder,
|
||||
};
|
||||
|
||||
/// Showcase [`Popup`].
|
||||
#[derive(Clone, PartialEq)]
|
||||
|
|
@ -10,6 +15,7 @@ pub struct PopupsDemo {
|
|||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
close_behavior: PopupCloseBehavior,
|
||||
popup_open: bool,
|
||||
checked: bool,
|
||||
}
|
||||
|
||||
impl PopupsDemo {
|
||||
|
|
@ -28,6 +34,7 @@ impl Default for PopupsDemo {
|
|||
gap: 4.0,
|
||||
close_behavior: PopupCloseBehavior::CloseOnClick,
|
||||
popup_open: false,
|
||||
checked: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,120 +57,70 @@ impl crate::Demo for PopupsDemo {
|
|||
}
|
||||
}
|
||||
|
||||
fn nested_menus(ui: &mut egui::Ui, checked: &mut bool) {
|
||||
ui.set_max_width(200.0); // To make sure we wrap long text
|
||||
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
ui.menu_button("Popups can have submenus", |ui| {
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
ui.menu_button("Recursive", |ui| nested_menus(ui, checked));
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
let _ = ui.button("Item");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
ui.menu_image_text_button(
|
||||
include_image!("../../data/icon.png"),
|
||||
"I have an icon!",
|
||||
|ui| {
|
||||
let _ = ui.button("Item1");
|
||||
let _ = ui.button("Item2");
|
||||
let _ = ui.button("Item3");
|
||||
let _ = ui.button("Item4");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
let _ = ui.button("Very long text for this item that should be wrapped");
|
||||
SubMenuButton::new("Always CloseOnClickOutside")
|
||||
.config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside))
|
||||
.ui(ui, |ui| {
|
||||
ui.checkbox(checked, "Checkbox");
|
||||
if ui.button("Open…").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl crate::View for PopupsDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.style_mut().spacing.item_spacing.x = 0.0;
|
||||
let align_combobox = |ui: &mut Ui, label: &str, align: &mut Align2| {
|
||||
let aligns = [
|
||||
(Align2::LEFT_TOP, "Left top"),
|
||||
(Align2::LEFT_CENTER, "Left center"),
|
||||
(Align2::LEFT_BOTTOM, "Left bottom"),
|
||||
(Align2::CENTER_TOP, "Center top"),
|
||||
(Align2::CENTER_CENTER, "Center center"),
|
||||
(Align2::CENTER_BOTTOM, "Center bottom"),
|
||||
(Align2::RIGHT_TOP, "Right top"),
|
||||
(Align2::RIGHT_CENTER, "Right center"),
|
||||
(Align2::RIGHT_BOTTOM, "Right bottom"),
|
||||
];
|
||||
|
||||
ui.label(label);
|
||||
ComboBox::new(label, "")
|
||||
.selected_text(aligns.iter().find(|(a, _)| a == align).unwrap().1)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align2, name) in &aligns {
|
||||
ui.selectable_value(align, *align2, *name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ui.label("Align4(");
|
||||
align_combobox(ui, "parent: ", &mut self.align4.parent);
|
||||
ui.label(", ");
|
||||
align_combobox(ui, "child: ", &mut self.align4.child);
|
||||
ui.label(") ");
|
||||
|
||||
let presets = [
|
||||
(RectAlign::TOP_START, "Top start"),
|
||||
(RectAlign::TOP, "Top"),
|
||||
(RectAlign::TOP_END, "Top end"),
|
||||
(RectAlign::RIGHT_START, "Right start"),
|
||||
(RectAlign::RIGHT, "Right Center"),
|
||||
(RectAlign::RIGHT_END, "Right end"),
|
||||
(RectAlign::BOTTOM_START, "Bottom start"),
|
||||
(RectAlign::BOTTOM, "Bottom"),
|
||||
(RectAlign::BOTTOM_END, "Bottom end"),
|
||||
(RectAlign::LEFT_START, "Left start"),
|
||||
(RectAlign::LEFT, "Left"),
|
||||
(RectAlign::LEFT_END, "Left end"),
|
||||
];
|
||||
|
||||
ui.label(" Presets: ");
|
||||
|
||||
ComboBox::new("Preset", "")
|
||||
.selected_text(
|
||||
presets
|
||||
.iter()
|
||||
.find(|(a, _)| a == &self.align4)
|
||||
.map_or("Select", |(_, name)| *name),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align4, name) in &presets {
|
||||
ui.selectable_value(&mut self.align4, *align4, *name);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Gap:");
|
||||
ui.add(egui::DragValue::new(&mut self.gap));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Close behavior:");
|
||||
ui.selectable_value(
|
||||
&mut self.close_behavior,
|
||||
PopupCloseBehavior::CloseOnClick,
|
||||
"Close on click",
|
||||
)
|
||||
.on_hover_text("Closes when the user clicks anywhere (inside or outside)");
|
||||
ui.selectable_value(
|
||||
&mut self.close_behavior,
|
||||
PopupCloseBehavior::CloseOnClickOutside,
|
||||
"Close on click outside",
|
||||
)
|
||||
.on_hover_text("Closes when the user clicks outside the popup");
|
||||
ui.selectable_value(
|
||||
&mut self.close_behavior,
|
||||
PopupCloseBehavior::IgnoreClicks,
|
||||
"Ignore clicks",
|
||||
)
|
||||
.on_hover_text("Close only when the button is clicked again");
|
||||
});
|
||||
|
||||
ui.checkbox(&mut self.popup_open, "Show popup");
|
||||
|
||||
let response = Frame::group(ui.style())
|
||||
.inner_margin(vec2(0.0, 25.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.vertical_centered(|ui| ui.button("Click, right-click and hover me!"))
|
||||
.inner
|
||||
})
|
||||
.inner;
|
||||
|
||||
self.apply_options(Popup::menu(&response).id(Id::new("menu")))
|
||||
.show(|ui| {
|
||||
_ = ui.button("Menu item 1");
|
||||
_ = ui.button("Menu item 2");
|
||||
|
||||
if ui.button("I always close the menu").clicked() {
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
.show(|ui| nested_menus(ui, &mut self.checked));
|
||||
|
||||
self.apply_options(Popup::context_menu(&response).id(Id::new("context_menu")))
|
||||
.show(|ui| {
|
||||
_ = ui.button("Context menu item 1");
|
||||
_ = ui.button("Context menu item 2");
|
||||
});
|
||||
.show(|ui| nested_menus(ui, &mut self.checked));
|
||||
|
||||
if self.popup_open {
|
||||
self.apply_options(Popup::from_response(&response).id(Id::new("popup")))
|
||||
|
|
@ -178,6 +135,148 @@ impl crate::View for PopupsDemo {
|
|||
ui.label("Tooltips are popups, too!");
|
||||
});
|
||||
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
let mut reset_btn_ui = ui.new_child(
|
||||
UiBuilder::new()
|
||||
.max_rect(ui.max_rect())
|
||||
.layout(Layout::right_to_left(Align::Min)),
|
||||
);
|
||||
if reset_btn_ui
|
||||
.button("⟲")
|
||||
.on_hover_text("Reset to defaults")
|
||||
.clicked()
|
||||
{
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
ui.set_width(ui.available_width());
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
let align_combobox = |ui: &mut Ui, label: &str, align: &mut Align2| {
|
||||
let aligns = [
|
||||
(Align2::LEFT_TOP, "LEFT_TOP"),
|
||||
(Align2::LEFT_CENTER, "LEFT_CENTER"),
|
||||
(Align2::LEFT_BOTTOM, "LEFT_BOTTOM"),
|
||||
(Align2::CENTER_TOP, "CENTER_TOP"),
|
||||
(Align2::CENTER_CENTER, "CENTER_CENTER"),
|
||||
(Align2::CENTER_BOTTOM, "CENTER_BOTTOM"),
|
||||
(Align2::RIGHT_TOP, "RIGHT_TOP"),
|
||||
(Align2::RIGHT_CENTER, "RIGHT_CENTER"),
|
||||
(Align2::RIGHT_BOTTOM, "RIGHT_BOTTOM"),
|
||||
];
|
||||
|
||||
ComboBox::new(label, "")
|
||||
.selected_text(aligns.iter().find(|(a, _)| a == align).unwrap().1)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align2, name) in &aligns {
|
||||
ui.selectable_value(align, *align2, *name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
rust_view_ui(ui, "let align = RectAlign {");
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, " parent: Align2::");
|
||||
align_combobox(ui, "parent", &mut self.align4.parent);
|
||||
rust_view_ui(ui, ",");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, " child: Align2::");
|
||||
align_combobox(ui, "child", &mut self.align4.child);
|
||||
rust_view_ui(ui, ",");
|
||||
});
|
||||
rust_view_ui(ui, "};");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, "let align = RectAlign::");
|
||||
|
||||
let presets = [
|
||||
(RectAlign::TOP_START, "TOP_START"),
|
||||
(RectAlign::TOP, "TOP"),
|
||||
(RectAlign::TOP_END, "TOP_END"),
|
||||
(RectAlign::RIGHT_START, "RIGHT_START"),
|
||||
(RectAlign::RIGHT, "RIGHT"),
|
||||
(RectAlign::RIGHT_END, "RIGHT_END"),
|
||||
(RectAlign::BOTTOM_START, "BOTTOM_START"),
|
||||
(RectAlign::BOTTOM, "BOTTOM"),
|
||||
(RectAlign::BOTTOM_END, "BOTTOM_END"),
|
||||
(RectAlign::LEFT_START, "LEFT_START"),
|
||||
(RectAlign::LEFT, "LEFT"),
|
||||
(RectAlign::LEFT_END, "LEFT_END"),
|
||||
];
|
||||
|
||||
ComboBox::new("Preset", "")
|
||||
.selected_text(
|
||||
presets
|
||||
.iter()
|
||||
.find(|(a, _)| a == &self.align4)
|
||||
.map_or("<Select Preset>", |(_, name)| *name),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align4, name) in &presets {
|
||||
ui.selectable_value(&mut self.align4, *align4, *name);
|
||||
}
|
||||
});
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, "let gap = ");
|
||||
ui.add(egui::DragValue::new(&mut self.gap));
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
|
||||
rust_view_ui(ui, "let close_behavior");
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, " = PopupCloseBehavior::");
|
||||
let close_behaviors = [
|
||||
(
|
||||
PopupCloseBehavior::CloseOnClick,
|
||||
"CloseOnClick",
|
||||
"Closes when the user clicks anywhere (inside or outside)",
|
||||
),
|
||||
(
|
||||
PopupCloseBehavior::CloseOnClickOutside,
|
||||
"CloseOnClickOutside",
|
||||
"Closes when the user clicks outside the popup",
|
||||
),
|
||||
(
|
||||
PopupCloseBehavior::IgnoreClicks,
|
||||
"IgnoreClicks",
|
||||
"Close only when the button is clicked again",
|
||||
),
|
||||
];
|
||||
ComboBox::new("Close behavior", "")
|
||||
.selected_text(
|
||||
close_behaviors
|
||||
.iter()
|
||||
.find_map(|(behavior, text, _)| {
|
||||
(behavior == &self.close_behavior).then_some(*text)
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
for (close_behavior, name, tooltip) in &close_behaviors {
|
||||
ui.selectable_value(&mut self.close_behavior, *close_behavior, *name)
|
||||
.on_hover_text(*tooltip);
|
||||
}
|
||||
});
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
rust_view_ui(ui, "let popup_open = ");
|
||||
ui.checkbox(&mut self.popup_open, "");
|
||||
rust_view_ui(ui, ";");
|
||||
});
|
||||
ui.monospace("");
|
||||
rust_view_ui(ui, "let response = ui.button(\"Click me!\");");
|
||||
rust_view_ui(ui, "Popup::menu(&response)");
|
||||
rust_view_ui(ui, " .gap(gap).align(align)");
|
||||
rust_view_ui(ui, " .close_behavior(close_behavior)");
|
||||
rust_view_ui(ui, " .show(|ui| { /* menu contents */ });");
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4806984f9c801a054cea80b89664293680abaa57cf0a95cf9682f111e3794fc1
|
||||
size 25080
|
||||
oid sha256:17dc2a2f98d4cc52f6c6337dcc2e40f22d7310a933de91bb60576b893926193c
|
||||
size 58674
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ document-features = { workspace = true, optional = true }
|
|||
[dev-dependencies]
|
||||
egui = { workspace = true, features = ["default_fonts"] }
|
||||
image = { workspace = true, features = ["png"] }
|
||||
egui_extras = { workspace = true, features = ["image"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
use egui::containers::menu::{Bar, MenuConfig, SubMenuButton};
|
||||
use egui::{include_image, PopupCloseBehavior, Ui};
|
||||
use egui_kittest::{Harness, SnapshotResults};
|
||||
use kittest::Queryable;
|
||||
|
||||
struct TestMenu {
|
||||
config: MenuConfig,
|
||||
checked: bool,
|
||||
}
|
||||
|
||||
impl TestMenu {
|
||||
fn new(config: MenuConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
checked: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.vertical(|ui| {
|
||||
Bar::new().config(self.config.clone()).ui(ui, |ui| {
|
||||
egui::Sides::new().show(
|
||||
ui,
|
||||
|ui| {
|
||||
ui.menu_button("Menu A", |ui| {
|
||||
_ = ui.button("Button in Menu A");
|
||||
ui.menu_button("Submenu A", |ui| {
|
||||
for i in 0..4 {
|
||||
_ = ui.button(format!("Button {i} in Submenu A"));
|
||||
}
|
||||
});
|
||||
ui.menu_image_text_button(
|
||||
include_image!("../../eframe/data/icon.png"),
|
||||
"Submenu B with icon",
|
||||
|ui| {
|
||||
_ = ui.button("Button in Submenu B");
|
||||
},
|
||||
);
|
||||
SubMenuButton::new("Submenu C (CloseOnClickOutside)")
|
||||
.config(
|
||||
MenuConfig::new()
|
||||
.close_behavior(PopupCloseBehavior::CloseOnClickOutside),
|
||||
)
|
||||
.ui(ui, |ui| {
|
||||
_ = ui.button("Button in Submenu C");
|
||||
ui.checkbox(&mut self.checked, "Checkbox in Submenu C");
|
||||
ui.menu_button("Submenu D", |ui| {
|
||||
if ui
|
||||
.button("Button in Submenu D (close on click)")
|
||||
.clicked()
|
||||
{
|
||||
ui.close();
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.menu_image_text_button(
|
||||
include_image!("../../eframe/data/icon.png"),
|
||||
"Menu B with icon",
|
||||
|ui| {
|
||||
_ = ui.button("Button in Menu B");
|
||||
},
|
||||
);
|
||||
_ = ui.button("Menu Button");
|
||||
ui.menu_button("Menu C", |ui| {
|
||||
_ = ui.button("Button in Menu C");
|
||||
});
|
||||
},
|
||||
|ui| {
|
||||
ui.label("Some other label");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn into_harness(self) -> Harness<'static, Self> {
|
||||
Harness::builder()
|
||||
.with_size(egui::Vec2::new(500.0, 300.0))
|
||||
.build_ui_state(
|
||||
|ui, menu| {
|
||||
egui_extras::install_image_loaders(ui.ctx());
|
||||
menu.ui(ui);
|
||||
},
|
||||
self,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_close_on_click_outside() {
|
||||
// We're intentionally setting CloseOnClick here so we can test if a submenu can override the
|
||||
// close behavior. (Note how Submenu C has CloseOnClickOutside set)
|
||||
let mut harness =
|
||||
TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick))
|
||||
.into_harness();
|
||||
|
||||
harness.get_by_label("Menu A").simulate_click();
|
||||
harness.run();
|
||||
|
||||
harness
|
||||
.get_by_label("Submenu C (CloseOnClickOutside)")
|
||||
.hover();
|
||||
harness.run();
|
||||
|
||||
// We should be able to check the checkbox without closing the menu
|
||||
// Click a couple of times, just in case
|
||||
for expect_checked in [true, false, true, false] {
|
||||
harness
|
||||
.get_by_label("Checkbox in Submenu C")
|
||||
.simulate_click();
|
||||
harness.run();
|
||||
assert_eq!(expect_checked, harness.state().checked);
|
||||
}
|
||||
|
||||
// Hovering outside should not close the menu
|
||||
harness.get_by_label("Some other label").hover();
|
||||
harness.run();
|
||||
assert!(harness.query_by_label("Checkbox in Submenu C").is_some());
|
||||
|
||||
// Clicking outside should close the menu
|
||||
harness.get_by_label("Some other label").simulate_click();
|
||||
harness.run();
|
||||
assert!(harness.query_by_label("Checkbox in Submenu C").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_close_on_click() {
|
||||
let mut harness =
|
||||
TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick))
|
||||
.into_harness();
|
||||
|
||||
harness.get_by_label("Menu A").simulate_click();
|
||||
harness.run();
|
||||
|
||||
harness.get_by_label("Submenu B with icon").hover();
|
||||
harness.run();
|
||||
|
||||
// Clicking the button should close the menu (even if ui.close() is not called by the button)
|
||||
harness.get_by_label("Button in Submenu B").simulate_click();
|
||||
harness.run();
|
||||
assert!(harness.query_by_label("Button in Submenu B").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_submenu_button_should_never_close_menu() {
|
||||
// We test for this since otherwise the menu wouldn't work on touch devices
|
||||
// The other tests use .hover to open submenus, but this test explicitly uses .simulate_click
|
||||
let mut harness =
|
||||
TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick))
|
||||
.into_harness();
|
||||
|
||||
harness.get_by_label("Menu A").simulate_click();
|
||||
harness.run();
|
||||
|
||||
// Clicking the submenu button should not close the menu
|
||||
harness.get_by_label("Submenu B with icon").simulate_click();
|
||||
harness.run();
|
||||
|
||||
harness.get_by_label("Button in Submenu B").simulate_click();
|
||||
harness.run();
|
||||
assert!(harness.query_by_label("Button in Submenu B").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_snapshots() {
|
||||
let mut harness = TestMenu::new(MenuConfig::new()).into_harness();
|
||||
|
||||
let mut results = SnapshotResults::new();
|
||||
|
||||
harness.get_by_label("Menu A").hover();
|
||||
harness.run();
|
||||
results.add(harness.try_snapshot("menu/closed_hovered"));
|
||||
|
||||
harness.get_by_label("Menu A").simulate_click();
|
||||
harness.run();
|
||||
results.add(harness.try_snapshot("menu/opened"));
|
||||
|
||||
harness
|
||||
.get_by_label("Submenu C (CloseOnClickOutside)")
|
||||
.hover();
|
||||
harness.run();
|
||||
results.add(harness.try_snapshot("menu/submenu"));
|
||||
|
||||
harness.get_by_label("Submenu D").hover();
|
||||
harness.run();
|
||||
results.add(harness.try_snapshot("menu/subsubmenu"));
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d96ef60e9cd8767c0a95cc1fb0e240014248d7b0d6f67777a5b6ca4f23e91380
|
||||
size 10732
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:23dced7849e2bdd436e0552b2d351fef5693dd688c875816aaba3607a3aa1197
|
||||
size 21756
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6b8790dbd01409496ca6dc6d322d69c59a02f552b244264c8f6d7ea847846d5a
|
||||
size 28979
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6f0b04c42dfb257ca650a2fe332509f847cc94f4666cc29d830086627b91fc25
|
||||
size 33737
|
||||
Loading…
Reference in New Issue