⚠️ 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",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui-wgpu",
|
"egui-wgpu",
|
||||||
|
"egui_extras",
|
||||||
"image",
|
"image",
|
||||||
"kittest",
|
"kittest",
|
||||||
"pollster 0.4.0",
|
"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()`].
|
/// The previous rectangle used by this area can be obtained through [`crate::Memory::area_rect()`].
|
||||||
#[must_use = "You should call .show()"]
|
#[must_use = "You should call .show()"]
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Area {
|
pub struct Area {
|
||||||
pub(crate) id: Id,
|
pub(crate) id: Id,
|
||||||
kind: UiKind,
|
info: UiStackInfo,
|
||||||
sense: Option<Sense>,
|
sense: Option<Sense>,
|
||||||
movable: bool,
|
movable: bool,
|
||||||
interactable: bool,
|
interactable: bool,
|
||||||
|
|
@ -132,7 +132,7 @@ impl Area {
|
||||||
pub fn new(id: Id) -> Self {
|
pub fn new(id: Id) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
kind: UiKind::GenericArea,
|
info: UiStackInfo::new(UiKind::GenericArea),
|
||||||
sense: None,
|
sense: None,
|
||||||
movable: true,
|
movable: true,
|
||||||
interactable: true,
|
interactable: true,
|
||||||
|
|
@ -164,7 +164,16 @@ impl Area {
|
||||||
/// Default to [`UiKind::GenericArea`].
|
/// Default to [`UiKind::GenericArea`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn kind(mut self, kind: UiKind) -> Self {
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,7 +360,7 @@ impl Area {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Prepared {
|
pub(crate) struct Prepared {
|
||||||
kind: UiKind,
|
info: Option<UiStackInfo>,
|
||||||
layer_id: LayerId,
|
layer_id: LayerId,
|
||||||
state: AreaState,
|
state: AreaState,
|
||||||
move_response: Response,
|
move_response: Response,
|
||||||
|
|
@ -376,7 +385,7 @@ impl Area {
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> InnerResponse<R> {
|
) -> InnerResponse<R> {
|
||||||
let prepared = self.begin(ctx);
|
let mut prepared = self.begin(ctx);
|
||||||
let mut content_ui = prepared.content_ui(ctx);
|
let mut content_ui = prepared.content_ui(ctx);
|
||||||
let inner = add_contents(&mut content_ui);
|
let inner = add_contents(&mut content_ui);
|
||||||
let response = prepared.end(ctx, content_ui);
|
let response = prepared.end(ctx, content_ui);
|
||||||
|
|
@ -386,7 +395,7 @@ impl Area {
|
||||||
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
|
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
|
||||||
let Self {
|
let Self {
|
||||||
id,
|
id,
|
||||||
kind,
|
info,
|
||||||
sense,
|
sense,
|
||||||
movable,
|
movable,
|
||||||
order,
|
order,
|
||||||
|
|
@ -518,7 +527,7 @@ impl Area {
|
||||||
move_response.interact_rect = state.rect();
|
move_response.interact_rect = state.rect();
|
||||||
|
|
||||||
Prepared {
|
Prepared {
|
||||||
kind,
|
info: Some(info),
|
||||||
layer_id,
|
layer_id,
|
||||||
state,
|
state,
|
||||||
move_response,
|
move_response,
|
||||||
|
|
@ -549,11 +558,11 @@ impl Prepared {
|
||||||
self.constrain_rect
|
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 max_rect = self.state.rect();
|
||||||
|
|
||||||
let mut ui_builder = UiBuilder::new()
|
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)
|
.layer_id(self.layer_id)
|
||||||
.max_rect(max_rect)
|
.max_rect(max_rect)
|
||||||
.layout(self.layout)
|
.layout(self.layout)
|
||||||
|
|
@ -596,7 +605,7 @@ impl Prepared {
|
||||||
#[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
|
#[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
|
||||||
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
|
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
|
||||||
let Self {
|
let Self {
|
||||||
kind: _,
|
info: _,
|
||||||
layer_id,
|
layer_id,
|
||||||
mut state,
|
mut state,
|
||||||
move_response: mut response,
|
move_response: mut response,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use epaint::Shape;
|
use epaint::Shape;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
epaint, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, NumExt, Painter, Popup,
|
epaint, style::StyleModifier, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse,
|
||||||
PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui,
|
NumExt, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
|
||||||
UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
|
TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(unused_imports)] // Documentation
|
#[allow(unused_imports)] // Documentation
|
||||||
|
|
@ -374,6 +374,7 @@ fn combo_box_dyn<'c, R>(
|
||||||
|
|
||||||
let inner = Popup::menu(&button_response)
|
let inner = Popup::menu(&button_response)
|
||||||
.id(popup_id)
|
.id(popup_id)
|
||||||
|
.style(StyleModifier::default())
|
||||||
.width(button_response.rect.width())
|
.width(button_response.rect.width())
|
||||||
.close_behavior(close_behavior)
|
.close_behavior(close_behavior)
|
||||||
.show(|ui| {
|
.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;
|
pub mod collapsing_header;
|
||||||
mod combo_box;
|
mod combo_box;
|
||||||
pub mod frame;
|
pub mod frame;
|
||||||
|
pub mod menu;
|
||||||
pub mod modal;
|
pub mod modal;
|
||||||
pub mod old_popup;
|
pub mod old_popup;
|
||||||
pub mod panel;
|
pub mod panel;
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,8 @@ pub fn popup_above_or_below_widget<R>(
|
||||||
) -> Option<R> {
|
) -> Option<R> {
|
||||||
let response = Popup::from_response(widget_response)
|
let response = Popup::from_response(widget_response)
|
||||||
.layout(Layout::top_down_justified(Align::LEFT))
|
.layout(Layout::top_down_justified(Align::LEFT))
|
||||||
.open_memory(None, close_behavior)
|
.open_memory(None)
|
||||||
|
.close_behavior(close_behavior)
|
||||||
.id(popup_id)
|
.id(popup_id)
|
||||||
.align(match above_or_below {
|
.align(match above_or_below {
|
||||||
AboveOrBelow::Above => RectAlign::TOP_START,
|
AboveOrBelow::Above => RectAlign::TOP_START,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
use crate::containers::menu::{menu_style, MenuConfig, MenuState};
|
||||||
|
use crate::style::StyleModifier;
|
||||||
use crate::{
|
use crate::{
|
||||||
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
|
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 emath::{vec2, Align, Pos2, Rect, RectAlign, Vec2};
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
|
|
@ -44,7 +46,8 @@ impl From<Pos2> for PopupAnchor {
|
||||||
|
|
||||||
impl From<&Response> for PopupAnchor {
|
impl From<&Response> for PopupAnchor {
|
||||||
fn from(response: &Response) -> Self {
|
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) {
|
if let Some(to_global) = response.ctx.layer_transform_to_global(response.layer_id) {
|
||||||
widget_rect = to_global * widget_rect;
|
widget_rect = to_global * widget_rect;
|
||||||
}
|
}
|
||||||
|
|
@ -69,11 +72,12 @@ impl PopupAnchor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines popup's close behavior
|
/// Determines popup's close behavior
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
|
||||||
pub enum PopupCloseBehavior {
|
pub enum PopupCloseBehavior {
|
||||||
/// Popup will be closed on click anywhere, inside or outside the popup.
|
/// 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,
|
CloseOnClick,
|
||||||
|
|
||||||
/// Popup will be closed if the click happened somewhere else
|
/// Popup will be closed if the click happened somewhere else
|
||||||
|
|
@ -109,13 +113,10 @@ enum OpenKind<'a> {
|
||||||
Closed,
|
Closed,
|
||||||
|
|
||||||
/// Open if the bool is true
|
/// Open if the bool is true
|
||||||
Bool(&'a mut bool, PopupCloseBehavior),
|
Bool(&'a mut bool),
|
||||||
|
|
||||||
/// Store the open state via [`crate::Memory`]
|
/// Store the open state via [`crate::Memory`]
|
||||||
Memory {
|
Memory { set: Option<SetOpenCommand> },
|
||||||
set: Option<SetOpenCommand>,
|
|
||||||
close_behavior: PopupCloseBehavior,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenKind<'_> {
|
impl OpenKind<'_> {
|
||||||
|
|
@ -124,7 +125,7 @@ impl OpenKind<'_> {
|
||||||
match self {
|
match self {
|
||||||
OpenKind::Open => true,
|
OpenKind::Open => true,
|
||||||
OpenKind::Closed => false,
|
OpenKind::Closed => false,
|
||||||
OpenKind::Bool(open, _) => **open,
|
OpenKind::Bool(open) => **open,
|
||||||
OpenKind::Memory { .. } => ctx.memory(|mem| mem.is_popup_open(id)),
|
OpenKind::Memory { .. } => ctx.memory(|mem| mem.is_popup_open(id)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,6 +139,26 @@ pub enum PopupKind {
|
||||||
Menu,
|
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> {
|
pub struct Popup<'a> {
|
||||||
id: Id,
|
id: Id,
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
|
|
@ -146,6 +167,8 @@ pub struct Popup<'a> {
|
||||||
alternative_aligns: Option<&'a [RectAlign]>,
|
alternative_aligns: Option<&'a [RectAlign]>,
|
||||||
layer_id: LayerId,
|
layer_id: LayerId,
|
||||||
open_kind: OpenKind<'a>,
|
open_kind: OpenKind<'a>,
|
||||||
|
close_behavior: PopupCloseBehavior,
|
||||||
|
info: Option<UiStackInfo>,
|
||||||
kind: PopupKind,
|
kind: PopupKind,
|
||||||
|
|
||||||
/// Gap between the anchor and the popup
|
/// Gap between the anchor and the popup
|
||||||
|
|
@ -159,6 +182,7 @@ pub struct Popup<'a> {
|
||||||
sense: Sense,
|
sense: Sense,
|
||||||
layout: Layout,
|
layout: Layout,
|
||||||
frame: Option<Frame>,
|
frame: Option<Frame>,
|
||||||
|
style: StyleModifier,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Popup<'a> {
|
impl<'a> Popup<'a> {
|
||||||
|
|
@ -169,6 +193,8 @@ impl<'a> Popup<'a> {
|
||||||
ctx,
|
ctx,
|
||||||
anchor: anchor.into(),
|
anchor: anchor.into(),
|
||||||
open_kind: OpenKind::Open,
|
open_kind: OpenKind::Open,
|
||||||
|
close_behavior: PopupCloseBehavior::default(),
|
||||||
|
info: None,
|
||||||
kind: PopupKind::Popup,
|
kind: PopupKind::Popup,
|
||||||
layer_id,
|
layer_id,
|
||||||
rect_align: RectAlign::BOTTOM_START,
|
rect_align: RectAlign::BOTTOM_START,
|
||||||
|
|
@ -179,6 +205,7 @@ impl<'a> Popup<'a> {
|
||||||
sense: Sense::click(),
|
sense: Sense::click(),
|
||||||
layout: Layout::default(),
|
layout: Layout::default(),
|
||||||
frame: None,
|
frame: None,
|
||||||
|
style: StyleModifier::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,6 +216,13 @@ impl<'a> Popup<'a> {
|
||||||
self
|
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`].
|
/// Set the [`RectAlign`] of the popup relative to the [`PopupAnchor`].
|
||||||
/// This is the default position, and will be used if it fits.
|
/// This is the default position, and will be used if it fits.
|
||||||
/// See [`Self::align_alternatives`] for more on this.
|
/// 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)`.
|
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
|
||||||
pub fn menu(response: &Response) -> Self {
|
pub fn menu(response: &Response) -> Self {
|
||||||
Self::from_response(response)
|
Self::from_response(response)
|
||||||
.open_memory(
|
.open_memory(response.clicked().then_some(SetOpenCommand::Toggle))
|
||||||
if response.clicked() {
|
.kind(PopupKind::Menu)
|
||||||
Some(SetOpenCommand::Toggle)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
PopupCloseBehavior::CloseOnClick,
|
|
||||||
)
|
|
||||||
.layout(Layout::top_down_justified(Align::Min))
|
.layout(Layout::top_down_justified(Align::Min))
|
||||||
|
.style(menu_style)
|
||||||
|
.gap(0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a context menu when the widget was secondary clicked.
|
/// Show a context menu when the widget was secondary clicked.
|
||||||
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
|
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
|
||||||
/// In contrast to [`Self::menu`], this will open at the pointer position.
|
/// In contrast to [`Self::menu`], this will open at the pointer position.
|
||||||
pub fn context_menu(response: &Response) -> Self {
|
pub fn context_menu(response: &Response) -> Self {
|
||||||
Self::from_response(response)
|
Self::menu(response)
|
||||||
.open_memory(
|
.open_memory(
|
||||||
response
|
response
|
||||||
.secondary_clicked()
|
.secondary_clicked()
|
||||||
.then_some(SetOpenCommand::Bool(true)),
|
.then_some(SetOpenCommand::Bool(true)),
|
||||||
PopupCloseBehavior::CloseOnClick,
|
|
||||||
)
|
)
|
||||||
.layout(Layout::top_down_justified(Align::Min))
|
|
||||||
.at_pointer_fixed()
|
.at_pointer_fixed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,22 +294,17 @@ impl<'a> Popup<'a> {
|
||||||
/// Store the open state via [`crate::Memory`].
|
/// Store the open state via [`crate::Memory`].
|
||||||
/// You can set the state via the first [`SetOpenCommand`] param.
|
/// You can set the state via the first [`SetOpenCommand`] param.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn open_memory(
|
pub fn open_memory(mut self, set_state: impl Into<Option<SetOpenCommand>>) -> Self {
|
||||||
mut self,
|
|
||||||
set_state: impl Into<Option<SetOpenCommand>>,
|
|
||||||
close_behavior: PopupCloseBehavior,
|
|
||||||
) -> Self {
|
|
||||||
self.open_kind = OpenKind::Memory {
|
self.open_kind = OpenKind::Memory {
|
||||||
set: set_state.into(),
|
set: set_state.into(),
|
||||||
close_behavior,
|
|
||||||
};
|
};
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store the open state via a mutable bool.
|
/// Store the open state via a mutable bool.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn open_bool(mut self, open: &'a mut bool, close_behavior: PopupCloseBehavior) -> Self {
|
pub fn open_bool(mut self, open: &'a mut bool) -> Self {
|
||||||
self.open_kind = OpenKind::Bool(open, close_behavior);
|
self.open_kind = OpenKind::Bool(open);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,16 +313,7 @@ impl<'a> Popup<'a> {
|
||||||
/// This will do nothing if [`Popup::open`] was called.
|
/// This will do nothing if [`Popup::open`] was called.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
|
pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
|
||||||
match &mut self.open_kind {
|
self.close_behavior = close_behavior;
|
||||||
OpenKind::Memory {
|
|
||||||
close_behavior: behavior,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| OpenKind::Bool(_, behavior) => {
|
|
||||||
*behavior = close_behavior;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -339,6 +353,13 @@ impl<'a> Popup<'a> {
|
||||||
self
|
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.
|
/// Set the sense of the popup.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn sense(mut self, sense: Sense) -> Self {
|
pub fn sense(mut self, sense: Sense) -> Self {
|
||||||
|
|
@ -367,6 +388,17 @@ impl<'a> Popup<'a> {
|
||||||
self
|
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`]
|
/// Get the [`Context`]
|
||||||
pub fn ctx(&self) -> &Context {
|
pub fn ctx(&self) -> &Context {
|
||||||
&self.ctx
|
&self.ctx
|
||||||
|
|
@ -408,11 +440,12 @@ impl<'a> Popup<'a> {
|
||||||
match &self.open_kind {
|
match &self.open_kind {
|
||||||
OpenKind::Open => true,
|
OpenKind::Open => true,
|
||||||
OpenKind::Closed => false,
|
OpenKind::Closed => false,
|
||||||
OpenKind::Bool(open, _) => **open,
|
OpenKind::Bool(open) => **open,
|
||||||
OpenKind::Memory { .. } => self.ctx.memory(|mem| mem.is_popup_open(self.id)),
|
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> {
|
pub fn get_expected_size(&self) -> Option<Vec2> {
|
||||||
AreaState::load(&self.ctx, self.id).and_then(|area| area.size)
|
AreaState::load(&self.ctx, self.id).and_then(|area| area.size)
|
||||||
}
|
}
|
||||||
|
|
@ -459,7 +492,9 @@ impl<'a> Popup<'a> {
|
||||||
ctx,
|
ctx,
|
||||||
anchor,
|
anchor,
|
||||||
open_kind,
|
open_kind,
|
||||||
|
close_behavior,
|
||||||
kind,
|
kind,
|
||||||
|
info,
|
||||||
layer_id,
|
layer_id,
|
||||||
rect_align: _,
|
rect_align: _,
|
||||||
alternative_aligns: _,
|
alternative_aligns: _,
|
||||||
|
|
@ -469,6 +504,7 @@ impl<'a> Popup<'a> {
|
||||||
sense,
|
sense,
|
||||||
layout,
|
layout,
|
||||||
frame,
|
frame,
|
||||||
|
style,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let hover_pos = ctx.pointer_hover_pos();
|
let hover_pos = ctx.pointer_hover_pos();
|
||||||
|
|
@ -497,13 +533,7 @@ impl<'a> Popup<'a> {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (ui_kind, order) = match kind {
|
if kind != PopupKind::Tooltip {
|
||||||
PopupKind::Popup => (UiKind::Popup, Order::Foreground),
|
|
||||||
PopupKind::Tooltip => (UiKind::Tooltip, Order::Tooltip),
|
|
||||||
PopupKind::Menu => (UiKind::Menu, Order::Foreground),
|
|
||||||
};
|
|
||||||
|
|
||||||
if kind == PopupKind::Popup {
|
|
||||||
ctx.pass_state_mut(|fs| {
|
ctx.pass_state_mut(|fs| {
|
||||||
fs.layers
|
fs.layers
|
||||||
.entry(layer_id)
|
.entry(layer_id)
|
||||||
|
|
@ -518,12 +548,17 @@ impl<'a> Popup<'a> {
|
||||||
let (pivot, anchor) = best_align.pivot_pos(&anchor_rect, gap);
|
let (pivot, anchor) = best_align.pivot_pos(&anchor_rect, gap);
|
||||||
|
|
||||||
let mut area = Area::new(id)
|
let mut area = Area::new(id)
|
||||||
.order(order)
|
.order(kind.order())
|
||||||
.kind(ui_kind)
|
|
||||||
.pivot(pivot)
|
.pivot(pivot)
|
||||||
.fixed_pos(anchor)
|
.fixed_pos(anchor)
|
||||||
.sense(sense)
|
.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 {
|
if let Some(width) = width {
|
||||||
area = area.default_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 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 closed_by_click = match close_behavior {
|
||||||
let should_close = match close_behavior {
|
PopupCloseBehavior::CloseOnClick => widget_clicked_elsewhere,
|
||||||
PopupCloseBehavior::CloseOnClick => widget_clicked_elsewhere,
|
PopupCloseBehavior::CloseOnClickOutside => {
|
||||||
PopupCloseBehavior::CloseOnClickOutside => {
|
widget_clicked_elsewhere && response.response.clicked_elsewhere()
|
||||||
widget_clicked_elsewhere && response.response.clicked_elsewhere()
|
}
|
||||||
}
|
PopupCloseBehavior::IgnoreClicks => false,
|
||||||
PopupCloseBehavior::IgnoreClicks => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
should_close
|
|
||||||
|| ctx.input(|i| i.key_pressed(Key::Escape))
|
|
||||||
|| response.response.should_close()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 {
|
match open_kind {
|
||||||
OpenKind::Open | OpenKind::Closed => {}
|
OpenKind::Open | OpenKind::Closed => {}
|
||||||
OpenKind::Bool(open, close_behavior) => {
|
OpenKind::Bool(open) => {
|
||||||
if should_close(close_behavior) {
|
if should_close {
|
||||||
*open = false;
|
*open = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpenKind::Memory { close_behavior, .. } => {
|
OpenKind::Memory { .. } => {
|
||||||
if should_close(close_behavior) {
|
if should_close {
|
||||||
ctx.memory_mut(|mem| mem.close_popup());
|
ctx.memory_mut(|mem| mem.close_popup());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ use crate::{
|
||||||
load,
|
load,
|
||||||
load::{Bytes, Loaders, SizedTexture},
|
load::{Bytes, Loaders, SizedTexture},
|
||||||
memory::{Options, Theme},
|
memory::{Options, Theme},
|
||||||
menu,
|
|
||||||
os::OperatingSystem,
|
os::OperatingSystem,
|
||||||
output::FullOutput,
|
output::FullOutput,
|
||||||
pass_state::PassState,
|
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.
|
/// 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 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> {
|
pub fn read_response(&self, id: Id) -> Option<Response> {
|
||||||
self.write(|ctx| {
|
self.write(|ctx| {
|
||||||
let viewport = ctx.viewport();
|
let viewport = ctx.viewport();
|
||||||
viewport
|
let widget_rect = viewport
|
||||||
.this_pass
|
.this_pass
|
||||||
.widgets
|
.widgets
|
||||||
.get(id)
|
.get(id)
|
||||||
.or_else(|| viewport.prev_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))
|
.map(|widget_rect| self.get_response(widget_rect))
|
||||||
}
|
}
|
||||||
|
|
@ -2626,12 +2637,27 @@ impl Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is an egui context menu open?
|
/// 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 {
|
pub fn is_context_menu_open(&self) -> bool {
|
||||||
self.data(|d| {
|
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_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
|
// 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).
|
/// 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) {
|
pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) {
|
||||||
self.memory_mut(|m| {
|
self.memory_mut(|m| {
|
||||||
if transform == TSTransform::IDENTITY {
|
if transform == TSTransform::IDENTITY {
|
||||||
|
|
@ -3166,13 +3193,14 @@ impl Context {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(format!(
|
ui.label(format!(
|
||||||
"{} menu bars",
|
"{} menu bars",
|
||||||
self.data(|d| d.count::<menu::BarState>())
|
self.data(|d| d.count::<crate::menu::BarState>())
|
||||||
));
|
));
|
||||||
if ui.button("Reset").clicked() {
|
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()
|
.clicked()
|
||||||
{
|
{
|
||||||
zoom_in(ui.ctx());
|
zoom_in(ui.ctx());
|
||||||
ui.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
|
|
@ -99,7 +98,6 @@ pub fn zoom_menu_buttons(ui: &mut Ui) {
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
zoom_out(ui.ctx());
|
zoom_out(ui.ctx());
|
||||||
ui.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
|
|
@ -110,6 +108,5 @@ pub fn zoom_menu_buttons(ui: &mut Ui) {
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
ui.ctx().set_zoom_factor(1.0);
|
ui.ctx().set_zoom_factor(1.0);
|
||||||
ui.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1333,6 +1333,18 @@ impl PointerState {
|
||||||
pub fn middle_down(&self) -> bool {
|
pub fn middle_down(&self) -> bool {
|
||||||
self.button_down(PointerButton::Middle)
|
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 {
|
impl InputState {
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,7 @@ pub mod layers;
|
||||||
mod layout;
|
mod layout;
|
||||||
pub mod load;
|
pub mod load;
|
||||||
mod memory;
|
mod memory;
|
||||||
|
#[deprecated = "Use `egui::containers::menu` instead"]
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
pub mod os;
|
pub mod os;
|
||||||
mod painter;
|
mod painter;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#![allow(deprecated)]
|
||||||
//! Menu bar functionality (very basic so far).
|
//! Menu bar functionality (very basic so far).
|
||||||
//!
|
//!
|
||||||
//! Usage:
|
//! Usage:
|
||||||
|
|
@ -146,7 +147,7 @@ pub fn menu_image_button<R>(
|
||||||
/// Opens on hover.
|
/// Opens on hover.
|
||||||
///
|
///
|
||||||
/// Returns `None` if the menu is not open.
|
/// Returns `None` if the menu is not open.
|
||||||
pub(crate) fn submenu_button<R>(
|
pub fn submenu_button<R>(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
parent_state: Arc<RwLock<MenuState>>,
|
parent_state: Arc<RwLock<MenuState>>,
|
||||||
title: impl Into<WidgetText>,
|
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";
|
pub(crate) const CONTEXT_MENU_ID_STR: &str = "__egui::context_menu";
|
||||||
|
|
||||||
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
||||||
pub(crate) fn context_menu(
|
pub fn context_menu(
|
||||||
response: &Response,
|
response: &Response,
|
||||||
add_contents: impl FnOnce(&mut Ui),
|
add_contents: impl FnOnce(&mut Ui),
|
||||||
) -> Option<InnerResponse<()>> {
|
) -> Option<InnerResponse<()>> {
|
||||||
|
|
@ -282,7 +283,7 @@ pub(crate) fn context_menu(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the context menu is opened for this widget.
|
/// 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 menu_id = Id::new(CONTEXT_MENU_ID_STR);
|
||||||
let bar_state = BarState::load(&response.ctx, menu_id);
|
let bar_state = BarState::load(&response.ctx, menu_id);
|
||||||
bar_state.is_menu_open(response.id)
|
bar_state.is_menu_open(response.id)
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ use std::{any::Any, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
emath::{Align, Pos2, Rect, Vec2},
|
emath::{Align, Pos2, Rect, Vec2},
|
||||||
menu, pass_state, Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense,
|
pass_state, Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip,
|
||||||
Tooltip, Ui, WidgetRect, WidgetText,
|
Ui, WidgetRect, WidgetText,
|
||||||
};
|
};
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -935,14 +935,14 @@ impl Response {
|
||||||
///
|
///
|
||||||
/// See also: [`Ui::menu_button`] and [`Ui::close`].
|
/// See also: [`Ui::menu_button`] and [`Ui::close`].
|
||||||
pub fn context_menu(&self, add_contents: impl FnOnce(&mut Ui)) -> Option<InnerResponse<()>> {
|
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.
|
/// Returns whether a context menu is currently open for this widget.
|
||||||
///
|
///
|
||||||
/// See [`Self::context_menu`].
|
/// See [`Self::context_menu`].
|
||||||
pub fn context_menu_opened(&self) -> bool {
|
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
|
/// 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)]
|
#![allow(clippy::if_same_then_else)]
|
||||||
|
|
||||||
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
|
||||||
|
|
||||||
use emath::Align;
|
use emath::Align;
|
||||||
use epaint::{text::FontTweak, CornerRadius, Shadow, Stroke};
|
use epaint::{text::FontTweak, CornerRadius, Shadow, Stroke};
|
||||||
|
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ecolor::Color32,
|
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.
|
/// Specifies the look and feel of egui.
|
||||||
///
|
///
|
||||||
/// You can change the visuals of a [`Ui`] with [`Ui::style_mut`]
|
/// 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 std::{any::Any, hash::Hash, sync::Arc};
|
||||||
|
|
||||||
use crate::close_tag::ClosableTag;
|
use crate::close_tag::ClosableTag;
|
||||||
|
use crate::containers::menu;
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
use crate::Stroke;
|
use crate::Stroke;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -15,8 +16,6 @@ use crate::{
|
||||||
epaint::text::Fonts,
|
epaint::text::Fonts,
|
||||||
grid,
|
grid,
|
||||||
layout::{Direction, Layout},
|
layout::{Direction, Layout},
|
||||||
menu,
|
|
||||||
menu::MenuState,
|
|
||||||
pass_state,
|
pass_state,
|
||||||
placer::Placer,
|
placer::Placer,
|
||||||
pos2, style,
|
pos2, style,
|
||||||
|
|
@ -99,7 +98,8 @@ pub struct Ui {
|
||||||
sizing_pass: bool,
|
sizing_pass: bool,
|
||||||
|
|
||||||
/// Indicates whether this Ui belongs to a Menu.
|
/// 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`].
|
/// The [`UiStack`] for this [`Ui`].
|
||||||
stack: Arc<UiStack>,
|
stack: Arc<UiStack>,
|
||||||
|
|
@ -1187,6 +1187,14 @@ impl Ui {
|
||||||
/// [`crate::Area`] e.g. will return true from it's [`Response::should_close`] method.
|
/// [`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.
|
/// 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) {
|
pub fn close(&self) {
|
||||||
let tag = self.stack.iter().find_map(|stack| {
|
let tag = self.stack.iter().find_map(|stack| {
|
||||||
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
|
/// 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.
|
/// `Collapsible`, [`Ui::close`] would close the `Collapsible` instead.
|
||||||
/// You can close the [`crate::Window`] by calling `ui.close_kind(UiKind::Window)`.
|
/// 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) {
|
pub fn close_kind(&self, ui_kind: UiKind) {
|
||||||
let tag = self
|
let tag = self
|
||||||
.stack
|
.stack
|
||||||
|
|
@ -1230,6 +1243,12 @@ impl Ui {
|
||||||
/// Only works if the [`Ui`] was created with [`UiBuilder::closable`].
|
/// Only works if the [`Ui`] was created with [`UiBuilder::closable`].
|
||||||
///
|
///
|
||||||
/// You can also check via this [`Ui`]'s [`Response::should_close`].
|
/// 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 {
|
pub fn should_close(&self) -> bool {
|
||||||
self.stack
|
self.stack
|
||||||
.info
|
.info
|
||||||
|
|
@ -1237,6 +1256,22 @@ impl Ui {
|
||||||
.get_downcast(ClosableTag::NAME)
|
.get_downcast(ClosableTag::NAME)
|
||||||
.is_some_and(|tag: &ClosableTag| tag.should_close())
|
.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?
|
/// # Allocating space: where do I put my widgets?
|
||||||
|
|
@ -2977,7 +3012,11 @@ impl Ui {
|
||||||
self.close_kind(UiKind::Menu);
|
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;
|
self.menu_state = menu_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3004,11 +3043,12 @@ impl Ui {
|
||||||
title: impl Into<WidgetText>,
|
title: impl Into<WidgetText>,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> InnerResponse<Option<R>> {
|
) -> InnerResponse<Option<R>> {
|
||||||
if let Some(menu_state) = self.menu_state.clone() {
|
let (response, inner) = if menu::is_in_menu(self) {
|
||||||
menu::submenu_button(self, menu_state, title, add_contents)
|
menu::SubMenuButton::new(title).ui(self, add_contents)
|
||||||
} else {
|
} 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.
|
/// 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>>,
|
image: impl Into<Image<'a>>,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> InnerResponse<Option<R>> {
|
) -> InnerResponse<Option<R>> {
|
||||||
if let Some(menu_state) = self.menu_state.clone() {
|
let (response, inner) = if menu::is_in_menu(self) {
|
||||||
menu::submenu_button(self, menu_state, String::new(), add_contents)
|
menu::SubMenuButton::from_button(
|
||||||
|
Button::image(image).right_text(menu::SubMenuButton::RIGHT_ARROW),
|
||||||
|
)
|
||||||
|
.ui(self, add_contents)
|
||||||
} else {
|
} 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.
|
/// 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>,
|
title: impl Into<WidgetText>,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> InnerResponse<Option<R>> {
|
) -> InnerResponse<Option<R>> {
|
||||||
if let Some(menu_state) = self.menu_state.clone() {
|
let (response, inner) = if menu::is_in_menu(self) {
|
||||||
menu::submenu_button(self, menu_state, title, add_contents)
|
menu::SubMenuButton::from_button(
|
||||||
|
Button::image_and_text(image, title).right_text(menu::SubMenuButton::RIGHT_ARROW),
|
||||||
|
)
|
||||||
|
.ui(self, add_contents)
|
||||||
} else {
|
} 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> {
|
pub struct Button<'a> {
|
||||||
image: Option<Image<'a>>,
|
image: Option<Image<'a>>,
|
||||||
text: Option<WidgetText>,
|
text: Option<WidgetText>,
|
||||||
shortcut_text: WidgetText,
|
right_text: WidgetText,
|
||||||
wrap_mode: Option<TextWrapMode>,
|
wrap_mode: Option<TextWrapMode>,
|
||||||
|
|
||||||
/// None means default for interact
|
/// None means default for interact
|
||||||
|
|
@ -61,7 +61,7 @@ impl<'a> Button<'a> {
|
||||||
Self {
|
Self {
|
||||||
text,
|
text,
|
||||||
image,
|
image,
|
||||||
shortcut_text: Default::default(),
|
right_text: Default::default(),
|
||||||
wrap_mode: None,
|
wrap_mode: None,
|
||||||
fill: None,
|
fill: None,
|
||||||
stroke: 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`).
|
/// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
|
||||||
///
|
///
|
||||||
/// The text can be created with [`crate::Context::format_shortcut`].
|
/// The text can be created with [`crate::Context::format_shortcut`].
|
||||||
|
///
|
||||||
|
/// See also [`Self::right_text`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn shortcut_text(mut self, shortcut_text: impl Into<WidgetText>) -> Self {
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,7 +209,7 @@ impl Widget for Button<'_> {
|
||||||
let Button {
|
let Button {
|
||||||
text,
|
text,
|
||||||
image,
|
image,
|
||||||
shortcut_text,
|
right_text,
|
||||||
wrap_mode,
|
wrap_mode,
|
||||||
fill,
|
fill,
|
||||||
stroke,
|
stroke,
|
||||||
|
|
@ -239,16 +248,16 @@ impl Widget for Button<'_> {
|
||||||
Vec2::ZERO
|
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;
|
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
|
||||||
if image.is_some() {
|
if image.is_some() {
|
||||||
text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
|
text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: we don't wrap the shortcut text
|
// Note: we don't wrap the right text
|
||||||
let shortcut_galley = (!shortcut_text.is_empty()).then(|| {
|
let right_galley = (!right_text.is_empty()).then(|| {
|
||||||
shortcut_text.into_galley(
|
right_text.into_galley(
|
||||||
ui,
|
ui,
|
||||||
Some(TextWrapMode::Extend),
|
Some(TextWrapMode::Extend),
|
||||||
f32::INFINITY,
|
f32::INFINITY,
|
||||||
|
|
@ -256,9 +265,9 @@ impl Widget for Button<'_> {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(shortcut_galley) = &shortcut_galley {
|
if let Some(right_galley) = &right_galley {
|
||||||
// Leave space for the shortcut text:
|
// Leave space for the right text:
|
||||||
text_wrap_width -= gap_before_shortcut_text + shortcut_galley.size().x;
|
text_wrap_width -= gap_before_right_text + right_galley.size().x;
|
||||||
}
|
}
|
||||||
|
|
||||||
let galley =
|
let galley =
|
||||||
|
|
@ -276,9 +285,9 @@ impl Widget for Button<'_> {
|
||||||
desired_size.x += galley.size().x;
|
desired_size.x += galley.size().x;
|
||||||
desired_size.y = desired_size.y.max(galley.size().y);
|
desired_size.y = desired_size.y.max(galley.size().y);
|
||||||
}
|
}
|
||||||
if let Some(shortcut_galley) = &shortcut_galley {
|
if let Some(right_galley) = &right_galley {
|
||||||
desired_size.x += gap_before_shortcut_text + shortcut_galley.size().x;
|
desired_size.x += gap_before_right_text + right_galley.size().x;
|
||||||
desired_size.y = desired_size.y.max(shortcut_galley.size().y);
|
desired_size.y = desired_size.y.max(right_galley.size().y);
|
||||||
}
|
}
|
||||||
desired_size += 2.0 * button_padding;
|
desired_size += 2.0 * button_padding;
|
||||||
if !small {
|
if !small {
|
||||||
|
|
@ -335,7 +344,7 @@ impl Widget for Button<'_> {
|
||||||
.layout()
|
.layout()
|
||||||
.align_size_within_rect(image_size, rect.shrink2(button_padding))
|
.align_size_within_rect(image_size, rect.shrink2(button_padding))
|
||||||
.min;
|
.min;
|
||||||
if galley.is_some() || shortcut_galley.is_some() {
|
if galley.is_some() || right_galley.is_some() {
|
||||||
image_pos.x = cursor_x;
|
image_pos.x = cursor_x;
|
||||||
}
|
}
|
||||||
let image_rect = Rect::from_min_size(image_pos, image_size);
|
let image_rect = Rect::from_min_size(image_pos, image_size);
|
||||||
|
|
@ -369,27 +378,25 @@ impl Widget for Button<'_> {
|
||||||
.layout()
|
.layout()
|
||||||
.align_size_within_rect(galley.size(), rect.shrink2(button_padding))
|
.align_size_within_rect(galley.size(), rect.shrink2(button_padding))
|
||||||
.min;
|
.min;
|
||||||
if image.is_some() || shortcut_galley.is_some() {
|
if image.is_some() || right_galley.is_some() {
|
||||||
text_pos.x = cursor_x;
|
text_pos.x = cursor_x;
|
||||||
}
|
}
|
||||||
ui.painter().galley(text_pos, galley, visuals.text_color());
|
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
|
// Always align to the right
|
||||||
let layout = if ui.layout().is_horizontal() {
|
let layout = if ui.layout().is_horizontal() {
|
||||||
ui.layout().with_main_align(Align::Max)
|
ui.layout().with_main_align(Align::Max)
|
||||||
} else {
|
} else {
|
||||||
ui.layout().with_cross_align(Align::Max)
|
ui.layout().with_cross_align(Align::Max)
|
||||||
};
|
};
|
||||||
let shortcut_text_pos = layout
|
let right_text_pos = layout
|
||||||
.align_size_within_rect(shortcut_galley.size(), rect.shrink2(button_padding))
|
.align_size_within_rect(right_galley.size(), rect.shrink2(button_padding))
|
||||||
.min;
|
.min;
|
||||||
ui.painter().galley(
|
|
||||||
shortcut_text_pos,
|
ui.painter()
|
||||||
shortcut_galley,
|
.galley(right_text_pos, right_galley, visuals.text_color());
|
||||||
ui.visuals().weak_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 std::collections::BTreeSet;
|
||||||
|
|
||||||
use egui::{Context, Modifiers, ScrollArea, Ui};
|
|
||||||
|
|
||||||
use super::About;
|
use super::About;
|
||||||
use crate::is_mobile;
|
use crate::is_mobile;
|
||||||
use crate::Demo;
|
use crate::Demo;
|
||||||
use crate::View;
|
use crate::View;
|
||||||
|
use egui::containers::menu;
|
||||||
|
use egui::style::StyleModifier;
|
||||||
|
use egui::{Context, Modifiers, ScrollArea, Ui};
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
struct DemoGroup {
|
struct DemoGroup {
|
||||||
|
|
@ -65,7 +65,6 @@ impl Default for DemoGroups {
|
||||||
Box::<super::paint_bezier::PaintBezier>::default(),
|
Box::<super::paint_bezier::PaintBezier>::default(),
|
||||||
Box::<super::code_editor::CodeEditor>::default(),
|
Box::<super::code_editor::CodeEditor>::default(),
|
||||||
Box::<super::code_example::CodeExample>::default(),
|
Box::<super::code_example::CodeExample>::default(),
|
||||||
Box::<super::context_menu::ContextMenus>::default(),
|
|
||||||
Box::<super::dancing_strings::DancingStrings>::default(),
|
Box::<super::dancing_strings::DancingStrings>::default(),
|
||||||
Box::<super::drag_and_drop::DragAndDropDemo>::default(),
|
Box::<super::drag_and_drop::DragAndDropDemo>::default(),
|
||||||
Box::<super::extra_viewport::ExtraViewport>::default(),
|
Box::<super::extra_viewport::ExtraViewport>::default(),
|
||||||
|
|
@ -227,29 +226,27 @@ impl DemoWindows {
|
||||||
|
|
||||||
fn mobile_top_bar(&mut self, ctx: &Context) {
|
fn mobile_top_bar(&mut self, ctx: &Context) {
|
||||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||||
egui::menu::bar(ui, |ui| {
|
menu::Bar::new()
|
||||||
let font_size = 16.5;
|
.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.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);
|
||||||
self.demo_list_ui(ui);
|
});
|
||||||
if ui.ui_contains_pointer() && ui.input(|i| i.pointer.any_click()) {
|
|
||||||
ui.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
use egui::special_emojis::GITHUB;
|
use egui::special_emojis::GITHUB;
|
||||||
ui.hyperlink_to(
|
ui.hyperlink_to(
|
||||||
egui::RichText::new("🦋").size(font_size),
|
egui::RichText::new("🦋").size(font_size),
|
||||||
"https://bsky.app/profile/ernerfeldt.bsky.social",
|
"https://bsky.app/profile/ernerfeldt.bsky.social",
|
||||||
);
|
);
|
||||||
ui.hyperlink_to(
|
ui.hyperlink_to(
|
||||||
egui::RichText::new(GITHUB).size(font_size),
|
egui::RichText::new(GITHUB).size(font_size),
|
||||||
"https://github.com/emilk/egui",
|
"https://github.com/emilk/egui",
|
||||||
);
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,7 +279,7 @@ impl DemoWindows {
|
||||||
});
|
});
|
||||||
|
|
||||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||||
egui::menu::bar(ui, |ui| {
|
menu::Bar::new().ui(ui, |ui| {
|
||||||
file_menu_button(ui);
|
file_menu_button(ui);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -345,7 +342,6 @@ fn file_menu_button(ui: &mut Ui) {
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
ui.ctx().memory_mut(|mem| mem.reset_areas());
|
ui.ctx().memory_mut(|mem| mem.reset_areas());
|
||||||
ui.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
|
|
@ -357,7 +353,6 @@ fn file_menu_button(ui: &mut Ui) {
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||||
ui.close();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
pub mod about;
|
pub mod about;
|
||||||
pub mod code_editor;
|
pub mod code_editor;
|
||||||
pub mod code_example;
|
pub mod code_example;
|
||||||
pub mod context_menu;
|
|
||||||
pub mod dancing_strings;
|
pub mod dancing_strings;
|
||||||
pub mod demo_app_windows;
|
pub mod demo_app_windows;
|
||||||
pub mod drag_and_drop;
|
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`].
|
/// Showcase [`Popup`].
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
|
|
@ -10,6 +15,7 @@ pub struct PopupsDemo {
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
close_behavior: PopupCloseBehavior,
|
close_behavior: PopupCloseBehavior,
|
||||||
popup_open: bool,
|
popup_open: bool,
|
||||||
|
checked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PopupsDemo {
|
impl PopupsDemo {
|
||||||
|
|
@ -28,6 +34,7 @@ impl Default for PopupsDemo {
|
||||||
gap: 4.0,
|
gap: 4.0,
|
||||||
close_behavior: PopupCloseBehavior::CloseOnClick,
|
close_behavior: PopupCloseBehavior::CloseOnClick,
|
||||||
popup_open: false,
|
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 {
|
impl crate::View for PopupsDemo {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
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())
|
let response = Frame::group(ui.style())
|
||||||
.inner_margin(vec2(0.0, 25.0))
|
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
|
ui.set_width(ui.available_width());
|
||||||
ui.vertical_centered(|ui| ui.button("Click, right-click and hover me!"))
|
ui.vertical_centered(|ui| ui.button("Click, right-click and hover me!"))
|
||||||
.inner
|
.inner
|
||||||
})
|
})
|
||||||
.inner;
|
.inner;
|
||||||
|
|
||||||
self.apply_options(Popup::menu(&response).id(Id::new("menu")))
|
self.apply_options(Popup::menu(&response).id(Id::new("menu")))
|
||||||
.show(|ui| {
|
.show(|ui| nested_menus(ui, &mut self.checked));
|
||||||
_ = ui.button("Menu item 1");
|
|
||||||
_ = ui.button("Menu item 2");
|
|
||||||
|
|
||||||
if ui.button("I always close the menu").clicked() {
|
|
||||||
ui.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.apply_options(Popup::context_menu(&response).id(Id::new("context_menu")))
|
self.apply_options(Popup::context_menu(&response).id(Id::new("context_menu")))
|
||||||
.show(|ui| {
|
.show(|ui| nested_menus(ui, &mut self.checked));
|
||||||
_ = ui.button("Context menu item 1");
|
|
||||||
_ = ui.button("Context menu item 2");
|
|
||||||
});
|
|
||||||
|
|
||||||
if self.popup_open {
|
if self.popup_open {
|
||||||
self.apply_options(Popup::from_response(&response).id(Id::new("popup")))
|
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!");
|
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.vertical_centered(|ui| {
|
||||||
ui.add(crate::egui_github_link_file!());
|
ui.add(crate::egui_github_link_file!());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4806984f9c801a054cea80b89664293680abaa57cf0a95cf9682f111e3794fc1
|
oid sha256:17dc2a2f98d4cc52f6c6337dcc2e40f22d7310a933de91bb60576b893926193c
|
||||||
size 25080
|
size 58674
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ document-features = { workspace = true, optional = true }
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
egui = { workspace = true, features = ["default_fonts"] }
|
egui = { workspace = true, features = ["default_fonts"] }
|
||||||
image = { workspace = true, features = ["png"] }
|
image = { workspace = true, features = ["png"] }
|
||||||
|
egui_extras = { workspace = true, features = ["image"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
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