1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Popup-in-popup (draft)

This commit is contained in:
lucasmerlin
2026-01-09 12:01:37 +01:00
parent f8e763378a
commit 5d2ddbfb15
9 changed files with 201 additions and 78 deletions

View File

@@ -521,8 +521,8 @@ impl SubMenu {
// Only edge case is the user hovering this submenu's button, so we also check // 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). // if we clicked outside the parent menu (which we luckily have access to here).
let clicked_outside = is_deepest_submenu let clicked_outside = is_deepest_submenu
&& popup_response.response.clicked_elsewhere() && popup_response.response.clicked_elsewhere_excluding_child_layers()
&& menu_root_response.clicked_elsewhere(); && menu_root_response.clicked_elsewhere_excluding_child_layers();
// We never automatically close when a submenu button is clicked, (so menus work // We never automatically close when a submenu button is clicked, (so menus work
// on touch devices) // on touch devices)

View File

@@ -513,19 +513,19 @@ impl<'a> Popup<'a> {
if open { if open {
match self.anchor { match self.anchor {
PopupAnchor::PointerFixed => { PopupAnchor::PointerFixed => {
self.ctx.memory_mut(|mem| mem.open_popup_at(id, hover_pos)); self.ctx.memory_mut(|mem| mem.open_popup_at(self.layer_id, id, hover_pos));
} }
_ => Popup::open_id(&self.ctx, id), _ => Popup::open_id(&self.ctx, self.layer_id, id),
} }
} else { } else {
Self::close_id(&self.ctx, id); Self::close_id(&self.ctx, id);
} }
} }
Some(SetOpenCommand::Toggle) => { Some(SetOpenCommand::Toggle) => {
Self::toggle_id(&self.ctx, id); Self::toggle_id(&self.ctx, self.layer_id, id);
} }
None => { None => {
self.ctx.memory_mut(|mem| mem.keep_popup_open(id)); self.ctx.memory_mut(|mem| mem.keep_popup_open(self.layer_id, id));
} }
} }
} }
@@ -570,7 +570,7 @@ 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(kind.order()) .order(layer_id.order)
.pivot(pivot) .pivot(pivot)
.fixed_pos(anchor) .fixed_pos(anchor)
.sense(sense) .sense(sense)
@@ -589,6 +589,7 @@ impl<'a> Popup<'a> {
} }
let mut response = area.show(&ctx, |ui| { let mut response = area.show(&ctx, |ui| {
ui.set_sublayer(layer_id, ui.layer_id());
style.apply(ui.style_mut()); style.apply(ui.style_mut());
let frame = frame.unwrap_or_else(|| Frame::popup(ui.style())); let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
frame.show(ui, content).inner frame.show(ui, content).inner
@@ -600,7 +601,7 @@ impl<'a> Popup<'a> {
let closed_by_click = match close_behavior { let closed_by_click = match close_behavior {
PopupCloseBehavior::CloseOnClick => close_click, PopupCloseBehavior::CloseOnClick => close_click,
PopupCloseBehavior::CloseOnClickOutside => { PopupCloseBehavior::CloseOnClickOutside => {
close_click && response.response.clicked_elsewhere() close_click && response.response.clicked_elsewhere_excluding_child_layers()
} }
PopupCloseBehavior::IgnoreClicks => false, PopupCloseBehavior::IgnoreClicks => false,
}; };
@@ -670,15 +671,15 @@ impl Popup<'_> {
/// If you are NOT using [`Popup::show`], you must /// If you are NOT using [`Popup::show`], you must
/// also call [`crate::Memory::keep_popup_open`] as long as /// also call [`crate::Memory::keep_popup_open`] as long as
/// you're showing the popup. /// you're showing the popup.
pub fn open_id(ctx: &Context, popup_id: Id) { pub fn open_id(ctx: &Context, layer_id: LayerId, popup_id: Id) {
ctx.memory_mut(|mem| mem.open_popup(popup_id)); ctx.memory_mut(|mem| mem.open_popup(layer_id, popup_id));
} }
/// Toggle the given popup between closed and open. /// Toggle the given popup between closed and open.
/// ///
/// Note: At most, only one popup can be open at a time. /// Note: At most, only one popup can be open at a time.
pub fn toggle_id(ctx: &Context, popup_id: Id) { pub fn toggle_id(ctx: &Context, layer_id: LayerId, popup_id: Id) {
ctx.memory_mut(|mem| mem.toggle_popup(popup_id)); ctx.memory_mut(|mem| mem.toggle_popup(layer_id, popup_id));
} }
/// Close all currently open popups. /// Close all currently open popups.

View File

@@ -6,8 +6,8 @@ use ahash::{HashMap, HashSet};
use epaint::emath::TSTransform; use epaint::emath::TSTransform;
use crate::{ use crate::{
EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, RawInput, Rect, Style, Vec2, ViewportId, area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, RawInput, Rect, Style,
ViewportIdMap, ViewportIdSet, area, vec2, Vec2, ViewportId, ViewportIdMap, ViewportIdSet,
}; };
mod theme; mod theme;
@@ -115,7 +115,7 @@ pub struct Memory {
/// If position is [`None`], the popup position will be calculated based on some configuration /// If position is [`None`], the popup position will be calculated based on some configuration
/// (e.g. relative to some other widget). /// (e.g. relative to some other widget).
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
popups: ViewportIdMap<OpenPopup>, popups: ViewportIdMap<HashMap<LayerId, OpenPopup>>,
} }
impl Default for Memory { impl Default for Memory {
@@ -743,7 +743,7 @@ impl Memory {
// Cleanup // Cleanup
self.interactions.retain(|id, _| viewports.contains(id)); self.interactions.retain(|id, _| viewports.contains(id));
self.areas.retain(|id, _| viewports.contains(id)); self.areas.retain(|id, _| viewports.contains(id));
self.popups.retain(|id, _| viewports.contains(id)); // self.popups.retain(|id, _| viewports.contains(id)); TODO
self.areas.entry(self.viewport_id).or_default(); self.areas.entry(self.viewport_id).or_default();
@@ -763,12 +763,12 @@ impl Memory {
self.focus_mut().end_pass(used_ids); self.focus_mut().end_pass(used_ids);
// Clean up abandoned popups. // Clean up abandoned popups.
if let Some(popup) = self.popups.get_mut(&self.viewport_id) { if let Some(popups) = self.popups.get_mut(&self.viewport_id) {
if popup.open_this_frame { popups.retain(|_, popup| {
let open_this_frame = popup.open_this_frame;
popup.open_this_frame = false; popup.open_this_frame = false;
} else { open_this_frame
self.popups.remove(&self.viewport_id); })
}
} }
} }
@@ -1013,23 +1013,31 @@ impl Memory {
pub fn is_popup_open(&self, popup_id: Id) -> bool { pub fn is_popup_open(&self, popup_id: Id) -> bool {
self.popups self.popups
.get(&self.viewport_id) .get(&self.viewport_id)
.is_some_and(|state| state.id == popup_id) .is_some_and(|state| {
state.values().any(|popup| popup.id == popup_id)
})
|| self.everything_is_visible() || self.everything_is_visible()
} }
/// Is any popup open? /// Is any popup open?
#[deprecated = "Use Popup::is_any_open instead"] #[deprecated = "Use Popup::is_any_open instead"]
pub fn any_popup_open(&self) -> bool { pub fn any_popup_open(&self) -> bool {
self.popups.contains_key(&self.viewport_id) || self.everything_is_visible() self.popups
.get(&self.viewport_id)
.is_some_and(|state| !state.is_empty())
|| self.everything_is_visible()
} }
/// Open the given popup and close all others. /// Open the given popup and close all others.
/// ///
/// Note that you must call `keep_popup_open` on subsequent frames as long as the popup is open. /// Note that you must call `keep_popup_open` on subsequent frames as long as the popup is open.
#[deprecated = "Use Popup::open_id instead"] #[deprecated = "Use Popup::open_id instead"]
pub fn open_popup(&mut self, popup_id: Id) { pub fn open_popup(&mut self, layer: LayerId, popup_id: Id) {
// TODO: Close non-parent popups
self.popups self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, None)); .entry(self.viewport_id)
.or_default()
.insert(layer, OpenPopup::new(popup_id, None));
} }
/// Popups must call this every frame while open. /// Popups must call this every frame while open.
@@ -1038,26 +1046,34 @@ impl Memory {
/// called. For example, when a context menu is open and the underlying widget stops /// called. For example, when a context menu is open and the underlying widget stops
/// being rendered. /// being rendered.
#[deprecated = "Use Popup::show instead"] #[deprecated = "Use Popup::show instead"]
pub fn keep_popup_open(&mut self, popup_id: Id) { pub fn keep_popup_open(&mut self, layer_id: LayerId, popup_id: Id) {
if let Some(state) = self.popups.get_mut(&self.viewport_id) if let Some(state) = self.popups.get_mut(&self.viewport_id) {
&& state.id == popup_id if let Some(popup) = state.get_mut(&layer_id) {
{ if popup.id == popup_id {
state.open_this_frame = true; popup.open_this_frame = true;
}
}
} }
} }
/// Open the popup and remember its position. /// Open the popup and remember its position.
#[deprecated = "Use Popup with PopupAnchor::Position instead"] #[deprecated = "Use Popup with PopupAnchor::Position instead"]
pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) { pub fn open_popup_at(&mut self, layer_id: LayerId, popup_id: Id, pos: impl Into<Option<Pos2>>) {
self.popups self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, pos.into())); .entry(self.viewport_id)
.or_default()
.insert(layer_id, OpenPopup::new(popup_id, pos.into()));
} }
/// Get the position for this popup. /// Get the position for this popup.
#[deprecated = "Use Popup::position_of_id instead"] #[deprecated = "Use Popup::position_of_id instead"]
pub fn popup_position(&self, id: Id) -> Option<Pos2> { pub fn popup_position(&self, id: Id) -> Option<Pos2> {
let state = self.popups.get(&self.viewport_id)?; self.popups.get(&self.viewport_id).and_then(|state| {
if state.id == id { state.pos } else { None } state
.values()
.find(|popup| popup.id == id)
.and_then(|popup| popup.pos)
})
} }
/// Close any currently open popup. /// Close any currently open popup.
@@ -1071,9 +1087,8 @@ impl Memory {
/// See also [`Self::close_all_popups`] if you want to close any / all currently open popups. /// See also [`Self::close_all_popups`] if you want to close any / all currently open popups.
#[deprecated = "Use Popup::close_id instead"] #[deprecated = "Use Popup::close_id instead"]
pub fn close_popup(&mut self, popup_id: Id) { pub fn close_popup(&mut self, popup_id: Id) {
#[expect(deprecated)] if let Some(state) = self.popups.get_mut(&self.viewport_id) {
if self.is_popup_open(popup_id) { state.retain(|_, popup| popup.id != popup_id);
self.popups.remove(&self.viewport_id);
} }
} }
@@ -1081,12 +1096,11 @@ impl Memory {
/// ///
/// Note: At most, only one popup can be open at a time. /// Note: At most, only one popup can be open at a time.
#[deprecated = "Use Popup::toggle_id instead"] #[deprecated = "Use Popup::toggle_id instead"]
pub fn toggle_popup(&mut self, popup_id: Id) { pub fn toggle_popup(&mut self, layer_id: LayerId, popup_id: Id) {
#[expect(deprecated)]
if self.is_popup_open(popup_id) { if self.is_popup_open(popup_id) {
self.close_popup(popup_id); self.close_popup(popup_id);
} else { } else {
self.open_popup(popup_id); self.open_popup(layer_id, popup_id);
} }
} }
} }
@@ -1294,6 +1308,17 @@ impl Areas {
self.sublayers.get(&layer_id).into_iter().flatten().copied() self.sublayers.get(&layer_id).into_iter().flatten().copied()
} }
pub fn is_child_recursive(&self, parent: LayerId, child: LayerId) -> bool {
if let Some(children) = self.sublayers.get(&parent) {
for &c in children {
if c == child || self.is_child_recursive(c, child) {
return true;
}
}
}
false
}
pub(crate) fn is_sublayer(&self, layer: &LayerId) -> bool { pub(crate) fn is_sublayer(&self, layer: &LayerId) -> bool {
self.parent_layer(*layer).is_some() self.parent_layer(*layer).is_some()
} }

View File

@@ -1,10 +1,10 @@
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
use crate::{ use crate::{
Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip, Ui, emath::{Align, Pos2, Rect, Vec2}, pass_state, Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense,
WidgetRect, WidgetText, Tooltip, Ui,
emath::{Align, Pos2, Rect, Vec2}, WidgetRect,
pass_state, WidgetText,
}; };
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -259,6 +259,28 @@ impl Response {
} }
} }
pub fn clicked_elsewhere_excluding_child_layers(&self) -> bool {
let clicked_elsewhere = self.clicked_elsewhere();
if !clicked_elsewhere {
return false;
}
if let Some(pos) = self.ctx.input(|i| i.pointer.interact_pos()) {
let layer_under_pointer = self.ctx.layer_id_at(pos);
if let Some(layer_under_pointer) = layer_under_pointer {
let child_clicked = self.ctx.memory(|mem| {
mem.areas()
.is_child_recursive(self.layer_id, layer_under_pointer)
});
!child_clicked
} else {
clicked_elsewhere
}
} else {
clicked_elsewhere
}
}
/// Was the widget enabled? /// Was the widget enabled?
/// If false, there was no interaction attempted /// If false, there was no interaction attempted
/// and the widget should be drawn in a gray disabled look. /// and the widget should be drawn in a gray disabled look.

