mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Don't allow resizing Window past minimum content size (#8152)
## Related - part of #7264 (required for Atoms in window titlebar) - part of https://github.com/emilk/egui/issues/2921 ## What This implements a fix for this weird edge case when resizing windows past it's contents minimum allocated width: Before: https://github.com/user-attachments/assets/33c6c7b2-3621-4eba-8122-99a3930ff67b After: https://github.com/user-attachments/assets/5dd47d8f-32bb-4463-aa01-3a5c8f39b10e There is a very slight flicker on the very first frame where we detect the minimum size. We could cover this with a request_discard, but in practise it should be barely noticable. --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
@@ -17,6 +17,14 @@ pub(crate) struct State {
|
||||
|
||||
/// Externally requested size (e.g. by Window) for the next frame
|
||||
pub(crate) requested_size: Option<Vec2>,
|
||||
|
||||
/// Minimum content width observed during the current interactive resize.
|
||||
/// `None` until content overflows the offered rect. At that point we record
|
||||
/// `last_content_size.x` here and clamp `desired_size.x` against it for the
|
||||
/// rest of the drag, so wrapping/centered widgets see the same width the window will
|
||||
/// end up clamped to. Reset to `None` whenever a drag is not in progress.
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
observed_min_content_width: Option<f32>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -221,6 +229,7 @@ impl Resize {
|
||||
desired_size: default_size,
|
||||
last_content_size: vec2(0.0, 0.0),
|
||||
requested_size: None,
|
||||
observed_min_content_width: None,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -242,13 +251,20 @@ impl Resize {
|
||||
user_requested_size = Some(pointer_pos - position + 0.5 * corner_response.rect.size());
|
||||
}
|
||||
|
||||
if let Some(user_requested_size) = user_requested_size {
|
||||
if let Some(mut user_requested_size) = user_requested_size {
|
||||
// We know the minimum width from a previous frame, so lets not shrink past that
|
||||
if let Some(observed_min) = state.observed_min_content_width {
|
||||
user_requested_size.x = user_requested_size.x.at_least(observed_min);
|
||||
}
|
||||
state.desired_size = user_requested_size;
|
||||
} else {
|
||||
// We are not being actively resized, so auto-expand to include size of last frame.
|
||||
// This prevents auto-shrinking if the contents contain width-filling widgets (separators etc)
|
||||
// but it makes a lot of interactions with [`Window`]s nicer.
|
||||
state.desired_size = state.desired_size.max(state.last_content_size);
|
||||
// No active drag, discard the observed min width, we'll rediscover on next drag,
|
||||
// in case content changed.
|
||||
state.observed_min_content_width = None;
|
||||
}
|
||||
|
||||
state.desired_size = state
|
||||
@@ -305,6 +321,15 @@ impl Resize {
|
||||
|
||||
state.last_content_size = content_ui.min_size();
|
||||
|
||||
// The content overflowed the rect we provided. This means last_content_size.x is now at the
|
||||
// very minimum size the content will allow to shrink to. We remember this so we can prevent
|
||||
// any previous wrapping content from shrinking further.
|
||||
// 4.0 is a bit of safety margin to ensure we don't prevent shrinking on rounding errors.
|
||||
let overflowed_x = state.last_content_size.x > state.desired_size.x + 4.0;
|
||||
if overflowed_x && state.observed_min_content_width.is_none() {
|
||||
state.observed_min_content_width = Some(state.last_content_size.x);
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
|
||||
let mut size = state.last_content_size;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use egui::accesskit::{self, Role};
|
||||
use egui::{Button, ComboBox, Image, Modifiers, Popup, Rect, Vec2, Widget as _};
|
||||
use egui::{
|
||||
Button, ComboBox, Image, Label, Modifiers, Popup, Pos2, Rect, Vec2, Widget as _, Window,
|
||||
};
|
||||
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
||||
use egui_kittest::SnapshotResults;
|
||||
use egui_kittest::{Harness, kittest::Queryable as _};
|
||||
@@ -454,3 +456,58 @@ pub fn pointer_click_on_open_submenu_button_should_not_close_it() {
|
||||
"Expected submenu to remain open on repeated pointer click"
|
||||
);
|
||||
}
|
||||
|
||||
/// This test checks if we correctly handle wrapping content proceeding non-wrapping content
|
||||
/// during window resize. When the window is resized past non-wrapping content, the wrapping content
|
||||
/// above should stay at that non wrapping width and not wrap any further.
|
||||
#[test]
|
||||
fn window_resize_wraps_to_content_min_width() {
|
||||
let wrap_text = "This label should wrap as the window is narrowed. \
|
||||
It should not shrink smaller than the bottom labels width though.";
|
||||
let non_wrap_text = "This is the bottom non-wrapping label which is wider.";
|
||||
|
||||
let window_title = "resize_wrap_regression";
|
||||
let mut harness = Harness::builder()
|
||||
.with_size(Vec2::new(800.0, 600.0))
|
||||
.build_ui(move |ui| {
|
||||
Window::new(window_title)
|
||||
.default_pos([20.0, 20.0])
|
||||
.default_size([400.0, 200.0])
|
||||
.show(ui.ctx(), |ui| {
|
||||
ui.add(Label::new(wrap_text).wrap());
|
||||
ui.add(Label::new(non_wrap_text).extend());
|
||||
});
|
||||
});
|
||||
|
||||
harness.run();
|
||||
|
||||
let window_rect = harness
|
||||
.get_by_role_and_label(Role::Window, window_title)
|
||||
.rect();
|
||||
|
||||
// Drag the right edge inward, well past the non-wrapping label's natural
|
||||
// width, so the non-wrapping label pins the window's minimum width while
|
||||
// the wrapping label would (without the fix) keep shrinking.
|
||||
let grab = Pos2::new(window_rect.right(), window_rect.center().y);
|
||||
let target = Pos2::new(window_rect.left() + 80.0, window_rect.center().y);
|
||||
|
||||
harness.drag_at(grab);
|
||||
harness.run();
|
||||
harness.hover_at(target);
|
||||
|
||||
harness.run();
|
||||
|
||||
let wrap_width = harness.get_by_label(wrap_text).rect().width();
|
||||
let non_wrap_width = harness.get_by_label(non_wrap_text).rect().width();
|
||||
|
||||
// Wrapped text won't perfectly fill the available width — each line ends
|
||||
// wherever the next word stops fitting. The tolerance absorbs that
|
||||
// word-break slack while still catching the bug, where the wrap label
|
||||
// would be substantially narrower than the non-wrapping label.
|
||||
assert!(
|
||||
non_wrap_width - wrap_width < 40.0,
|
||||
"wrapping label width ({wrap_width}) is much narrower than the \
|
||||
non-wrapping label width ({non_wrap_width}) after shrinking the \
|
||||
window past the non-wrapping label's natural width"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user