Add `Ui::close` and `Response::should_close` (#5729)

This adds a generic way of telling containers to close from their child
`Ui`s.

* Part of #5727 
* [x] I have followed the instructions in the PR template

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
lucasmerlin 2025-02-20 17:59:29 +01:00 committed by GitHub
parent 264749b8af
commit f5b058b908
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 233 additions and 52 deletions

View File

@ -556,7 +556,8 @@ impl Prepared {
.ui_stack_info(UiStackInfo::new(self.kind)) .ui_stack_info(UiStackInfo::new(self.kind))
.layer_id(self.layer_id) .layer_id(self.layer_id)
.max_rect(max_rect) .max_rect(max_rect)
.layout(self.layout); .layout(self.layout)
.closable();
if !self.enabled { if !self.enabled {
ui_builder = ui_builder.disabled(); ui_builder = ui_builder.disabled();
@ -611,6 +612,12 @@ impl Prepared {
response.rect = final_rect; response.rect = final_rect;
response.interact_rect = final_rect; response.interact_rect = final_rect;
// TODO(lucasmerlin): Can the area response be based on Ui::response? Then this won't be needed
// Bubble up the close event
if content_ui.should_close() {
response.set_close();
}
ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state)); ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
if sizing_pass { if sizing_pass {

View File

@ -0,0 +1,20 @@
use std::sync::atomic::AtomicBool;
#[derive(Debug, Default)]
pub struct ClosableTag {
pub close: AtomicBool,
}
impl ClosableTag {
pub const NAME: &'static str = "egui_close_tag";
/// Set close to `true`
pub fn set_close(&self) {
self.close.store(true, std::sync::atomic::Ordering::Relaxed);
}
/// Returns `true` if [`ClosableTag::set_close`] has been called.
pub fn should_close(&self) -> bool {
self.close.load(std::sync::atomic::Ordering::Relaxed)
}
}

View File

@ -2,7 +2,8 @@ use std::hash::Hash;
use crate::{ use crate::{
emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect, emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect,
Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, WidgetInfo, WidgetText, WidgetType, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
WidgetInfo, WidgetText, WidgetType,
}; };
use emath::GuiRounding as _; use emath::GuiRounding as _;
use epaint::{Shape, StrokeKind}; use epaint::{Shape, StrokeKind};
@ -203,11 +204,16 @@ impl CollapsingState {
add_body: impl FnOnce(&mut Ui) -> R, add_body: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> { ) -> Option<InnerResponse<R>> {
let openness = self.openness(ui.ctx()); let openness = self.openness(ui.ctx());
let builder = UiBuilder::new()
.ui_stack_info(UiStackInfo::new(UiKind::Collapsible))
.closable();
if openness <= 0.0 { if openness <= 0.0 {
self.store(ui.ctx()); // we store any earlier toggling as promised in the docstring self.store(ui.ctx()); // we store any earlier toggling as promised in the docstring
None None
} else if openness < 1.0 { } else if openness < 1.0 {
Some(ui.scope(|child_ui| { Some(ui.scope_builder(builder, |child_ui| {
let max_height = if self.state.open && self.state.open_height.is_none() { let max_height = if self.state.open && self.state.open_height.is_none() {
// First frame of expansion. // First frame of expansion.
// We don't know full height yet, but we will next frame. // We don't know full height yet, but we will next frame.
@ -226,6 +232,9 @@ impl CollapsingState {
let mut min_rect = child_ui.min_rect(); let mut min_rect = child_ui.min_rect();
self.state.open_height = Some(min_rect.height()); self.state.open_height = Some(min_rect.height());
if child_ui.should_close() {
self.state.open = false;
}
self.store(child_ui.ctx()); // remember the height self.store(child_ui.ctx()); // remember the height
// Pretend children took up at most `max_height` space: // Pretend children took up at most `max_height` space:
@ -234,7 +243,10 @@ impl CollapsingState {
ret ret
})) }))
} else { } else {
let ret_response = ui.scope(add_body); let ret_response = ui.scope_builder(builder, add_body);
if ret_response.response.should_close() {
self.state.open = false;
}
let full_size = ret_response.response.rect.size(); let full_size = ret_response.response.rect.size();
self.state.open_height = Some(full_size.y); self.state.open_height = Some(full_size.y);
self.store(ui.ctx()); // remember the height self.store(ui.ctx()); // remember the height

View File

@ -3,6 +3,7 @@
//! For instance, a [`Frame`] adds a frame and background to some contained UI. //! For instance, a [`Frame`] adds a frame and background to some contained UI.
pub(crate) mod area; pub(crate) mod area;
pub mod close_tag;
pub mod collapsing_header; pub mod collapsing_header;
mod combo_box; mod combo_box;
pub mod frame; pub mod frame;

View File

@ -150,7 +150,10 @@ impl<T> ModalResponse<T> {
let escape_clicked = let escape_clicked =
|| ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape)); || ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
let ui_close_called = self.response.should_close();
self.backdrop_response.clicked() self.backdrop_response.clicked()
|| ui_close_called
|| (self.is_top_modal && !self.any_popup_open && escape_clicked()) || (self.is_top_modal && !self.any_popup_open && escape_clicked())
} }
} }

View File

@ -542,7 +542,9 @@ impl<'a> Popup<'a> {
PopupCloseBehavior::IgnoreClicks => false, PopupCloseBehavior::IgnoreClicks => false,
}; };
should_close || ctx.input(|i| i.key_pressed(Key::Escape)) should_close
|| ctx.input(|i| i.key_pressed(Key::Escape))
|| response.response.should_close()
}; };
match open_kind { match open_kind {

View File

@ -434,7 +434,7 @@ impl Window<'_> {
) -> Option<InnerResponse<Option<R>>> { ) -> Option<InnerResponse<Option<R>>> {
let Window { let Window {
title, title,
open, mut open,
area, area,
frame, frame,
resize, resize,
@ -634,7 +634,7 @@ impl Window<'_> {
title_bar.ui( title_bar.ui(
&mut area_content_ui, &mut area_content_ui,
&content_response, &content_response,
open, open.as_deref_mut(),
&mut collapsing, &mut collapsing,
collapsible, collapsible,
); );
@ -650,6 +650,12 @@ impl Window<'_> {
let full_response = area.end(ctx, area_content_ui); let full_response = area.end(ctx, area_content_ui);
if full_response.should_close() {
if let Some(open) = open {
*open = false;
}
}
let inner_response = InnerResponse { let inner_response = InnerResponse {
inner: content_inner, inner: content_inner,
response: full_response, response: full_response,

View File

@ -88,7 +88,7 @@ pub fn zoom_menu_buttons(ui: &mut Ui) {
.clicked() .clicked()
{ {
zoom_in(ui.ctx()); zoom_in(ui.ctx());
ui.close_menu(); ui.close();
} }
if ui if ui
@ -99,7 +99,7 @@ pub fn zoom_menu_buttons(ui: &mut Ui) {
.clicked() .clicked()
{ {
zoom_out(ui.ctx()); zoom_out(ui.ctx());
ui.close_menu(); ui.close();
} }
if ui if ui
@ -110,6 +110,6 @@ 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_menu(); ui.close();
} }
} }

View File

@ -364,7 +364,10 @@ impl MenuRoot {
let menu_state = self.menu_state.read(); let menu_state = self.menu_state.read();
let escape_pressed = button.ctx.input(|i| i.key_pressed(Key::Escape)); let escape_pressed = button.ctx.input(|i| i.key_pressed(Key::Escape));
if menu_state.response.is_close() || escape_pressed { if menu_state.response.is_close()
|| escape_pressed
|| inner_response.response.should_close()
{
return (MenuResponse::Close, Some(inner_response)); return (MenuResponse::Close, Some(inner_response));
} }
} }
@ -667,6 +670,9 @@ impl MenuState {
) -> Option<R> { ) -> Option<R> {
let (sub_response, response) = self.submenu(id).map(|sub| { let (sub_response, response) = self.submenu(id).map(|sub| {
let inner_response = menu_popup(ctx, parent_layer, sub, id, add_contents); let inner_response = menu_popup(ctx, parent_layer, sub, id, add_contents);
if inner_response.response.should_close() {
sub.write().close();
}
(sub.read().response, inner_response.inner) (sub.read().response, inner_response.inner)
})?; })?;
self.cascade_close_response(sub_response); self.cascade_close_response(sub_response);

View File

@ -133,6 +133,9 @@ bitflags::bitflags! {
/// Note that this can be `true` even if the user did not interact with the widget, /// Note that this can be `true` even if the user did not interact with the widget,
/// for instance if an existing slider value was clamped to the given range. /// for instance if an existing slider value was clamped to the given range.
const CHANGED = 1<<11; const CHANGED = 1<<11;
/// Should this container be closed?
const CLOSE = 1<<12;
} }
} }
@ -528,6 +531,21 @@ impl Response {
self.flags.set(Flags::CHANGED, true); self.flags.set(Flags::CHANGED, true);
} }
/// Should the container be closed?
///
/// Will e.g. be set by calling [`Ui::close`] in a child [`Ui`] or by calling
/// [`Self::set_close`].
pub fn should_close(&self) -> bool {
self.flags.contains(Flags::CLOSE)
}
/// Set the [`Flags::CLOSE`] flag.
///
/// Can be used to e.g. signal that a container should be closed.
pub fn set_close(&mut self) {
self.flags.set(Flags::CLOSE, true);
}
/// Show this UI if the widget was hovered (i.e. a tooltip). /// Show this UI if the widget was hovered (i.e. a tooltip).
/// ///
/// The text will not be visible if the widget is not enabled. /// The text will not be visible if the widget is not enabled.
@ -909,13 +927,13 @@ impl Response {
/// let response = ui.add(Label::new("Right-click me!").sense(Sense::click())); /// let response = ui.add(Label::new("Right-click me!").sense(Sense::click()));
/// response.context_menu(|ui| { /// response.context_menu(|ui| {
/// if ui.button("Close the menu").clicked() { /// if ui.button("Close the menu").clicked() {
/// ui.close_menu(); /// ui.close();
/// } /// }
/// }); /// });
/// # }); /// # });
/// ``` /// ```
/// ///
/// See also: [`Ui::menu_button`] and [`Ui::close_menu`]. /// 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) menu::context_menu(self, add_contents)
} }

View File

@ -1,11 +1,13 @@
#![warn(missing_docs)] // Let's keep `Ui` well-documented. #![warn(missing_docs)] // Let's keep `Ui` well-documented.
#![allow(clippy::use_self)] #![allow(clippy::use_self)]
use std::{any::Any, hash::Hash, sync::Arc};
use emath::GuiRounding as _; use emath::GuiRounding as _;
use epaint::mutex::RwLock; use epaint::mutex::RwLock;
use std::{any::Any, hash::Hash, sync::Arc};
use crate::close_tag::ClosableTag;
#[cfg(debug_assertions)]
use crate::Stroke;
use crate::{ use crate::{
containers::{CollapsingHeader, CollapsingResponse, Frame}, containers::{CollapsingHeader, CollapsingResponse, Frame},
ecolor::Hsva, ecolor::Hsva,
@ -26,11 +28,9 @@ use crate::{
}, },
Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, LayerId, Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, LayerId,
Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText, Sense, Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText, Sense,
Style, TextStyle, TextWrapMode, UiBuilder, UiStack, UiStackInfo, Vec2, WidgetRect, WidgetText, Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2, WidgetRect,
WidgetText,
}; };
#[cfg(debug_assertions)]
use crate::Stroke;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// This is what you use to place widgets. /// This is what you use to place widgets.
@ -1095,7 +1095,8 @@ impl Ui {
// This is the inverse of Context::read_response. We prefer a response // This is the inverse of Context::read_response. We prefer a response
// based on last frame's widget rect since the one from this frame is Rect::NOTHING until // based on last frame's widget rect since the one from this frame is Rect::NOTHING until
// Ui::interact_bg is called or the Ui is dropped. // Ui::interact_bg is called or the Ui is dropped.
self.ctx() let mut response = self
.ctx()
.viewport(|viewport| { .viewport(|viewport| {
viewport viewport
.prev_pass .prev_pass
@ -1107,7 +1108,11 @@ impl Ui {
.map(|widget_rect| self.ctx().get_response(widget_rect)) .map(|widget_rect| self.ctx().get_response(widget_rect))
.expect( .expect(
"Since we always call Context::create_widget in Ui::new, this should never be None", "Since we always call Context::create_widget in Ui::new, this should never be None",
) );
if self.should_close() {
response.set_close();
}
response
} }
/// Update the [`WidgetRect`] created in [`Ui::new`] or [`Ui::new_child`] with the current /// Update the [`WidgetRect`] created in [`Ui::new`] or [`Ui::new_child`] with the current
@ -1121,7 +1126,7 @@ impl Ui {
fs.used_ids.remove(&self.unique_id); fs.used_ids.remove(&self.unique_id);
}); });
// This will update the WidgetRect that was first created in `Ui::new`. // This will update the WidgetRect that was first created in `Ui::new`.
self.ctx().create_widget( let mut response = self.ctx().create_widget(
WidgetRect { WidgetRect {
id: self.unique_id, id: self.unique_id,
layer_id: self.layer_id(), layer_id: self.layer_id(),
@ -1131,7 +1136,11 @@ impl Ui {
enabled: self.enabled, enabled: self.enabled,
}, },
false, false,
) );
if self.should_close() {
response.set_close();
}
response
} }
/// Interact with the background of this [`Ui`], /// Interact with the background of this [`Ui`],
@ -1165,6 +1174,69 @@ impl Ui {
pub fn ui_contains_pointer(&self) -> bool { pub fn ui_contains_pointer(&self) -> bool {
self.rect_contains_pointer(self.min_rect()) self.rect_contains_pointer(self.min_rect())
} }
/// Find and close the first closable parent.
///
/// Use [`UiBuilder::closable`] to make a [`Ui`] closable.
/// You can then use [`Ui::should_close`] to check if it should be closed.
///
/// This is implemented for all egui containers, e.g. [`crate::Popup`], [`crate::Modal`],
/// [`crate::Area`], [`crate::Window`], [`crate::CollapsingHeader`], etc.
///
/// What exactly happens when you close a container depends on the container implementation.
/// [`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.
pub fn close(&self) {
let tag = self.stack.iter().find_map(|stack| {
stack
.info
.tags
.get_downcast::<ClosableTag>(ClosableTag::NAME)
});
if let Some(tag) = tag {
tag.set_close();
} else {
#[cfg(feature = "log")]
log::warn!("Called ui.close() on a Ui that has no closable parent.");
}
}
/// Find and close the first closable parent of a specific [`UiKind`].
///
/// This is useful if you want to e.g. close a [`crate::Window`]. Since it contains a
/// `Collapsible`, [`Ui::close`] would close the `Collapsible` instead.
/// You can close the [`crate::Window`] by calling `ui.close_kind(UiKind::Window)`.
pub fn close_kind(&self, ui_kind: UiKind) {
let tag = self
.stack
.iter()
.filter(|stack| stack.info.kind == Some(ui_kind))
.find_map(|stack| {
stack
.info
.tags
.get_downcast::<ClosableTag>(ClosableTag::NAME)
});
if let Some(tag) = tag {
tag.set_close();
} else {
#[cfg(feature = "log")]
log::warn!("Called ui.close_kind({ui_kind:?}) on ui with no such closable parent.");
}
}
/// Was [`Ui::close`] called on this [`Ui`] or any of its children?
/// Only works if the [`Ui`] was created with [`UiBuilder::closable`].
///
/// You can also check via this [`Ui`]'s [`Response::should_close`].
pub fn should_close(&self) -> bool {
self.stack
.info
.tags
.get_downcast(ClosableTag::NAME)
.is_some_and(|tag: &ClosableTag| tag.should_close())
}
} }
/// # Allocating space: where do I put my widgets? /// # Allocating space: where do I put my widgets?
@ -2900,11 +2972,9 @@ impl Ui {
/// Close the menu we are in (including submenus), if any. /// Close the menu we are in (including submenus), if any.
/// ///
/// See also: [`Self::menu_button`] and [`Response::context_menu`]. /// See also: [`Self::menu_button`] and [`Response::context_menu`].
pub fn close_menu(&mut self) { #[deprecated = "Use `ui.close()` or `ui.close_kind(UiKind::Menu)` instead"]
if let Some(menu_state) = &mut self.menu_state { pub fn close_menu(&self) {
menu_state.write().close(); self.close_kind(UiKind::Menu);
}
self.menu_state = None;
} }
pub(crate) fn set_menu_state(&mut self, menu_state: Option<Arc<RwLock<MenuState>>>) { pub(crate) fn set_menu_state(&mut self, menu_state: Option<Arc<RwLock<MenuState>>>) {
@ -2921,14 +2991,14 @@ impl Ui {
/// ui.menu_button("My menu", |ui| { /// ui.menu_button("My menu", |ui| {
/// ui.menu_button("My sub-menu", |ui| { /// ui.menu_button("My sub-menu", |ui| {
/// if ui.button("Close the menu").clicked() { /// if ui.button("Close the menu").clicked() {
/// ui.close_menu(); /// ui.close();
/// } /// }
/// }); /// });
/// }); /// });
/// # }); /// # });
/// ``` /// ```
/// ///
/// See also: [`Self::close_menu`] and [`Response::context_menu`]. /// See also: [`Self::close`] and [`Response::context_menu`].
pub fn menu_button<R>( pub fn menu_button<R>(
&mut self, &mut self,
title: impl Into<WidgetText>, title: impl Into<WidgetText>,
@ -2952,7 +3022,7 @@ impl Ui {
/// ui.menu_image_button(title, img, |ui| { /// ui.menu_image_button(title, img, |ui| {
/// ui.menu_button("My sub-menu", |ui| { /// ui.menu_button("My sub-menu", |ui| {
/// if ui.button("Close the menu").clicked() { /// if ui.button("Close the menu").clicked() {
/// ui.close_menu(); /// ui.close();
/// } /// }
/// }); /// });
/// }); /// });
@ -2960,7 +3030,7 @@ impl Ui {
/// ``` /// ```
/// ///
/// ///
/// See also: [`Self::close_menu`] and [`Response::context_menu`]. /// See also: [`Self::close`] and [`Response::context_menu`].
#[inline] #[inline]
pub fn menu_image_button<'a, R>( pub fn menu_image_button<'a, R>(
&mut self, &mut self,
@ -2986,14 +3056,14 @@ impl Ui {
/// ui.menu_image_text_button(img, title, |ui| { /// ui.menu_image_text_button(img, title, |ui| {
/// ui.menu_button("My sub-menu", |ui| { /// ui.menu_button("My sub-menu", |ui| {
/// if ui.button("Close the menu").clicked() { /// if ui.button("Close the menu").clicked() {
/// ui.close_menu(); /// ui.close();
/// } /// }
/// }); /// });
/// }); /// });
/// # }); /// # });
/// ``` /// ```
/// ///
/// See also: [`Self::close_menu`] and [`Response::context_menu`]. /// See also: [`Self::close`] and [`Response::context_menu`].
#[inline] #[inline]
pub fn menu_image_text_button<'a, R>( pub fn menu_image_text_button<'a, R>(
&mut self, &mut self,

View File

@ -1,9 +1,9 @@
use std::{hash::Hash, sync::Arc}; use std::{hash::Hash, sync::Arc};
use crate::{Id, LayerId, Layout, Rect, Sense, Style, UiStackInfo}; use crate::close_tag::ClosableTag;
#[allow(unused_imports)] // Used for doclinks #[allow(unused_imports)] // Used for doclinks
use crate::Ui; use crate::Ui;
use crate::{Id, LayerId, Layout, Rect, Sense, Style, UiStackInfo};
/// Build a [`Ui`] as the child of another [`Ui`]. /// Build a [`Ui`] as the child of another [`Ui`].
/// ///
@ -125,6 +125,7 @@ impl UiBuilder {
} }
/// Set if you want sense clicks and/or drags. Default is [`Sense::hover`]. /// Set if you want sense clicks and/or drags. Default is [`Sense::hover`].
///
/// The sense will be registered below the Senses of any widgets contained in this [`Ui`], so /// The sense will be registered below the Senses of any widgets contained in this [`Ui`], so
/// if the user clicks a button contained within this [`Ui`], that button will receive the click /// if the user clicks a button contained within this [`Ui`], that button will receive the click
/// instead. /// instead.
@ -135,4 +136,19 @@ impl UiBuilder {
self.sense = Some(sense); self.sense = Some(sense);
self self
} }
/// Make this [`Ui`] closable.
///
/// Calling [`Ui::close`] in a child [`Ui`] will mark this [`Ui`] for closing.
/// After [`Ui::close`] was called, [`Ui::should_close`] and [`crate::Response::should_close`] will
/// return `true` (for this frame).
///
/// This works by adding a [`ClosableTag`] to the [`UiStackInfo`].
#[inline]
pub fn closable(mut self) -> Self {
self.ui_stack_info
.tags
.insert(ClosableTag::NAME, Some(Arc::new(ClosableTag::default())));
self
}
} }

View File

@ -53,6 +53,9 @@ pub enum UiKind {
/// An [`crate::Area`] that is not of any other kind. /// An [`crate::Area`] that is not of any other kind.
GenericArea, GenericArea,
/// A collapsible container, e.g. a [`crate::CollapsingHeader`].
Collapsible,
} }
impl UiKind { impl UiKind {
@ -81,6 +84,7 @@ impl UiKind {
| Self::Frame | Self::Frame
| Self::ScrollArea | Self::ScrollArea
| Self::Resize | Self::Resize
| Self::Collapsible
| Self::TableCell => false, | Self::TableCell => false,
Self::Window Self::Window

View File

@ -333,7 +333,7 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
.send_viewport_cmd(egui::ViewportCommand::InnerSize(size)); .send_viewport_cmd(egui::ViewportCommand::InnerSize(size));
ui.ctx() ui.ctx()
.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false)); .send_viewport_cmd(egui::ViewportCommand::Fullscreen(false));
ui.close_menu(); ui.close();
} }
}); });
} }

View File

@ -384,12 +384,12 @@ impl WrapApp {
.clicked() .clicked()
{ {
ui.ctx().memory_mut(|mem| *mem = Default::default()); ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close_menu(); ui.close();
} }
if ui.button("Reset everything").clicked() { if ui.button("Reset everything").clicked() {
*cmd = Command::ResetEverything; *cmd = Command::ResetEverything;
ui.close_menu(); ui.close();
} }
}); });
} }

