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:
@@ -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`.
|
/// 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
|
/// `rect` is the full widget rect (frame included). For a top-level layout this is
|
||||||
|
|||||||
@@ -133,6 +133,23 @@ impl<'ui, 'layout> AtomUi<'ui, 'layout> {
|
|||||||
Self { ctx, 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 {
|
pub fn response(&self) -> Response {
|
||||||
self.ctx
|
self.ctx
|
||||||
.read_response(self.layout.id.expect("set in constructor"))
|
.read_response(self.layout.id.expect("set in constructor"))
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
use emath::GuiRounding as _;
|
use emath::GuiRounding as _;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2, Rect, Response,
|
Align2, AtomLayout, AtomUi, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order,
|
||||||
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, emath, pos2,
|
Pos2, Rect, Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect,
|
||||||
|
WidgetWithState, emath, pos2,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// State of an [`Area`] that is persisted between frames.
|
/// State of an [`Area`] that is persisted between frames.
|
||||||
@@ -391,6 +392,15 @@ pub(crate) struct Prepared {
|
|||||||
constrain: bool,
|
constrain: bool,
|
||||||
constrain_rect: Rect,
|
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".
|
/// 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,
|
/// 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 {
|
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 {
|
let Self {
|
||||||
id,
|
id,
|
||||||
info,
|
info,
|
||||||
@@ -441,7 +457,7 @@ impl Area {
|
|||||||
let layer_id = LayerId::new(order, id);
|
let layer_id = LayerId::new(order, id);
|
||||||
|
|
||||||
let state = AreaState::load(ctx, 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 {
|
let mut state = state.unwrap_or(AreaState {
|
||||||
pivot_pos: None,
|
pivot_pos: None,
|
||||||
pivot,
|
pivot,
|
||||||
@@ -449,7 +465,7 @@ impl Area {
|
|||||||
interactable,
|
interactable,
|
||||||
last_became_visible_at: None,
|
last_became_visible_at: None,
|
||||||
});
|
});
|
||||||
if force_sizing_pass {
|
if force_sizing_pass && !measured {
|
||||||
sizing_pass = true;
|
sizing_pass = true;
|
||||||
state.size = None;
|
state.size = None;
|
||||||
}
|
}
|
||||||
@@ -461,10 +477,9 @@ impl Area {
|
|||||||
default_pos.unwrap_or_else(|| automatic_area_position(ctx, constrain_rect, layer_id))
|
default_pos.unwrap_or_else(|| automatic_area_position(ctx, constrain_rect, layer_id))
|
||||||
});
|
});
|
||||||
|
|
||||||
let size = *state.size.get_or_insert_with(|| {
|
// The size the content gets to lay itself out in.
|
||||||
sizing_pass = true;
|
// During a sizing pass we use it as the max size of the area too.
|
||||||
|
let available_size = {
|
||||||
// during the sizing pass we will use this as the max size
|
|
||||||
let mut size = default_size;
|
let mut size = default_size;
|
||||||
|
|
||||||
let default_area_size = ctx.global_style().spacing.default_area_size;
|
let default_area_size = ctx.global_style().spacing.default_area_size;
|
||||||
@@ -480,6 +495,11 @@ impl Area {
|
|||||||
}
|
}
|
||||||
|
|
||||||
size
|
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
|
// 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
|
move_response
|
||||||
};
|
};
|
||||||
|
|
||||||
state.set_left_top_pos(round_area_position(
|
// Constraining needs the real size, and `state.size` is still last frame's guess.
|
||||||
ctx,
|
// If the caller measures the contents it will call `Prepared::resize`, which does
|
||||||
if constrain {
|
// this once the size is known. Doing it here as well would move the pivot with the
|
||||||
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min
|
// wrong size and place the area somewhere else entirely.
|
||||||
} else {
|
if !measured {
|
||||||
state.left_top_pos()
|
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:
|
// Update response with possibly moved/constrained rect:
|
||||||
move_response.rect = state.rect();
|
move_response.rect = state.rect();
|
||||||
move_response.interact_rect = state.rect();
|
move_response.interact_rect = state.rect();
|
||||||
|
}
|
||||||
|
|
||||||
Prepared {
|
Prepared {
|
||||||
info: Some(info),
|
info: Some(info),
|
||||||
@@ -581,8 +607,62 @@ impl Area {
|
|||||||
sizing_pass,
|
sizing_pass,
|
||||||
fade_in,
|
fade_in,
|
||||||
layout,
|
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 {
|
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 {
|
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()
|
let mut ui_builder = UiBuilder::new()
|
||||||
.ui_stack_info(self.info.take().unwrap_or_default())
|
.ui_stack_info(self.info.take().unwrap_or_default())
|
||||||
.layer_id(self.layer_id)
|
.layer_id(self.layer_id)
|
||||||
@@ -654,8 +747,44 @@ impl Prepared {
|
|||||||
self.move_response.id
|
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 {
|
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 {
|
let Self {
|
||||||
info: _,
|
info: _,
|
||||||
layer_id,
|
layer_id,
|
||||||
@@ -665,7 +794,7 @@ impl Prepared {
|
|||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
state.size = Some(content_ui.min_size());
|
state.size = Some(size);
|
||||||
|
|
||||||
// Make sure we report back the correct 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.
|
// Very important after the initial sizing pass, when the initial estimate of the size is way off.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use core::iter::once;
|
|||||||
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
|
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
|
Area, AreaState, AtomUi, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order,
|
||||||
Sense, Ui, UiKind, UiStackInfo,
|
Response, Sense, Ui, UiKind, UiStackInfo,
|
||||||
containers::menu::{MenuConfig, MenuState, menu_style},
|
containers::menu::{MenuConfig, MenuState, menu_style},
|
||||||
style::StyleModifier,
|
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
|
/// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
|
||||||
/// no pointer.
|
/// no pointer.
|
||||||
pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
|
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;
|
let id = self.id;
|
||||||
// When the popup was just opened with a click we don't want to immediately close it based
|
// 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
|
// 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);
|
area = area.default_width(width);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut response = area.show(&ctx, |ui| {
|
let mut response = show_area(area, &ctx, style, frame);
|
||||||
style.apply(ui.style_mut());
|
|
||||||
let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
|
|
||||||
frame.show(ui, content).inner
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the popup was just opened with a click, we don't want to immediately close it again.
|
// 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());
|
let close_click = was_open_last_frame && ctx.input(|i| i.pointer.any_click());
|
||||||
|
|||||||
Reference in New Issue
Block a user