View File

@@ -1,9 +1,9 @@
use crate::rust_view_ui; use crate::rust_view_ui;
use egui::color_picker::{Alpha, color_picker_color32}; use egui::color_picker::{color_picker_color32, Alpha};
use egui::containers::menu::{MenuConfig, SubMenuButton}; use egui::containers::menu::{MenuConfig, SubMenuButton};
use egui::{ use egui::{
Align, Align2, Atom, Button, ComboBox, Frame, Id, Layout, Popup, PopupCloseBehavior, RectAlign, include_image, Align, Align2, Atom, Button, ComboBox, Frame, Id, Layout, Popup, PopupCloseBehavior,
RichText, Tooltip, Ui, UiBuilder, include_image, RectAlign, RichText, Tooltip, Ui, UiBuilder,
}; };
/// Showcase [`Popup`]. /// Showcase [`Popup`].
@@ -119,6 +119,13 @@ impl PopupsDemo {
if ui.button("Open…").clicked() { if ui.button("Open…").clicked() {
ui.close(); ui.close();
} }
ComboBox::new("Combobox in menu", "")
.selected_text(if self.checked { "Option 1" } else { "Option 2" })
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.checked, true, "Option 1");
ui.selectable_value(&mut self.checked, false, "Option 2");
});
}); });
} }
} }

