use super::{
style::WidgetVisuals, Align, Context, Id, InnerResponse, PointerState, Pos2, Rect, Response,
Sense, TextStyle, Ui, Vec2,
};
use crate::{
epaint, vec2,
widgets::{Button, ImageButton},
Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt, Order, Stroke, Style, TextWrapMode,
UiKind, WidgetText,
};
use epaint::mutex::RwLock;
use std::sync::Arc;
#[derive(Clone, Default)]
pub struct BarState {
open_menu: MenuRootManager,
}
impl BarState {
pub fn load(ctx: &Context, bar_id: Id) -> Self {
ctx.data_mut(|d| d.get_temp::<Self>(bar_id).unwrap_or_default())
}
pub fn store(self, ctx: &Context, bar_id: Id) {
ctx.data_mut(|d| d.insert_temp(bar_id, self));
}
pub fn bar_menu<R>(
&mut self,
button: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
MenuRoot::stationary_click_interaction(button, &mut self.open_menu);
self.open_menu.show(button, add_contents)
}
pub(crate) fn has_root(&self) -> bool {
self.open_menu.inner.is_some()
}
}
impl std::ops::Deref for BarState {
type Target = MenuRootManager;
fn deref(&self) -> &Self::Target {
&self.open_menu
}
}
impl std::ops::DerefMut for BarState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.open_menu
}
}
fn set_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.hovered.bg_stroke = Stroke::NONE;
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
}
pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
ui.horizontal(|ui| {
set_menu_style(ui.style_mut());
let height = ui.spacing().interact_size.y;
ui.set_min_size(vec2(ui.available_width(), height));
add_contents(ui)
})
}
pub fn menu_button<R>(
ui: &mut Ui,
title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_impl(ui, title, Box::new(add_contents))
}
pub fn menu_custom_button<R>(
ui: &mut Ui,
button: Button<'_>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_button_impl(ui, button, Box::new(add_contents))
}
#[deprecated = "Use `menu_custom_button` instead"]
pub fn menu_image_button<R>(
ui: &mut Ui,
image_button: ImageButton<'_>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_button_impl(
ui,
Button::image(image_button.image),
Box::new(add_contents),
)
}
pub(crate) fn submenu_button<R>(
ui: &mut Ui,
parent_state: Arc<RwLock<MenuState>>,
title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
SubMenu::new(parent_state, title).show(ui, add_contents)
}
fn menu_popup<'c, R>(
ctx: &Context,
parent_layer: LayerId,
menu_state_arc: &Arc<RwLock<MenuState>>,
menu_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R + 'c,
) -> InnerResponse<R> {
let pos = {
let mut menu_state = menu_state_arc.write();
menu_state.entry_count = 0;
menu_state.rect.min
};
let area_id = menu_id.with("__menu");
ctx.pass_state_mut(|fs| {
fs.layers
.entry(parent_layer)
.or_default()
.open_popups
.insert(area_id)
});
let area = Area::new(area_id)
.kind(UiKind::Menu)
.order(Order::Foreground)
.fixed_pos(pos)
.default_width(ctx.style().spacing.menu_width)
.sense(Sense::hover());
let mut sizing_pass = false;
let area_response = area.show(ctx, |ui| {
sizing_pass = ui.is_sizing_pass();
set_menu_style(ui.style_mut());
Frame::menu(ui.style())
.show(ui, |ui| {
ui.set_menu_state(Some(menu_state_arc.clone()));
ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents)
.inner
})
.inner
});
let area_rect = area_response.response.rect;
menu_state_arc.write().rect = if sizing_pass {
Rect::from_min_size(pos, area_rect.size())
} else {
area_rect
};
area_response
}
fn stationary_menu_impl<'c, R>(
ui: &mut Ui,
title: impl Into<WidgetText>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let title = title.into();
let bar_id = ui.id();
let menu_id = bar_id.with(title.text());
let mut bar_state = BarState::load(ui.ctx(), bar_id);
let mut button = Button::new(title);
if bar_state.open_menu.is_menu_open(menu_id) {
button = button.fill(ui.visuals().widgets.open.weak_bg_fill);
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
}
let button_response = ui.add(button);
let inner = bar_state.bar_menu(&button_response, add_contents);
bar_state.store(ui.ctx(), bar_id);
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
fn stationary_menu_button_impl<'c, R>(
ui: &mut Ui,
button: Button<'_>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let bar_id = ui.id();
let mut bar_state = BarState::load(ui.ctx(), bar_id);
let button_response = ui.add(button);
let inner = bar_state.bar_menu(&button_response, add_contents);
bar_state.store(ui.ctx(), bar_id);
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
pub(crate) const CONTEXT_MENU_ID_STR: &str = "__egui::context_menu";
pub(crate) fn context_menu(
response: &Response,
add_contents: impl FnOnce(&mut Ui),
) -> Option<InnerResponse<()>> {
let menu_id = Id::new(CONTEXT_MENU_ID_STR);
let mut bar_state = BarState::load(&response.ctx, menu_id);
MenuRoot::context_click_interaction(response, &mut bar_state);
let inner_response = bar_state.show(response, add_contents);
bar_state.store(&response.ctx, menu_id);
inner_response
}
pub(crate) fn context_menu_opened(response: &Response) -> bool {
let menu_id = Id::new(CONTEXT_MENU_ID_STR);
let bar_state = BarState::load(&response.ctx, menu_id);
bar_state.is_menu_open(response.id)
}
#[derive(Clone, Default)]
pub struct MenuRootManager {
inner: Option<MenuRoot>,
}
impl MenuRootManager {
pub fn show<R>(
&mut self,
button: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
if let Some(root) = self.inner.as_mut() {
let (menu_response, inner_response) = root.show(button, add_contents);
if menu_response.is_close() {
self.inner = None;
}
inner_response
} else {
None
}
}
fn is_menu_open(&self, id: Id) -> bool {
self.inner.as_ref().map(|m| m.id) == Some(id)
}
}
impl std::ops::Deref for MenuRootManager {
type Target = Option<MenuRoot>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for MenuRootManager {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
#[derive(Clone)]
pub struct MenuRoot {
pub menu_state: Arc<RwLock<MenuState>>,
pub id: Id,
}
impl MenuRoot {
pub fn new(position: Pos2, id: Id) -> Self {
Self {
menu_state: Arc::new(RwLock::new(MenuState::new(position))),
id,
}
}
pub fn show<R>(
&self,
button: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> (MenuResponse, Option<InnerResponse<R>>) {
if self.id == button.id {
let inner_response = menu_popup(
&button.ctx,
button.layer_id,
&self.menu_state,
self.id,
add_contents,
);
let menu_state = self.menu_state.read();
let escape_pressed = button.ctx.input(|i| i.key_pressed(Key::Escape));
if menu_state.response.is_close() || escape_pressed {
return (MenuResponse::Close, Some(inner_response));
}
}
(MenuResponse::Stay, None)
}
fn stationary_interaction(button: &Response, root: &mut MenuRootManager) -> MenuResponse {
let id = button.id;
if (button.clicked() && root.is_menu_open(id))
|| button.ctx.input(|i| i.key_pressed(Key::Escape))
{
return MenuResponse::Close;
} else if (button.clicked() && !root.is_menu_open(id))
|| (button.hovered() && root.is_some())
{
let mut pos = button.rect.left_bottom();
let menu_frame = Frame::menu(&button.ctx.style());
pos.x -= menu_frame.total_margin().left; pos.y += button.ctx.style().spacing.menu_spacing;
if let Some(root) = root.inner.as_mut() {
let menu_rect = root.menu_state.read().rect;
let screen_rect = button.ctx.input(|i| i.screen_rect);
if pos.y + menu_rect.height() > screen_rect.max.y {
pos.y = screen_rect.max.y - menu_rect.height() - button.rect.height();
}
if pos.x + menu_rect.width() > screen_rect.max.x {
pos.x = screen_rect.max.x - menu_rect.width();
}
}
if let Some(transform) = button
.ctx
.memory(|m| m.layer_transforms.get(&button.layer_id).copied())
{
pos = transform * pos;
}
return MenuResponse::Create(pos, id);
} else if button
.ctx
.input(|i| i.pointer.any_pressed() && i.pointer.primary_down())
{
if let Some(pos) = button.ctx.input(|i| i.pointer.interact_pos()) {
if let Some(root) = root.inner.as_mut() {
if root.id == id {
let in_menu = root.menu_state.read().area_contains(pos);
if !in_menu {
return MenuResponse::Close;
}
}
}
}
}
MenuResponse::Stay
}
pub fn context_interaction(response: &Response, root: &mut Option<Self>) -> MenuResponse {
let response = response.interact(Sense::click());
let hovered = response.hovered();
let secondary_clicked = response.secondary_clicked();
response.ctx.input(|input| {
let pointer = &input.pointer;
if let Some(pos) = pointer.interact_pos() {
let mut in_old_menu = false;
let mut destroy = false;
if let Some(root) = root {
in_old_menu = root.menu_state.read().area_contains(pos);
destroy = !in_old_menu && pointer.any_pressed() && root.id == response.id;
}
if !in_old_menu {
if hovered && secondary_clicked {
return MenuResponse::Create(pos, response.id);
} else if destroy || hovered && pointer.primary_down() {
return MenuResponse::Close;
}
}
}
MenuResponse::Stay
})
}
pub fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
match menu_response {
MenuResponse::Create(pos, id) => {
root.inner = Some(Self::new(pos, id));
}
MenuResponse::Close => root.inner = None,
MenuResponse::Stay => {}
}
}
pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager) {
let menu_response = Self::context_interaction(response, root);
Self::handle_menu_response(root, menu_response);
}
pub fn stationary_click_interaction(button: &Response, root: &mut MenuRootManager) {
let menu_response = Self::stationary_interaction(button, root);
Self::handle_menu_response(root, menu_response);
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum MenuResponse {
Close,
Stay,
Create(Pos2, Id),
}
impl MenuResponse {
pub fn is_close(&self) -> bool {
*self == Self::Close
}
}
pub struct SubMenuButton {
text: WidgetText,
icon: WidgetText,
index: usize,
}
impl SubMenuButton {
fn new(text: impl Into<WidgetText>, icon: impl Into<WidgetText>, index: usize) -> Self {
Self {
text: text.into(),
icon: icon.into(),
index,
}
}
fn visuals<'a>(
ui: &'a Ui,
response: &Response,
menu_state: &MenuState,
sub_id: Id,
) -> &'a WidgetVisuals {
if menu_state.is_open(sub_id) && !response.hovered() {
&ui.style().visuals.widgets.open
} else {
ui.style().interact(response)
}
}
#[inline]
pub fn icon(mut self, icon: impl Into<WidgetText>) -> Self {
self.icon = icon.into();
self
}
pub(crate) fn show(self, ui: &mut Ui, menu_state: &MenuState, sub_id: Id) -> Response {
let Self { text, icon, .. } = self;
let text_style = TextStyle::Button;
let sense = Sense::click();
let text_icon_gap = ui.spacing().item_spacing.x;
let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + button_padding;
let text_available_width = ui.available_width() - total_extra.x;
let text_galley = text.into_galley(
ui,
Some(TextWrapMode::Wrap),
text_available_width,
text_style.clone(),
);
let icon_available_width = text_available_width - text_galley.size().x;
let icon_galley = icon.into_galley(
ui,
Some(TextWrapMode::Wrap),
icon_available_width,
text_style,
);
let text_and_icon_size = Vec2::new(
text_galley.size().x + text_icon_gap + icon_galley.size().x,
text_galley.size().y.max(icon_galley.size().y),
);
let mut desired_size = text_and_icon_size + 2.0 * button_padding;
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| {
crate::WidgetInfo::labeled(
crate::WidgetType::Button,
ui.is_enabled(),
text_galley.text(),
)
});
if ui.is_rect_visible(rect) {
let visuals = Self::visuals(ui, &response, menu_state, sub_id);
let text_pos = Align2::LEFT_CENTER
.align_size_within_rect(text_galley.size(), rect.shrink2(button_padding))
.min;
let icon_pos = Align2::RIGHT_CENTER
.align_size_within_rect(icon_galley.size(), rect.shrink2(button_padding))
.min;
if ui.visuals().button_frame {
ui.painter().rect_filled(
rect.expand(visuals.expansion),
visuals.rounding,
visuals.weak_bg_fill,
);
}
let text_color = visuals.text_color();
ui.painter().galley(text_pos, text_galley, text_color);
ui.painter().galley(icon_pos, icon_galley, text_color);
}
response
}
}
pub struct SubMenu {
button: SubMenuButton,
parent_state: Arc<RwLock<MenuState>>,
}
impl SubMenu {
fn new(parent_state: Arc<RwLock<MenuState>>, text: impl Into<WidgetText>) -> Self {
let index = parent_state.write().next_entry_index();
Self {
button: SubMenuButton::new(text, "⏵", index),
parent_state,
}
}
pub fn show<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
let sub_id = ui.id().with(self.button.index);
let response = self.button.show(ui, &self.parent_state.read(), sub_id);
self.parent_state
.write()
.submenu_button_interaction(ui, sub_id, &response);
let inner =
self.parent_state
.write()
.show_submenu(ui.ctx(), ui.layer_id(), sub_id, add_contents);
InnerResponse::new(inner, response)
}
}
pub struct MenuState {
sub_menu: Option<(Id, Arc<RwLock<MenuState>>)>,
pub rect: Rect,
pub response: MenuResponse,
entry_count: usize,
}
impl MenuState {
pub fn new(position: Pos2) -> Self {
Self {
rect: Rect::from_min_size(position, Vec2::ZERO),
sub_menu: None,
response: MenuResponse::Stay,
entry_count: 0,
}
}
pub fn close(&mut self) {
self.response = MenuResponse::Close;
}
fn show_submenu<R>(
&mut self,
ctx: &Context,
parent_layer: LayerId,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let (sub_response, response) = self.submenu(id).map(|sub| {
let inner_response = menu_popup(ctx, parent_layer, sub, id, add_contents);
(sub.read().response, inner_response.inner)
})?;
self.cascade_close_response(sub_response);
Some(response)
}
pub fn area_contains(&self, pos: Pos2) -> bool {
self.rect.contains(pos)
|| self
.sub_menu
.as_ref()
.map_or(false, |(_, sub)| sub.read().area_contains(pos))
}
fn next_entry_index(&mut self) -> usize {
self.entry_count += 1;
self.entry_count - 1
}
fn submenu_button_interaction(&mut self, ui: &Ui, sub_id: Id, button: &Response) {
let pointer = ui.input(|i| i.pointer.clone());
let open = self.is_open(sub_id);
if self.moving_towards_current_submenu(&pointer) {
ui.ctx().request_repaint();
} else if !open && button.hovered() {
let mut pos = button.rect.right_top();
pos.x = self.rect.right() + ui.spacing().menu_spacing;
pos.y -= Frame::menu(ui.style()).total_margin().top; self.open_submenu(sub_id, pos);
} else if open
&& ui.response().contains_pointer()
&& !button.hovered()
&& !self.hovering_current_submenu(&pointer)
{
self.close_submenu();
}
}
fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool {
if pointer.is_still() {
return false;
}
if let Some(sub_menu) = self.current_submenu() {
if let Some(pos) = pointer.hover_pos() {
let rect = sub_menu.read().rect;
return rect.intersects_ray(pos, pointer.direction().normalized());
}
}
false
}
fn hovering_current_submenu(&self, pointer: &PointerState) -> bool {
if let Some(sub_menu) = self.current_submenu() {
if let Some(pos) = pointer.hover_pos() {
return sub_menu.read().area_contains(pos);
}
}
false
}
fn cascade_close_response(&mut self, response: MenuResponse) {
if response.is_close() {
self.response = response;
}
}
fn is_open(&self, id: Id) -> bool {
self.sub_id() == Some(id)
}
fn sub_id(&self) -> Option<Id> {
self.sub_menu.as_ref().map(|(id, _)| *id)
}
fn current_submenu(&self) -> Option<&Arc<RwLock<Self>>> {
self.sub_menu.as_ref().map(|(_, sub)| sub)
}
fn submenu(&self, id: Id) -> Option<&Arc<RwLock<Self>>> {
self.sub_menu
.as_ref()
.and_then(|(k, sub)| if id == *k { Some(sub) } else { None })
}
fn open_submenu(&mut self, id: Id, pos: Pos2) {
if !self.is_open(id) {
self.sub_menu = Some((id, Arc::new(RwLock::new(Self::new(pos)))));
}
}
fn close_submenu(&mut self) {
self.sub_menu = None;
}
}