From 9dcfec47bee0b509682cb6f7e8f516318dcca134 Mon Sep 17 00:00:00 2001 From: Enzo Novoselic <41305715+StarryWorm@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:07:54 -0400 Subject: [PATCH 1/2] Fix some `Container`s not allocating desired sizes Make `GridContainer` respect desired sizes Make `BoxContainer` respect desired sizes Make `FlowContainer` respect desired sizes --- scene/gui/box_container.cpp | 105 +++++++++++++++++++++-------------- scene/gui/flow_container.cpp | 12 +++- scene/gui/grid_container.cpp | 60 +++++++++++++++++++- 3 files changed, 133 insertions(+), 44 deletions(-) diff --git a/scene/gui/box_container.cpp b/scene/gui/box_container.cpp index fba334fbf8b1..fd0d3950875b 100644 --- a/scene/gui/box_container.cpp +++ b/scene/gui/box_container.cpp @@ -37,25 +37,26 @@ struct _MinSizeCache { int min_size = 0; + int desired_size = 0; int max_size = -1; + real_t stretch_ratio = 0; bool will_stretch = false; int final_size = 0; }; void BoxContainer::_resort() { - /** First pass, determine minimum size AND amount of stretchable elements */ - Size2i new_size = get_size(); - Size2 combined_max_size = get_combined_maximum_size(); + Size2i combined_max_size = get_combined_maximum_size(); bool propagating_max_size = vertical ? is_propagating_maximum_size() && combined_max_size.height >= 0 : is_propagating_maximum_size() && combined_max_size.width >= 0; bool rtl = is_layout_rtl(); bool first = true; int children_count = 0; - int stretch_min = 0; - int stretch_avail = 0; + int combined_min = 0; + int stretch_space = 0; float stretch_ratio_total = 0.0; + int desired_extra_space = 0; HashMap min_size_cache; for (int i = 0; i < get_child_count(); i++) { @@ -68,30 +69,31 @@ void BoxContainer::_resort() { c->set_parent_maximum_size_cache(combined_max_size); } Size2i min_size = c->get_bound_minimum_size().ceil(); - Size2i max_size = c->get_combined_maximum_size(); + Size2i desired_size = c->get_bound_desired_size().ceil(); + Size2i max_size = c->get_combined_maximum_size().floor(); _MinSizeCache msc; if (vertical) { /* VERTICAL */ - stretch_min += min_size.height; msc.min_size = min_size.height; + msc.desired_size = desired_size.height; msc.max_size = max_size.height; msc.will_stretch = c->get_v_size_flags().has_flag(SIZE_EXPAND); } else { /* HORIZONTAL */ - stretch_min += min_size.width; msc.min_size = min_size.width; + msc.desired_size = desired_size.width; msc.max_size = max_size.width; msc.will_stretch = c->get_h_size_flags().has_flag(SIZE_EXPAND); } - if (msc.max_size >= 0 && msc.max_size < msc.min_size) { - msc.max_size = msc.min_size; - } - if (msc.will_stretch) { - stretch_avail += msc.min_size; + stretch_space += msc.min_size; stretch_ratio_total += c->get_stretch_ratio(); } + + combined_min += msc.min_size; + desired_extra_space += msc.desired_size - msc.min_size; + msc.final_size = msc.min_size; min_size_cache[c] = msc; children_count++; @@ -101,27 +103,50 @@ void BoxContainer::_resort() { return; } - int stretch_max = (vertical ? new_size.height : new_size.width) - (children_count - 1) * theme_cache.separation; - int stretch_diff = stretch_max - stretch_min; + int max_space = (vertical ? new_size.height : new_size.width); + if (propagating_max_size) { + max_space = MIN(max_space, vertical ? combined_max_size.height : combined_max_size.width); + } + max_space -= theme_cache.separation * (children_count - 1); + int stretch_diff = max_space - combined_min; if (stretch_diff < 0) { //avoid negative stretch space stretch_diff = 0; } - stretch_avail += stretch_diff; //available stretch space. - /** Second, pass successively to discard elements that can't be stretched, this will run while stretchable - elements exist */ + stretch_space += stretch_diff; //available stretch space. - if (propagating_max_size) { - if (vertical) { - stretch_avail = MIN(stretch_avail, combined_max_size.height); - } else { - stretch_avail = MIN(stretch_avail, combined_max_size.width); + // First, allocate extra space to Controls which have a desired size larger than their minimum size, up to their desired size, in proportion to how much extra space they want. + if (stretch_space > 0 && desired_extra_space > 0) { + real_t space_available_ratio = MIN(real_t(stretch_space) / real_t(desired_extra_space), 1.0); + + for (Node *child : iterate_children()) { + Control *c = as_sortable_control(child, SortableVisibilityMode::VISIBLE); + if (!c) { + continue; + } + + _MinSizeCache &msc = min_size_cache[c]; + + if (msc.desired_size > msc.min_size) { + int desired_size_increase = floor((msc.desired_size - msc.min_size) * space_available_ratio); + + if (msc.will_stretch) { + // Increase the minimum size to reflect the desired size allocation, so that the SIZE_EXPAND allocation does not shrink them back down. + msc.min_size += desired_size_increase; + } else { + // If the Control isn't stretchable + stretch_space -= desired_size_increase; + } + + msc.final_size += desired_size_increase; + } } } - while (stretch_ratio_total > 0) { // first of all, don't even be here if no stretchable objects exist - bool refit_successful = true; //assume refit-test will go well + // Second, allocate stretch space to Controls with the SIZE_EXPAND flag, in proportion to their stretch ratio. + while (stretch_ratio_total > 0) { + bool refit_successful = true; float error = 0.0; // Keep track of accumulated error in pixels for (int i = 0; i < get_child_count(); i++) { @@ -133,37 +158,35 @@ void BoxContainer::_resort() { ERR_FAIL_COND(!min_size_cache.has(c)); _MinSizeCache &msc = min_size_cache[c]; - if (msc.will_stretch) { //wants to stretch - //let's see if it can really stretch + if (msc.will_stretch) { float stretch_ratio = c->get_stretch_ratio(); - float final_pixel_size = stretch_avail * stretch_ratio / stretch_ratio_total; - // Add leftover fractional pixels to error accumulator + float final_pixel_size = stretch_space * stretch_ratio / stretch_ratio_total; + + // Add leftover fractional pixels to error accumulator and dump if greater than 1. error += final_pixel_size - (int)final_pixel_size; + if (error >= 1) { + final_pixel_size += 1; + error -= 1; + } + if (final_pixel_size < msc.min_size) { - //if available stretching area is too small for widget, - //then remove it from stretching area + // If stretching would make the Control smaller than its minimum size, cap it and redistribute its unused share. msc.will_stretch = false; stretch_ratio_total -= stretch_ratio; refit_successful = false; - stretch_avail -= msc.min_size; + stretch_space -= msc.min_size; msc.final_size = msc.min_size; break; } else if (msc.max_size >= 0 && final_pixel_size > msc.max_size) { - // If stretching would exceed the Control's maximum size, - // cap it and redistribute its unused share. + // If stretching would exceed the Control's maximum size, cap it and redistribute its unused share. msc.will_stretch = false; stretch_ratio_total -= stretch_ratio; refit_successful = false; - stretch_avail -= msc.max_size; + stretch_space -= msc.max_size; msc.final_size = msc.max_size; break; } else { msc.final_size = final_pixel_size; - // Dump accumulated error if one pixel or more - if (error >= 1 && (msc.max_size < 0 || msc.final_size < msc.max_size)) { - msc.final_size += 1; - error -= 1; - } } } } @@ -176,7 +199,7 @@ void BoxContainer::_resort() { /** Final pass, draw and stretch elements **/ int ofs = 0; - int final_stretch_diff = stretch_max - stretch_min; + int final_stretch_diff = max_space - combined_min; for (int i = 0; i < get_child_count(); i++) { Control *c = as_sortable_control(get_child(i)); if (!c) { diff --git a/scene/gui/flow_container.cpp b/scene/gui/flow_container.cpp index cf900308e3eb..b969e174b827 100644 --- a/scene/gui/flow_container.cpp +++ b/scene/gui/flow_container.cpp @@ -72,7 +72,8 @@ void FlowContainer::_resort() { continue; } - Size2i child_msc = child->get_bound_minimum_size(); + // Since we are in a FlowContainer, children will always have up to the full width/height available to them, so we can use the desired size as the minimum size for layout purposes. + Size2i child_msc = child->get_bound_desired_size(); Size2i child_max_size = child->get_combined_maximum_size(); if (vertical) { /* VERTICAL */ @@ -332,6 +333,15 @@ void FlowContainer::_resort() { child_rect.position.x = get_rect().size.x - child_rect.position.x - child_rect.size.width; } + // Ensure that the child does not exceed the container's size in the flow direction. + // This will only ever apply in the case of having a single child in a line that is larger than the container's minimum size, i.e. a child with a desired size greater than its own minimum size. + // This will result in the child being given a size between its minimum size and its desired size, which is the expected behavior. + if (vertical) { + child_rect.size.height = MIN(child_rect.size.height, current_container_size - ofs.y); + } else { + child_rect.size.width = MIN(child_rect.size.width, current_container_size - ofs.x); + } + fit_child_in_rect(child, child_rect); if (vertical) { /* VERTICAL */ diff --git a/scene/gui/grid_container.cpp b/scene/gui/grid_container.cpp index 5e284e7e92e9..e190581adfa8 100644 --- a/scene/gui/grid_container.cpp +++ b/scene/gui/grid_container.cpp @@ -38,6 +38,8 @@ void GridContainer::_resort() { RBMap col_minw; // Max of min_width of all controls in each col (indexed by col). RBMap row_minh; // Max of min_height of all controls in each row (indexed by row). + RBMap col_desiredw; // Max of desired_width of all controls in each col (indexed by col). + RBMap row_desiredh; // Max of desired_height of all controls in each row (indexed by row). RBMap col_maxw; // Min positive max_width of all controls in each col (indexed by col). RBMap row_maxh; // Min positive max_height of all controls in each row (indexed by row). RBMap col_fixed_size; // Final fixed width for non-expanded columns. @@ -63,8 +65,9 @@ void GridContainer::_resort() { if (is_propagating_maximum_size()) { c->set_parent_maximum_size_cache(combined_max_size); } - Size2i ms = c->get_bound_minimum_size(); - Size2 max_size = c->get_combined_maximum_size(); + Size2i ms = c->get_bound_minimum_size().ceil(); + Size2i desired_size = c->get_bound_desired_size().ceil(); + Size2i max_size = c->get_combined_maximum_size().floor(); if (col_minw.has(col)) { col_minw[col] = MAX(col_minw[col], ms.width); } else { @@ -76,6 +79,17 @@ void GridContainer::_resort() { row_minh[row] = ms.height; } + if (col_desiredw.has(col)) { + col_desiredw[col] = MAX(col_desiredw[col], desired_size.width); + } else { + col_desiredw[col] = desired_size.width; + } + if (row_desiredh.has(row)) { + row_desiredh[row] = MAX(row_desiredh[row], desired_size.height); + } else { + row_desiredh[row] = desired_size.height; + } + int max_width = int(max_size.width) >= 0 ? int(max_size.width) : (combined_max_size.width >= 0 ? combined_max_size.width : size.width); if (col_maxw.has(col)) { col_maxw[col] = MAX(col_maxw[col], max_width); @@ -134,6 +148,48 @@ void GridContainer::_resort() { remaining_space.height -= theme_cache.v_separation * MAX(max_row - 1, 0); remaining_space.width -= theme_cache.h_separation * MAX(max_col - 1, 0); + // Distribute the remaining space to cols/rows which have a desired size larger than their minimum size, up to their desired size, in proportion to how much extra space they want. + int total_desired_extra_space = 0; + for (const KeyValue &E : col_desiredw) { + int desired_extra_space = E.value - col_minw[E.key]; + if (desired_extra_space > 0) { + total_desired_extra_space += desired_extra_space; + } + } + if (remaining_space.width > 0 && total_desired_extra_space > 0) { + real_t space_available_ratio = MIN(real_t(remaining_space.width) / real_t(total_desired_extra_space), 1.0); + for (const KeyValue &E : col_desiredw) { + int desired_extra_space = E.value - col_minw[E.key]; + if (desired_extra_space > 0) { + int desired_size_increase = floor(desired_extra_space * space_available_ratio); + col_minw[E.key] += desired_size_increase; + if (!col_expanded.has(E.key)) { + remaining_space.width -= desired_size_increase; + } + } + } + } + total_desired_extra_space = 0; + for (const KeyValue &E : row_desiredh) { + int desired_extra_space = E.value - row_minh[E.key]; + if (desired_extra_space > 0) { + total_desired_extra_space += desired_extra_space; + } + } + if (remaining_space.height > 0 && total_desired_extra_space > 0) { + real_t space_available_ratio = MIN(real_t(remaining_space.height) / real_t(total_desired_extra_space), 1.0); + for (const KeyValue &E : row_desiredh) { + int desired_extra_space = E.value - row_minh[E.key]; + if (desired_extra_space > 0) { + int desired_size_increase = floor(desired_extra_space * space_available_ratio); + row_minh[E.key] += desired_size_increase; + if (!row_expanded.has(E.key)) { + remaining_space.height -= desired_size_increase; + } + } + } + } + bool can_fit = false; while (!can_fit && col_expanded.size() > 0) { // Check if all minwidth constraints are OK if we use the remaining space. From 02d671b3dd379ba7a1fcbdba57bcf6faffbe74f2 Mon Sep 17 00:00:00 2001 From: Enzo Novoselic <41305715+StarryWorm@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:38:05 -0400 Subject: [PATCH 2/2] Fix `Label` not autowrapping with max size --- scene/gui/control.cpp | 5 +++++ scene/gui/control.h | 1 + scene/gui/label.cpp | 6 +++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 26fc944743a9..a0a55b9e2a41 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -2095,6 +2095,11 @@ void Control::grow_to_desired_size() { } } +bool Control::is_expanded_by_desired_size() const { + ERR_READ_THREAD_GUARD_V(false); + return data.expanded_by_desired_size; +} + void Control::add_child_notify(Node *p_child) { CanvasItem::add_child_notify(p_child); diff --git a/scene/gui/control.h b/scene/gui/control.h index 44709c6508c5..a54ee635c96f 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -618,6 +618,7 @@ class Control : public CanvasItem { void update_desired_size(); void grow_to_desired_size(); + bool is_expanded_by_desired_size() const; void set_block_maximum_size_adjust(bool p_block); void set_block_minimum_size_adjust(bool p_block); diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp index 4de80ad74034..768b15002ae5 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -154,7 +154,11 @@ void Label::_shape() const { if (maximum_width <= 0) { maximum_width = 1; } - width = maximum_width; + if (width > 0 && !is_expanded_by_desired_size()) { + width = MIN(width, maximum_width); + } else { + width = maximum_width; + } } if (text_dirty) {