mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Allow explicit popup sizing passes (#8407)
* [x] I have followed the instructions in the PR template ## Summary - Add an opt-in `Popup::sizing_pass(bool)` builder for remeasuring a popup whose contents change while it remains open. - Preserve the automatic first-open/reopen sizing pass and all existing default behavior. - Add a headless regression covering growth to a capped scroll viewport with overflowing content remaining scrollable. ## Why this is necessary A continuously open popup can first shrink around a short result set and later receive more content, such as an autocomplete after its query changes or a “show more” action. The cached `Area` height constrains the `ScrollArea` input size, so `ScrollArea::max_height` can cap the viewport but cannot make the containing popup grow again. PR #8315 taught `Popup` to rerun its sizing pass after closing and reopening. That fixes the same cached-size feedback loop when `was_open_last_frame` is false, but a continuously open popup keeps that value true while its contents change. In that case the caller is the component that knows the cached natural size is stale. This API exposes the existing one-frame `Area` sizing mechanism through `Popup`. It is additive, defaults to false, and combines with the automatic reopen pass, so unrelated popups, menus, tooltips, and areas keep their current behavior.
This commit is contained in:
committed by
GitHub
parent
9338f23bc6
commit
6d98e1dccb
@@ -179,6 +179,7 @@ pub struct Popup<'a> {
|
|||||||
|
|
||||||
/// Default width passed to the Area
|
/// Default width passed to the Area
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
|
sizing_pass: bool,
|
||||||
sense: Sense,
|
sense: Sense,
|
||||||
interactable: bool,
|
interactable: bool,
|
||||||
layout: Layout,
|
layout: Layout,
|
||||||
@@ -202,6 +203,7 @@ impl<'a> Popup<'a> {
|
|||||||
alternative_aligns: None,
|
alternative_aligns: None,
|
||||||
gap: 0.0,
|
gap: 0.0,
|
||||||
width: None,
|
width: None,
|
||||||
|
sizing_pass: false,
|
||||||
sense: Sense::click(),
|
sense: Sense::click(),
|
||||||
interactable: true,
|
interactable: true,
|
||||||
layout: Layout::default(),
|
layout: Layout::default(),
|
||||||
@@ -401,6 +403,19 @@ impl<'a> Popup<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Force the popup's underlying [`Area`] to run an invisible sizing pass.
|
||||||
|
///
|
||||||
|
/// Popups automatically run a sizing pass when they open or reopen. Set this to `true` for
|
||||||
|
/// one frame when the contents of an already open popup change and its cached size may no
|
||||||
|
/// longer fit. Do not leave it enabled continuously, because the popup would remain invisible.
|
||||||
|
///
|
||||||
|
/// Default: `false`.
|
||||||
|
#[inline]
|
||||||
|
pub fn sizing_pass(mut self, sizing_pass: bool) -> Self {
|
||||||
|
self.sizing_pass = sizing_pass;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the id of the Area.
|
/// Set the id of the Area.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn id(mut self, id: Id) -> Self {
|
pub fn id(mut self, id: Id) -> Self {
|
||||||
@@ -556,6 +571,7 @@ impl<'a> Popup<'a> {
|
|||||||
alternative_aligns: _,
|
alternative_aligns: _,
|
||||||
gap,
|
gap,
|
||||||
width,
|
width,
|
||||||
|
sizing_pass,
|
||||||
sense,
|
sense,
|
||||||
interactable,
|
interactable,
|
||||||
layout,
|
layout,
|
||||||
@@ -584,7 +600,7 @@ impl<'a> Popup<'a> {
|
|||||||
.sense(sense)
|
.sense(sense)
|
||||||
.interactable(interactable)
|
.interactable(interactable)
|
||||||
.layout(layout)
|
.layout(layout)
|
||||||
.sizing_pass(!was_open_last_frame)
|
.sizing_pass(sizing_pass || !was_open_last_frame)
|
||||||
.info(info.unwrap_or_else(|| {
|
.info(info.unwrap_or_else(|| {
|
||||||
UiStackInfo::new(kind.into()).with_tag_value(
|
UiStackInfo::new(kind.into()).with_tag_value(
|
||||||
MenuConfig::MENU_CONFIG_TAG,
|
MenuConfig::MENU_CONFIG_TAG,
|
||||||
|
|||||||
@@ -67,6 +67,85 @@ fn reopened_popup_resizes_for_wider_items() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_popup_resizes_after_explicit_sizing_pass() {
|
||||||
|
const POPUP_BUTTON: &str = "Growing popup";
|
||||||
|
const MAX_HEIGHT: f32 = 100.0;
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
item_count: usize,
|
||||||
|
needs_sizing_pass: bool,
|
||||||
|
popup_height: f32,
|
||||||
|
viewport_height: f32,
|
||||||
|
content_height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut harness = Harness::builder()
|
||||||
|
.with_size(egui::Vec2::new(500.0, 300.0))
|
||||||
|
.build_ui_state(
|
||||||
|
|ui, state| {
|
||||||
|
let response = ui.button(POPUP_BUTTON);
|
||||||
|
let needs_sizing_pass = core::mem::take(&mut state.needs_sizing_pass);
|
||||||
|
let item_count = state.item_count;
|
||||||
|
|
||||||
|
if let Some(popup) = Popup::from_response(&response)
|
||||||
|
.sizing_pass(needs_sizing_pass)
|
||||||
|
.show(|ui| {
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.max_height(MAX_HEIGHT)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for index in 0..item_count {
|
||||||
|
ui.label(format!("Item {index}"));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
{
|
||||||
|
state.popup_height = popup.response.rect.height();
|
||||||
|
state.viewport_height = popup.inner.inner_rect.height();
|
||||||
|
state.content_height = popup.inner.content_size.y;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
item_count: 2,
|
||||||
|
needs_sizing_pass: false,
|
||||||
|
popup_height: 0.0,
|
||||||
|
viewport_height: 0.0,
|
||||||
|
content_height: 0.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
harness.run();
|
||||||
|
let initial_popup_height = harness.state().popup_height;
|
||||||
|
|
||||||
|
harness.state_mut().item_count = 20;
|
||||||
|
harness.run();
|
||||||
|
let stale_viewport_height = harness.state().viewport_height;
|
||||||
|
assert!(
|
||||||
|
stale_viewport_height < MAX_HEIGHT,
|
||||||
|
"viewport unexpectedly reached its maximum without a sizing pass"
|
||||||
|
);
|
||||||
|
|
||||||
|
harness.state_mut().needs_sizing_pass = true;
|
||||||
|
harness.run();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
harness.state().popup_height > initial_popup_height,
|
||||||
|
"popup did not grow after an explicit sizing pass"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
harness.state().viewport_height > stale_viewport_height,
|
||||||
|
"scroll viewport did not grow after an explicit sizing pass"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(harness.state().viewport_height - MAX_HEIGHT).abs() <= 0.5,
|
||||||
|
"scroll viewport did not stop at its maximum height"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
harness.state().content_height > harness.state().viewport_height,
|
||||||
|
"popup contents did not remain scrollable at the maximum height"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_interactive_tooltip() {
|
fn test_interactive_tooltip() {
|
||||||
struct State {
|
struct State {
|
||||||
|
|||||||
Reference in New Issue
Block a user