View File

@@ -17,6 +17,14 @@ pub struct Node<'tree> {
pub(crate) queue: &'tree EventQueue, pub(crate) queue: &'tree EventQueue,
} }
impl PartialEq for Node<'_> {
fn eq(&self, other: &Self) -> bool {
self.accesskit_node.id() == other.accesskit_node.id()
}
}
impl Eq for Node<'_> {}
impl Debug for Node<'_> { impl Debug for Node<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
debug_fmt_node(self, f) debug_fmt_node(self, f)

View File

@@ -1,31 +0,0 @@
use kittest::Queryable as _;
#[test]
fn test_interactive_tooltip() {
struct State {
link_clicked: bool,
}
let mut harness = egui_kittest::Harness::new_ui_state(
|ui, state| {
ui.label("I have a tooltip").on_hover_ui(|ui| {
if ui.link("link").clicked() {
state.link_clicked = true;
}
});
},
State {
link_clicked: false,
},
);
harness.get_by_label_contains("tooltip").hover();
harness.run();
harness.get_by_label("link").hover();
harness.run();
harness.get_by_label("link").click();
harness.run();
assert!(harness.state().link_clicked);
}

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8876d1b19c1d8c0cac30edc022913ca88ba7afaedf677e9c5de07c3b45e3b8a
size 10595

