1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Add Popup::show_atom

This commit is contained in:
Lucas Meurer
2026-08-18 13:21:32 +02:00
parent 84ba1578e7
commit 5710474253
4 changed files with 218 additions and 29 deletions

View File

@@ -577,6 +577,16 @@ impl<'atom> SizedAtomLayout<'atom> {
});
}
/// [`Ui::interact`] at `rect` and [`Self::paint_at`] there.
///
/// This is what [`AllocatedAtomLayout::paint`] does, but at a [`Rect`] you choose instead
/// of one allocated from the [`Ui`] cursor. Use it when you know where the layout goes
/// only after measuring it, like [`crate::Area::show_atom`] does.
pub fn show_at(self, ui: &Ui, rect: Rect) -> AtomLayoutResponse {
let response = ui.interact(rect, self.id, self.sense);
self.paint_at(ui, rect, response)
}
/// Paint the [`Frame`] and individual [`crate::Atom`]s within `rect`.
///
/// `rect` is the full widget rect (frame included). For a top-level layout this is

View File

@@ -133,6 +133,23 @@ impl<'ui, 'layout> AtomUi<'ui, 'layout> {
Self { ctx, layout }
}
/// The [`Style`] the widgets built here will use.
pub fn style(&self) -> &Style {
self.ctx.style()
}
/// Mutate the [`Style`] the widgets built here will use.
///
/// Call this before adding anything, since each widget reads the style as it is built.
pub fn style_mut(&mut self) -> &mut Style {
self.ctx.style_mut()
}
/// Set the [`crate::Frame`] painted around this layout.
pub fn set_frame(&mut self, frame: crate::Frame) {
self.layout.frame = frame;
}
pub fn response(&self) -> Response {
self.ctx
.read_response(self.layout.id.expect("set in constructor"))

View File

@@ -5,8 +5,9 @@
use emath::GuiRounding as _;
use crate::{
Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2, Rect, Response,
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, emath, pos2,
Align2, AtomLayout, AtomUi, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order,
Pos2, Rect, Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect,
WidgetWithState, emath, pos2,
};
/// State of an [`Area`] that is persisted between frames.
@@ -391,6 +392,15 @@ pub(crate) struct Prepared {
constrain: bool,
constrain_rect: Rect,
/// See [`Area::anchor`]. Needed to re-place the area in [`Prepared::resize`].
anchor: Option<(Align2, Vec2)>,
/// The size the content is laid out against.
///
/// This is [`Area::default_size`], and unlike [`AreaState::size`] it does not depend on
/// how big the area happened to be last frame.
available_size: Vec2,
/// We always make windows invisible the first frame to hide "first-frame-jitters".
///
/// This is so that we use the first frame to calculate the window size,
@@ -416,6 +426,12 @@ impl Area {
}
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
self.begin_impl(ctx, false)
}
/// If `measured`, the caller knows the size of the contents up front and will call
/// [`Prepared::resize`], so no sizing pass is needed and none is done.
fn begin_impl(self, ctx: &Context, measured: bool) -> Prepared {
let Self {
id,
info,
@@ -441,7 +457,7 @@ impl Area {
let layer_id = LayerId::new(order, id);
let state = AreaState::load(ctx, id);
let mut sizing_pass = state.is_none();
let mut sizing_pass = !measured && state.is_none();
let mut state = state.unwrap_or(AreaState {
pivot_pos: None,
pivot,
@@ -449,7 +465,7 @@ impl Area {
interactable,
last_became_visible_at: None,
});
if force_sizing_pass {
if force_sizing_pass && !measured {
sizing_pass = true;
state.size = None;
}
@@ -461,10 +477,9 @@ impl Area {
default_pos.unwrap_or_else(|| automatic_area_position(ctx, constrain_rect, layer_id))
});
let size = *state.size.get_or_insert_with(|| {
sizing_pass = true;
// during the sizing pass we will use this as the max size
// The size the content gets to lay itself out in.
// During a sizing pass we use it as the max size of the area too.
let available_size = {
let mut size = default_size;
let default_area_size = ctx.global_style().spacing.default_area_size;
@@ -480,6 +495,11 @@ impl Area {
}
size
};
let size = *state.size.get_or_insert_with(|| {
sizing_pass = !measured;
available_size
});
// We should never be interactable during a sizing pass, since then we are shown at a different
@@ -557,18 +577,24 @@ impl Area {
move_response
};
state.set_left_top_pos(round_area_position(
ctx,
if constrain {
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min
} else {
state.left_top_pos()
},
));
// Constraining needs the real size, and `state.size` is still last frame's guess.
// If the caller measures the contents it will call `Prepared::resize`, which does
// this once the size is known. Doing it here as well would move the pivot with the
// wrong size and place the area somewhere else entirely.
if !measured {
state.set_left_top_pos(round_area_position(
ctx,
if constrain {
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min
} else {
state.left_top_pos()
},
));
// Update response with possibly moved/constrained rect:
move_response.rect = state.rect();
move_response.interact_rect = state.rect();
// Update response with possibly moved/constrained rect:
move_response.rect = state.rect();
move_response.interact_rect = state.rect();
}
Prepared {
info: Some(info),
@@ -581,8 +607,62 @@ impl Area {
sizing_pass,
fade_in,
layout,
anchor,
available_size,
}
}
/// Show the area, with [`crate::Atom`]-based contents.
///
/// Atoms are measured before they are painted, so the area knows its size before it has
/// to place itself. It is therefore correctly sized and positioned on the very first
/// frame, and needs none of the invisible sizing pass that [`Self::show`] does.
///
/// The atoms are laid out along the main direction of [`Self::layout`].
///
/// ```
/// # egui::__run_test_ctx(|ctx| {
/// egui::Area::new(egui::Id::new("my_area"))
/// .fixed_pos(egui::pos2(32.0, 32.0))
/// .show_atom(ctx, |ui| {
/// ui.add(egui::atom(), egui::Button::new("Floating button!"));
/// });
/// # });
/// ```
pub fn show_atom<'a, R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut AtomUi<'_, 'a>) -> R,
) -> InnerResponse<R> {
let builder = AtomLayout::default().direction(self.layout.main_dir());
self.show_atom_layout(ctx, builder, add_contents)
}
/// Same as [`Self::show_atom`], but you provide the outer [`AtomLayout`].
pub fn show_atom_layout<'a, R>(
self,
ctx: &Context,
builder: AtomLayout<'a>,
add_contents: impl FnOnce(&mut AtomUi<'_, 'a>) -> R,
) -> InnerResponse<R> {
let mut prepared = self.begin_impl(ctx, true);
let mut content_ui = prepared.content_atom_ui(ctx);
let (inner, layout) = {
let mut atom_ui = AtomUi::new(&mut content_ui, builder);
let inner = add_contents(&mut atom_ui);
(inner, atom_ui.show().0)
};
// Measure first, then place the area, then paint.
let sized = layout.measure(&content_ui, prepared.available_size);
let size = sized.outer_size;
prepared.resize(ctx, size);
sized.show_at(&content_ui, prepared.state.rect());
let response = prepared.end_with_size(ctx, content_ui, size);
InnerResponse { inner, response }
}
}
pub(crate) fn round_area_position(ctx: &Context, pos: Pos2) -> Pos2 {
@@ -609,8 +689,21 @@ impl Prepared {
}
pub(crate) fn content_ui(&mut self, ctx: &Context) -> Ui {
let max_rect = self.state.rect();
self.content_ui_at(ctx, self.state.rect())
}
/// The [`Ui`] used to build, measure and paint [`crate::Atom`]-based contents.
///
/// Unlike [`Self::content_ui`] it is sized to [`Self::available_size`], not to whatever
/// size the area had last frame, so measured contents are free to grow and shrink.
/// Its position is provisional: atoms are painted at the [`Rect`] passed to
/// [`crate::SizedAtomLayout::show_at`], not at the [`Ui`] cursor.
pub(crate) fn content_atom_ui(&mut self, ctx: &Context) -> Ui {
let max_rect = Rect::from_min_size(self.state.left_top_pos(), self.available_size);
self.content_ui_at(ctx, max_rect)
}
fn content_ui_at(&mut self, ctx: &Context, max_rect: Rect) -> Ui {
let mut ui_builder = UiBuilder::new()
.ui_stack_info(self.info.take().unwrap_or_default())
.layer_id(self.layer_id)
@@ -654,8 +747,44 @@ impl Prepared {
self.move_response.id
}
#[expect(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
/// Give the area its final size and place it accordingly.
///
/// [`Self::content_ui`] has to place the contents before it knows how big they are, which
/// is why [`Area::show`] needs a sizing pass to get the position right. Contents that can
/// be measured up front call this instead and are correct on the first frame.
pub(crate) fn resize(&mut self, ctx: &Context, size: Vec2) {
self.state.size = Some(size);
if let Some((anchor, offset)) = self.anchor {
self.state.set_left_top_pos(
anchor
.align_size_within_rect(size, self.constrain_rect)
.left_top()
+ offset,
);
}
self.state.set_left_top_pos(round_area_position(
ctx,
if self.constrain {
Context::constrain_window_rect_to_area(self.state.rect(), self.constrain_rect).min
} else {
self.state.left_top_pos()
},
));
let rect = self.state.rect();
self.move_response.rect = rect;
self.move_response.interact_rect = rect;
}
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
let size = content_ui.min_size();
self.end_with_size(ctx, content_ui, size)
}
#[expect(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
pub(crate) fn end_with_size(self, ctx: &Context, content_ui: Ui, size: Vec2) -> Response {
let Self {
info: _,
layer_id,
@@ -665,7 +794,7 @@ impl Prepared {
..
} = self;
state.size = Some(content_ui.min_size());
state.size = Some(size);
// Make sure we report back the correct size.
// Very important after the initial sizing pass, when the initial estimate of the size is way off.

View File

@@ -3,8 +3,8 @@ use core::iter::once;
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
use crate::{
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
Sense, Ui, UiKind, UiStackInfo,
Area, AreaState, AtomUi, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order,
Response, Sense, Ui, UiKind, UiStackInfo,
containers::menu::{MenuConfig, MenuState, menu_style},
style::StyleModifier,
};
@@ -521,6 +521,43 @@ impl<'a> Popup<'a> {
/// 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>> {
self.show_impl(|area, ctx, style, frame| {
area.show(ctx, |ui| {
style.apply(ui.style_mut());
let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
frame.show(ui, content).inner
})
})
}
/// Show the popup, with [`crate::Atom`]-based contents.
///
/// The contents are measured before they are painted, so the popup is correctly sized and
/// placed on the frame it opens. [`Self::show`] instead has to spend that frame on an
/// invisible sizing pass, during which the popup is neither visible nor interactable.
///
/// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
/// no pointer.
pub fn show_atom<'l, R>(
self,
content: impl FnOnce(&mut AtomUi<'_, 'l>) -> R,
) -> Option<InnerResponse<R>> {
self.show_impl(|area, ctx, style, frame| {
area.show_atom(ctx, |ui| {
style.apply(ui.style_mut());
let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
ui.set_frame(frame);
content(ui)
})
})
}
/// Everything [`Self::show`] and [`Self::show_atom`] have in common: the open state, the
/// alignment, the [`Area`] setup and the close behavior. `show_area` builds the contents.
fn show_impl<R>(
self,
show_area: impl FnOnce(Area, &Context, StyleModifier, Option<Frame>) -> InnerResponse<R>,
) -> Option<InnerResponse<R>> {
let id = self.id;
// When the popup was just opened with a click we don't want to immediately close it based
// on the `PopupCloseBehavior`, so we need to remember if the popup was already open on
@@ -614,11 +651,7 @@ impl<'a> Popup<'a> {
area = area.default_width(width);
}
let mut response = area.show(&ctx, |ui| {
style.apply(ui.style_mut());
let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
frame.show(ui, content).inner
});
let mut response = show_area(area, &ctx, style, frame);
// If the popup was just opened with a click, we don't want to immediately close it again.
let close_click = was_open_last_frame && ctx.input(|i| i.pointer.any_click());