View File

@ -59,25 +59,25 @@ impl ContextMenus {
ui.set_max_width(200.0); // To make sure we wrap long text ui.set_max_width(200.0); // To make sure we wrap long text
if ui.button("Open…").clicked() { if ui.button("Open…").clicked() {
ui.close_menu(); ui.close();
} }
ui.menu_button("SubMenu", |ui| { ui.menu_button("SubMenu", |ui| {
ui.menu_button("SubMenu", |ui| { ui.menu_button("SubMenu", |ui| {
if ui.button("Open…").clicked() { if ui.button("Open…").clicked() {
ui.close_menu(); ui.close();
} }
let _ = ui.button("Item"); let _ = ui.button("Item");
ui.menu_button("Recursive", Self::nested_menus) ui.menu_button("Recursive", Self::nested_menus)
}); });
ui.menu_button("SubMenu", |ui| { ui.menu_button("SubMenu", |ui| {
if ui.button("Open…").clicked() { if ui.button("Open…").clicked() {
ui.close_menu(); ui.close();
} }
let _ = ui.button("Item"); let _ = ui.button("Item");
}); });
let _ = ui.button("Item"); let _ = ui.button("Item");
if ui.button("Open…").clicked() { if ui.button("Open…").clicked() {
ui.close_menu(); ui.close();
} }
}); });
ui.menu_button("SubMenu", |ui| { ui.menu_button("SubMenu", |ui| {
@ -86,7 +86,7 @@ impl ContextMenus {
let _ = ui.button("Item3"); let _ = ui.button("Item3");
let _ = ui.button("Item4"); let _ = ui.button("Item4");
if ui.button("Open…").clicked() { if ui.button("Open…").clicked() {
ui.close_menu(); ui.close();
} }
}); });
let _ = ui.button("Very long text for this item that should be wrapped"); let _ = ui.button("Very long text for this item that should be wrapped");

View File

@ -234,7 +234,7 @@ impl DemoWindows {
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`. 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()) { if ui.ui_contains_pointer() && ui.input(|i| i.pointer.any_click()) {
ui.close_menu(); ui.close();
} }
}); });
@ -345,7 +345,7 @@ 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_menu(); ui.close();
} }
if ui if ui
@ -357,7 +357,7 @@ 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_menu(); ui.close();
} }
}); });
} }

View File

@ -96,7 +96,9 @@ impl crate::View for Modals {
*save_modal_open = true; *save_modal_open = true;
} }
if ui.button("Cancel").clicked() { if ui.button("Cancel").clicked() {
*user_modal_open = false; // You can call `ui.close()` to close the modal.
// (This causes the current modals `should_close` to return true)
ui.close();
} }
}, },
); );
@ -123,7 +125,7 @@ impl crate::View for Modals {
} }
if ui.button("No Thanks").clicked() { if ui.button("No Thanks").clicked() {
*save_modal_open = false; ui.close();
} }
}, },
); );

View File

@ -153,6 +153,10 @@ impl crate::View for PopupsDemo {
.show(|ui| { .show(|ui| {
_ = ui.button("Menu item 1"); _ = ui.button("Menu item 1");
_ = ui.button("Menu item 2"); _ = 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")))

View File

@ -1,4 +1,4 @@
use egui::Vec2b; use egui::{UiKind, Vec2b};
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@ -149,6 +149,16 @@ impl crate::View for WindowOptions {
self.disabled_time = ui.input(|i| i.time); self.disabled_time = ui.input(|i| i.time);
} }
egui::reset_button(ui, self, "Reset"); egui::reset_button(ui, self, "Reset");
if ui
.button("Close")
.on_hover_text("You can collapse / close Windows via Ui::close")
.clicked()
{
// Calling close would close the collapsible within the window
// ui.close();
// Instead, we close the window itself
ui.close_kind(UiKind::Window);
}
ui.add(crate::egui_github_link_file!()); ui.add(crate::egui_github_link_file!());
}); });
} }

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3411a4a8939b7e731c9c1a6331921b0ac905f4e3e86a51af70bdb38d9446f5e1 oid sha256:e67b1e676ff994cb9557939db3dca5ddd15c69d167afd96c0957a2a3b75c0fd8
size 35193 size 36007