1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 14:20:04 -04:00
Files
egui/crates/egui/src/containers/popup.rs
Lucas Meurer d3c1ac3798 Fix context_menu not closing when clicking widget (#5799)
The rerun timeline context menu wouldn't close when clicking outside,
this fixes it
2025-03-18 11:32:18 +01:00

617 lines
18 KiB
Rust

use crate::containers::menu::{menu_style, MenuConfig, MenuState};
use crate::style::StyleModifier;
use crate::{
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
Sense, Ui, UiKind, UiStackInfo,
};
use emath::{vec2, Align, Pos2, Rect, RectAlign, Vec2};
use std::iter::once;
/// What should we anchor the popup to?
///
/// The final position for the popup will be calculated based on [`RectAlign`]
/// and can be customized with [`Popup::align`] and [`Popup::align_alternatives`].
/// [`PopupAnchor`] is the parent rect of [`RectAlign`].
///
/// For [`PopupAnchor::Pointer`], [`PopupAnchor::PointerFixed`] and [`PopupAnchor::Position`],
/// the rect will be derived via [`Rect::from_pos`] (so a zero-sized rect at the given position).
///
/// The rect should be in global coordinates. `PopupAnchor::from(&response)` will automatically
/// do this conversion.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PopupAnchor {
/// Show the popup relative to some parent [`Rect`].
ParentRect(Rect),
/// Show the popup relative to the mouse pointer.
Pointer,
/// Remember the mouse position and show the popup relative to that (like a context menu).
PointerFixed,
/// Show the popup relative to a specific position.
Position(Pos2),
}
impl From<Rect> for PopupAnchor {
fn from(rect: Rect) -> Self {
Self::ParentRect(rect)
}
}
impl From<Pos2> for PopupAnchor {
fn from(pos: Pos2) -> Self {
Self::Position(pos)
}
}
impl From<&Response> for PopupAnchor {
fn from(response: &Response) -> Self {
// We use interact_rect so we don't show the popup relative to some clipped point
let mut widget_rect = response.interact_rect;
if let Some(to_global) = response.ctx.layer_transform_to_global(response.layer_id) {
widget_rect = to_global * widget_rect;
}
Self::ParentRect(widget_rect)
}
}
impl PopupAnchor {
/// Get the rect the popup should be shown relative to.
/// Returns `Rect::from_pos` for [`PopupAnchor::Pointer`], [`PopupAnchor::PointerFixed`]
/// and [`PopupAnchor::Position`] (so the rect will be zero-sized).
pub fn rect(self, popup_id: Id, ctx: &Context) -> Option<Rect> {
match self {
Self::ParentRect(rect) => Some(rect),
Self::Pointer => ctx.pointer_hover_pos().map(Rect::from_pos),
Self::PointerFixed => ctx
.memory(|mem| mem.popup_position(popup_id))
.map(Rect::from_pos),
Self::Position(pos) => Some(Rect::from_pos(pos)),
}
}
}
/// Determines popup's close behavior
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub enum PopupCloseBehavior {
/// Popup will be closed on click anywhere, inside or outside the popup.
///
/// It is used in [`crate::ComboBox`] and in [`crate::containers::menu`]s.
#[default]
CloseOnClick,
/// Popup will be closed if the click happened somewhere else
/// but in the popup's body
CloseOnClickOutside,
/// Clicks will be ignored. Popup might be closed manually by calling [`crate::Memory::close_popup`]
/// or by pressing the escape button
IgnoreClicks,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SetOpenCommand {
/// Set the open state to the given value
Bool(bool),
/// Toggle the open state
Toggle,
}
impl From<bool> for SetOpenCommand {
fn from(b: bool) -> Self {
Self::Bool(b)
}
}
/// How do we determine if the popup should be open or closed
enum OpenKind<'a> {
/// Always open
Open,
/// Always closed
Closed,
/// Open if the bool is true
Bool(&'a mut bool),
/// Store the open state via [`crate::Memory`]
Memory { set: Option<SetOpenCommand> },
}
impl OpenKind<'_> {
/// Returns `true` if the popup should be open
fn is_open(&self, id: Id, ctx: &Context) -> bool {
match self {
OpenKind::Open => true,
OpenKind::Closed => false,
OpenKind::Bool(open) => **open,
OpenKind::Memory { .. } => ctx.memory(|mem| mem.is_popup_open(id)),
}
}
}
/// Is the popup a popup, tooltip or menu?
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PopupKind {
Popup,
Tooltip,
Menu,
}
impl PopupKind {
/// Returns the order to be used with this kind.
pub fn order(self) -> Order {
match self {
Self::Tooltip => Order::Tooltip,
Self::Menu | Self::Popup => Order::Foreground,
}
}
}
impl From<PopupKind> for UiKind {
fn from(kind: PopupKind) -> Self {
match kind {
PopupKind::Popup => Self::Popup,
PopupKind::Tooltip => Self::Tooltip,
PopupKind::Menu => Self::Menu,
}
}
}
pub struct Popup<'a> {
id: Id,
ctx: Context,
anchor: PopupAnchor,
rect_align: RectAlign,
alternative_aligns: Option<&'a [RectAlign]>,
layer_id: LayerId,
open_kind: OpenKind<'a>,
close_behavior: PopupCloseBehavior,
info: Option<UiStackInfo>,
kind: PopupKind,
/// Gap between the anchor and the popup
gap: f32,
/// Used later depending on close behavior
widget_clicked_elsewhere: bool,
/// Default width passed to the Area
width: Option<f32>,
sense: Sense,
layout: Layout,
frame: Option<Frame>,
style: StyleModifier,
}
impl<'a> Popup<'a> {
/// Create a new popup
pub fn new(id: Id, ctx: Context, anchor: impl Into<PopupAnchor>, layer_id: LayerId) -> Self {
Self {
id,
ctx,
anchor: anchor.into(),
open_kind: OpenKind::Open,
close_behavior: PopupCloseBehavior::default(),
info: None,
kind: PopupKind::Popup,
layer_id,
rect_align: RectAlign::BOTTOM_START,
alternative_aligns: None,
gap: 0.0,
widget_clicked_elsewhere: false,
width: None,
sense: Sense::click(),
layout: Layout::default(),
frame: None,
style: StyleModifier::default(),
}
}
/// Set the kind of the popup. Used for [`Area::kind`] and [`Area::order`].
#[inline]
pub fn kind(mut self, kind: PopupKind) -> Self {
self.kind = kind;
self
}
/// Set the [`UiStackInfo`] of the popup's [`Ui`].
#[inline]
pub fn info(mut self, info: UiStackInfo) -> Self {
self.info = Some(info);
self
}
/// Set the [`RectAlign`] of the popup relative to the [`PopupAnchor`].
/// This is the default position, and will be used if it fits.
/// See [`Self::align_alternatives`] for more on this.
#[inline]
pub fn align(mut self, position_align: RectAlign) -> Self {
self.rect_align = position_align;
self
}
/// Set alternative positions to try if the default one doesn't fit. Set to an empty slice to
/// always use the position you set with [`Self::align`].
/// By default, this will try [`RectAlign::symmetries`] and then [`RectAlign::MENU_ALIGNS`].
#[inline]
pub fn align_alternatives(mut self, alternatives: &'a [RectAlign]) -> Self {
self.alternative_aligns = Some(alternatives);
self
}
/// Show a popup relative to some widget.
/// The popup will be always open.
///
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
pub fn from_response(response: &Response) -> Self {
let mut popup = Self::new(
response.id.with("popup"),
response.ctx.clone(),
response,
response.layer_id,
);
popup.widget_clicked_elsewhere = response.clicked_elsewhere();
popup
}
/// Show a popup when the widget was clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
pub fn menu(response: &Response) -> Self {
Self::from_response(response)
.open_memory(response.clicked().then_some(SetOpenCommand::Toggle))
.kind(PopupKind::Menu)
.layout(Layout::top_down_justified(Align::Min))
.style(menu_style)
.gap(0.0)
}
/// Show a context menu when the widget was secondary clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
/// In contrast to [`Self::menu`], this will open at the pointer position.
pub fn context_menu(response: &Response) -> Self {
Self::menu(response)
.open_memory(if response.secondary_clicked() {
Some(SetOpenCommand::Bool(true))
} else if response.clicked() {
// Explicitly close the menu if the widget was clicked
// Without this, the context menu would stay open if the user clicks the widget
Some(SetOpenCommand::Bool(false))
} else {
None
})
.at_pointer_fixed()
}
/// Force the popup to be open or closed.
#[inline]
pub fn open(mut self, open: bool) -> Self {
self.open_kind = if open {
OpenKind::Open
} else {
OpenKind::Closed
};
self
}
/// Store the open state via [`crate::Memory`].
/// You can set the state via the first [`SetOpenCommand`] param.
#[inline]
pub fn open_memory(mut self, set_state: impl Into<Option<SetOpenCommand>>) -> Self {
self.open_kind = OpenKind::Memory {
set: set_state.into(),
};
self
}
/// Store the open state via a mutable bool.
#[inline]
pub fn open_bool(mut self, open: &'a mut bool) -> Self {
self.open_kind = OpenKind::Bool(open);
self
}
/// Set the close behavior of the popup.
///
/// This will do nothing if [`Popup::open`] was called.
#[inline]
pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
self.close_behavior = close_behavior;
self
}
/// Show the popup relative to the pointer.
#[inline]
pub fn at_pointer(mut self) -> Self {
self.anchor = PopupAnchor::Pointer;
self
}
/// Remember the pointer position at the time of opening the popup, and show the popup
/// relative to that.
#[inline]
pub fn at_pointer_fixed(mut self) -> Self {
self.anchor = PopupAnchor::PointerFixed;
self
}
/// Show the popup relative to a specific position.
#[inline]
pub fn at_position(mut self, position: Pos2) -> Self {
self.anchor = PopupAnchor::Position(position);
self
}
/// Show the popup relative to the given [`PopupAnchor`].
#[inline]
pub fn anchor(mut self, anchor: impl Into<PopupAnchor>) -> Self {
self.anchor = anchor.into();
self
}
/// Set the gap between the anchor and the popup.
#[inline]
pub fn gap(mut self, gap: f32) -> Self {
self.gap = gap;
self
}
/// Set the frame of the popup.
#[inline]
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
/// Set the sense of the popup.
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set the layout of the popup.
#[inline]
pub fn layout(mut self, layout: Layout) -> Self {
self.layout = layout;
self
}
/// The width that will be passed to [`Area::default_width`].
#[inline]
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
/// Set the id of the Area.
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = id;
self
}
/// Set the style for the popup contents.
///
/// Default:
/// - is [`menu_style`] for [`Self::menu`] and [`Self::context_menu`]
/// - is [`None`] otherwise
#[inline]
pub fn style(mut self, style: impl Into<StyleModifier>) -> Self {
self.style = style.into();
self
}
/// Get the [`Context`]
pub fn ctx(&self) -> &Context {
&self.ctx
}
/// Return the [`PopupAnchor`] of the popup.
pub fn get_anchor(&self) -> PopupAnchor {
self.anchor
}
/// Return the anchor rect of the popup.
///
/// Returns `None` if the anchor is [`PopupAnchor::Pointer`] and there is no pointer.
pub fn get_anchor_rect(&self) -> Option<Rect> {
self.anchor.rect(self.id, &self.ctx)
}
/// Get the expected rect the popup will be shown in.
///
/// Returns `None` if the popup wasn't shown before or anchor is `PopupAnchor::Pointer` and
/// there is no pointer.
pub fn get_popup_rect(&self) -> Option<Rect> {
let size = self.get_expected_size();
if let Some(size) = size {
self.get_anchor_rect()
.map(|anchor| self.get_best_align().align_rect(&anchor, size, self.gap))
} else {
None
}
}
/// Get the id of the popup.
pub fn get_id(&self) -> Id {
self.id
}
/// Is the popup open?
pub fn is_open(&self) -> bool {
match &self.open_kind {
OpenKind::Open => true,
OpenKind::Closed => false,
OpenKind::Bool(open) => **open,
OpenKind::Memory { .. } => self.ctx.memory(|mem| mem.is_popup_open(self.id)),
}
}
/// Get the expected size of the popup.
pub fn get_expected_size(&self) -> Option<Vec2> {
AreaState::load(&self.ctx, self.id).and_then(|area| area.size)
}
/// Calculate the best alignment for the popup, based on the last size and screen rect.
pub fn get_best_align(&self) -> RectAlign {
let expected_popup_size = self
.get_expected_size()
.unwrap_or(vec2(self.width.unwrap_or(0.0), 0.0));
let Some(anchor_rect) = self.anchor.rect(self.id, &self.ctx) else {
return self.rect_align;
};
RectAlign::find_best_align(
#[allow(clippy::iter_on_empty_collections)]
once(self.rect_align).chain(
self.alternative_aligns
// Need the empty slice so the iters have the same type so we can unwrap_or
.map(|a| a.iter().copied().chain([].iter().copied()))
.unwrap_or(
self.rect_align
.symmetries()
.iter()
.copied()
.chain(RectAlign::MENU_ALIGNS.iter().copied()),
),
),
self.ctx.screen_rect(),
anchor_rect,
self.gap,
expected_popup_size,
)
}
/// Show the popup.
/// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
/// no pointer.
pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
let best_align = self.get_best_align();
let Popup {
id,
ctx,
anchor,
open_kind,
close_behavior,
kind,
info,
layer_id,
rect_align: _,
alternative_aligns: _,
gap,
widget_clicked_elsewhere,
width,
sense,
layout,
frame,
style,
} = self;
let hover_pos = ctx.pointer_hover_pos();
if let OpenKind::Memory { set, .. } = open_kind {
ctx.memory_mut(|mem| match set {
Some(SetOpenCommand::Bool(open)) => {
if open {
match self.anchor {
PopupAnchor::PointerFixed => {
mem.open_popup_at(id, hover_pos);
}
_ => mem.open_popup(id),
}
} else {
mem.close_popup();
}
}
Some(SetOpenCommand::Toggle) => {
mem.toggle_popup(id);
}
None => {}
});
}
if !open_kind.is_open(id, &ctx) {
return None;
}
if kind != PopupKind::Tooltip {
ctx.pass_state_mut(|fs| {
fs.layers
.entry(layer_id)
.or_default()
.open_popups
.insert(id)
});
}
let anchor_rect = anchor.rect(id, &ctx)?;
let (pivot, anchor) = best_align.pivot_pos(&anchor_rect, gap);
let mut area = Area::new(id)
.order(kind.order())
.pivot(pivot)
.fixed_pos(anchor)
.sense(sense)
.layout(layout)
.info(info.unwrap_or_else(|| {
UiStackInfo::new(kind.into()).with_tag_value(
MenuConfig::MENU_CONFIG_TAG,
MenuConfig::new()
.close_behavior(close_behavior)
.style(style.clone()),
)
}));
if let Some(width) = width {
area = area.default_width(width);
}
let frame = frame.unwrap_or_else(|| Frame::popup(&ctx.style()));
let mut response = area.show(&ctx, |ui| {
style.apply(ui.style_mut());
frame.show(ui, content).inner
});
let closed_by_click = match close_behavior {
PopupCloseBehavior::CloseOnClick => widget_clicked_elsewhere,
PopupCloseBehavior::CloseOnClickOutside => {
widget_clicked_elsewhere && response.response.clicked_elsewhere()
}
PopupCloseBehavior::IgnoreClicks => false,
};
// If a submenu is open, the CloseBehavior is handled there
let is_any_submenu_open = !MenuState::is_deepest_sub_menu(&response.response.ctx, id);
let should_close = (!is_any_submenu_open && closed_by_click)
|| ctx.input(|i| i.key_pressed(Key::Escape))
|| response.response.should_close();
if should_close {
response.response.set_close();
}
match open_kind {
OpenKind::Open | OpenKind::Closed => {}
OpenKind::Bool(open) => {
if should_close {
*open = false;
}
}
OpenKind::Memory { .. } => {
if should_close {
ctx.memory_mut(|mem| mem.close_popup());
}
}
}
Some(response)
}
}