View File

@@ -0,0 +1,88 @@
use egui::{ComboBox, Popup, PopupCloseBehavior, Vec2};
use egui_kittest::kittest::Queryable as _;
use egui_kittest::Harness;
#[test]
fn test_interactive_tooltip() {
struct State {
link_clicked: bool,
}
let mut harness = egui_kittest::Harness::new_ui_state(
|ui, state| {
ui.label("I have a tooltip").on_hover_ui(|ui| {
if ui.link("link").clicked() {
state.link_clicked = true;
}
});
},
State {
link_clicked: false,
},
);
harness.get_by_label_contains("tooltip").hover();
harness.run();
harness.get_by_label("link").hover();
harness.run();
harness.get_by_label("link").click();
harness.run();
assert!(harness.state().link_clicked);
}
#[test]
fn test_combobox_in_popup() {
let mut harness = Harness::builder()
.with_size(Vec2::new(200.0, 200.0))
.build_ui_state(
|ui, state| {
let response = ui.button("Open Popup");
Popup::menu(&response)
.close_behavior(PopupCloseBehavior::CloseOnClickOutside)
.show(|ui| {
ui.heading("Popup");
ComboBox::new("combo", "")
.selected_text("Select an option")
.close_behavior(PopupCloseBehavior::CloseOnClickOutside)
.show_ui(ui, |ui| {
ui.selectable_value(state, 0, "Option 0");
ui.selectable_value(state, 1, "Option 1");
});
});
},
0,
);
harness.get_by_label("Open Popup").click();
harness.run();
harness.get_by_value("Select an option").click();
harness.run();
harness.get_by_label("Option 1").click();
harness.run();
assert_eq!(*harness.state(), 1);
// The parent popup should not close when clicking on the child popup
harness.get_by_label("Option 0").click();
harness.run();
assert_eq!(*harness.state(), 0);
harness.snapshot("combobox_in_popup");
// Clicking the parent popup should close the child popup
harness.get_by_label("Popup").click();
harness.run();
assert_eq!(harness.query_by_label("Option 0"), None);
harness.get_by_value("Select an option").click();
harness.run();
assert_eq!(harness.query_by_label("Option 0").is_some(), true);
// Clicking outside should close both popups
harness.get_by_label("Open Popup").click();
harness.run();
assert_eq!(harness.query_by_label("Popup"), None);
}