From 5347b0a4ac89c1ff8d59019f40e83652867d7392 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Aug 2026 07:54:06 -0700 Subject: [PATCH] Fix window with a `Grid` being widenable but not shrinkable again (#8386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Related * Fixes a regression from #8152 * Part of #2921 Reported symptom: you can widen the Widget Gallery window, but it won't shrink again. I'm not sure this fix is the best one, but it does work. # Claude says ## Cause A `Grid` gives its **last** column all the available width, so a width-filling widget in it (`Separator`, `TextEdit`, `ProgressBar`, …) makes the `Grid` remember a `col_width` that is really just "however wide we happened to be". At the start of a resize drag, `Resize` runs a one-frame sizing pass (#8152) to measure the minimum content width and clamps the drag against it. But `GridLayout::next_cell` inflated every cell to `prev_state.col_width`, so the `Grid` reported its previous width as its minimum — even though it was only offered `min_size.x`. The clamp is a lower bound, so widening kept working while shrinking was blocked at the widened width. ## Fix During an enclosing sizing pass, don't inflate the stretchy last column to its remembered width, and don't store the measured (narrow) widths. Minimal repro (fails before, passes after — added as a regression test): ```rust Window::new("x").default_width(280.0).show(ctx, |ui| { egui::Grid::new("grid").num_columns(2).show(ui, |ui| { ui.label("Separator"); ui.separator(); // fills the last column ui.end_row(); }); }); ``` `Panel` is unaffected — it clamps only against the user's `min_size`, with no content-min sizing pass. * [x] I have followed the instructions in the PR template 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Lucas Meurer --- crates/egui/src/grid.rs | 16 +++++- crates/egui_kittest/tests/regression_tests.rs | 55 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index e5c20f05f..981025713 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -75,6 +75,11 @@ pub(crate) struct GridLayout { curr_state: State, initial_available: Rect, + /// Are we inside an enclosing sizing pass (e.g. [`crate::Resize`] measuring + /// the minimum content width)? If so we must not remember the (narrow) sizes + /// we measure during it. + sizing_pass: bool, + // Options: num_columns: Option, spacing: Vec2, @@ -90,6 +95,10 @@ pub(crate) struct GridLayout { impl GridLayout { pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option) -> Self { let is_first_frame = prev_state.is_none(); + + // An outer sizing pass, we should render as small as possible. + let sizing_pass = ui.is_sizing_pass(); + let prev_state = prev_state.unwrap_or_default(); // TODO(emilk): respect current layout @@ -110,6 +119,7 @@ impl GridLayout { prev_state, curr_state: State::default(), initial_available, + sizing_pass, num_columns: None, spacing: ui.spacing().item_spacing, @@ -180,7 +190,11 @@ impl GridLayout { } pub(crate) fn next_cell(&self, cursor: Rect, child_size: Vec2) -> Rect { - let width = self.prev_state.col_width(self.col).unwrap_or(0.0); + let width = if self.sizing_pass { + 0.0 + } else { + self.prev_state.col_width(self.col).unwrap_or(0.0) + }; let height = self.prev_row_height(self.row); let size = child_size.max(vec2(width, height)); Rect::from_min_size(cursor.min, size).round_ui() diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 459e2f024..8aad4dea1 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -513,6 +513,61 @@ fn window_resize_wraps_to_content_min_width() { ); } +/// A `Grid` gives its last column all the available width, so a width-filling widget in it +/// (here a `Separator`) makes the grid remember a column width that is really just +/// "however wide the window happened to be". +/// +/// When `Resize` then measures the minimum content width in a sizing pass, that remembered +/// width must not be reported as the minimum — otherwise the window can be widened but +/// never shrunk again. +#[test] +fn window_with_grid_can_shrink_after_being_widened() { + let window_title = "grid_shrink_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_width(280.0) + .show(ui.ctx(), |ui| { + egui::Grid::new("grid").num_columns(2).show(ui, |ui| { + ui.label("Separator"); + ui.separator(); // Fills the available width + ui.end_row(); + }); + }); + }); + harness.run(); + + let drag_right_edge = |harness: &mut Harness<'_>, dx: f32| { + let rect = harness + .get_by_role_and_label(Role::Window, window_title) + .rect(); + let grab = Pos2::new(rect.right(), rect.center().y); + harness.hover_at(grab); + harness.run(); + harness.drag_at(grab); + harness.run(); + harness.hover_at(grab + Vec2::new(dx, 0.0)); + harness.run(); + harness.drop_at(grab + Vec2::new(dx, 0.0)); + harness.run(); + harness + .get_by_role_and_label(Role::Window, window_title) + .rect() + .width() + }; + + let widened = drag_right_edge(&mut harness, 300.0); + let shrunk = drag_right_edge(&mut harness, -300.0); + + assert!( + shrunk < widened - 200.0, + "window could not be shrunk again after being widened: \ + widened to {widened}, then only shrunk to {shrunk}" + ); +} + /// Ensure that the size passed to window is actually treated as outer size (including /// margins and borders). #[test]