|
@@ -1,5 +1,6 @@
|
|
|
/*
|
|
|
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
|
|
+ * Copyright (c) 2022, Frhun <serenitystuff@frhun.de>
|
|
|
*
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
*/
|
|
@@ -142,94 +143,211 @@ void BoxLayout::run(Widget& widget)
|
|
|
|
|
|
struct Item {
|
|
|
Widget* widget { nullptr };
|
|
|
- int min_size { -1 };
|
|
|
- int max_size { -1 };
|
|
|
+ UIDimension min_size { SpecialDimension::Shrink };
|
|
|
+ UIDimension max_size { SpecialDimension::Grow };
|
|
|
+ UIDimension preferred_size { SpecialDimension::Shrink };
|
|
|
int size { 0 };
|
|
|
bool final { false };
|
|
|
};
|
|
|
|
|
|
Vector<Item, 32> items;
|
|
|
+ int opportunistic_growth_item_count = 0;
|
|
|
+ int opportunistic_growth_items_base_size_total = 0;
|
|
|
|
|
|
for (size_t i = 0; i < m_entries.size(); ++i) {
|
|
|
auto& entry = m_entries[i];
|
|
|
if (entry.type == Entry::Type::Spacer) {
|
|
|
- items.append(Item { nullptr, -1, -1 });
|
|
|
+ items.append(Item { nullptr, { SpecialDimension::Shrink }, { SpecialDimension::Grow }, { SpecialDimension::Grow } });
|
|
|
continue;
|
|
|
}
|
|
|
if (!entry.widget)
|
|
|
continue;
|
|
|
if (!entry.widget->is_visible())
|
|
|
continue;
|
|
|
- auto min_size = entry.widget->min_size();
|
|
|
- auto max_size = entry.widget->max_size();
|
|
|
-
|
|
|
- if (entry.widget->is_shrink_to_fit() && entry.widget->layout()) {
|
|
|
- auto preferred_size = entry.widget->layout()->preferred_size();
|
|
|
- min_size = max_size = preferred_size;
|
|
|
+ auto min_size = entry.widget->effective_min_size().primary_size_for_orientation(orientation());
|
|
|
+ auto max_size = entry.widget->max_size().primary_size_for_orientation(orientation());
|
|
|
+ auto preferred_size = entry.widget->effective_preferred_size().primary_size_for_orientation(orientation());
|
|
|
+
|
|
|
+ if (preferred_size == SpecialDimension::OpportunisticGrow) {
|
|
|
+ opportunistic_growth_item_count++;
|
|
|
+ opportunistic_growth_items_base_size_total += MUST(min_size.shrink_value());
|
|
|
+ } else {
|
|
|
+ preferred_size = clamp(preferred_size, min_size, max_size);
|
|
|
}
|
|
|
|
|
|
- items.append(Item { entry.widget.ptr(), min_size.primary_size_for_orientation(orientation()), max_size.primary_size_for_orientation(orientation()) });
|
|
|
+ items.append(
|
|
|
+ Item {
|
|
|
+ entry.widget.ptr(),
|
|
|
+ min_size,
|
|
|
+ max_size,
|
|
|
+ preferred_size });
|
|
|
}
|
|
|
|
|
|
if (items.is_empty())
|
|
|
return;
|
|
|
|
|
|
Gfx::IntRect content_rect = widget.content_rect();
|
|
|
- int available_size = content_rect.size().primary_size_for_orientation(orientation()) - spacing() * (items.size() - 1);
|
|
|
- int unfinished_items = items.size();
|
|
|
-
|
|
|
- if (orientation() == Gfx::Orientation::Horizontal)
|
|
|
- available_size -= margins().left() + margins().right();
|
|
|
- else
|
|
|
- available_size -= margins().top() + margins().bottom();
|
|
|
+ int uncommitted_size = content_rect.size().primary_size_for_orientation(orientation())
|
|
|
+ - spacing() * (items.size() - 1)
|
|
|
+ - margins().primary_total_for_orientation(orientation());
|
|
|
+ int unfinished_regular_items = items.size() - opportunistic_growth_item_count;
|
|
|
+ int max_amongst_the_min_sizes = 0;
|
|
|
+ int max_amongst_the_min_sizes_of_opportunistically_growing_items = 0;
|
|
|
+ int regular_items_to_layout = 0;
|
|
|
+ int regular_items_min_size_total = 0;
|
|
|
|
|
|
// Pass 1: Set all items to their minimum size.
|
|
|
for (auto& item : items) {
|
|
|
- item.size = 0;
|
|
|
- if (item.min_size >= 0)
|
|
|
- item.size = item.min_size;
|
|
|
- available_size -= item.size;
|
|
|
+ VERIFY(item.min_size.is_one_of(SpecialDimension::Regular, SpecialDimension::Shrink));
|
|
|
+ item.size = MUST(item.min_size.shrink_value());
|
|
|
+ uncommitted_size -= item.size;
|
|
|
|
|
|
- if (item.min_size >= 0 && item.max_size >= 0 && item.min_size == item.max_size) {
|
|
|
+ if (item.min_size.is_int() && item.max_size.is_int() && item.min_size == item.max_size) {
|
|
|
// Fixed-size items finish immediately in the first pass.
|
|
|
item.final = true;
|
|
|
- --unfinished_items;
|
|
|
+ if (item.preferred_size == SpecialDimension::OpportunisticGrow) {
|
|
|
+ opportunistic_growth_item_count--;
|
|
|
+ opportunistic_growth_items_base_size_total -= MUST(item.min_size.shrink_value());
|
|
|
+ } else {
|
|
|
+ --unfinished_regular_items;
|
|
|
+ }
|
|
|
+ } else if (item.preferred_size != SpecialDimension::OpportunisticGrow && item.widget) {
|
|
|
+ max_amongst_the_min_sizes = max(max_amongst_the_min_sizes, MUST(item.min_size.shrink_value()));
|
|
|
+ regular_items_to_layout++;
|
|
|
+ regular_items_min_size_total += item.size;
|
|
|
+ } else if (item.preferred_size == SpecialDimension::OpportunisticGrow) {
|
|
|
+ max_amongst_the_min_sizes_of_opportunistically_growing_items = max(max_amongst_the_min_sizes_of_opportunistically_growing_items, MUST(item.min_size.shrink_value()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Pass 2: Set all non final, non spacer items to the previously encountered maximum min_size of these kind of items
|
|
|
+ // This is done to ensure even growth, if the items don't have the same min_size, which most won't have.
|
|
|
+ // If you are unsure what effect this has, try looking at widget gallery with, and without this, it'll be obvious.
|
|
|
+ if (uncommitted_size > 0) {
|
|
|
+ int total_growth_if_not_overcommitted = regular_items_to_layout * max_amongst_the_min_sizes - regular_items_min_size_total;
|
|
|
+ int overcommitment_if_all_same_min_size = total_growth_if_not_overcommitted - uncommitted_size;
|
|
|
+ for (auto& item : items) {
|
|
|
+ if (item.final || item.preferred_size == SpecialDimension::OpportunisticGrow || !item.widget)
|
|
|
+ continue;
|
|
|
+ int extra_needed_space = max_amongst_the_min_sizes - item.size;
|
|
|
+
|
|
|
+ if (overcommitment_if_all_same_min_size > 0) {
|
|
|
+ extra_needed_space -= (overcommitment_if_all_same_min_size * extra_needed_space + (total_growth_if_not_overcommitted - 1)) / (total_growth_if_not_overcommitted);
|
|
|
+ }
|
|
|
+
|
|
|
+ VERIFY(extra_needed_space >= 0);
|
|
|
+ VERIFY(uncommitted_size >= extra_needed_space);
|
|
|
+
|
|
|
+ item.size += extra_needed_space;
|
|
|
+ if (item.max_size.is_int() && item.size > item.max_size.as_int())
|
|
|
+ item.size = item.max_size.as_int();
|
|
|
+ uncommitted_size -= item.size - MUST(item.min_size.shrink_value());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Pass 2: Distribute remaining available size evenly, respecting each item's maximum size.
|
|
|
- while (unfinished_items && available_size > 0) {
|
|
|
- int slice = available_size / unfinished_items;
|
|
|
- // If available_size does not divide evenly by unfinished_items,
|
|
|
+ // Pass 3: Determine final item size for non spacers, and non opportunisticially growing widgets
|
|
|
+ int loop_counter = 0; // This doubles as a safeguard for when the loop below doesn't finish for some reason, and as a mechanism to ensure it runs at least once.
|
|
|
+ // This has to run at least once, to handle the case where the loop for evening out the min sizes was in an overcommitted state,
|
|
|
+ // and gave the Widget a larger size than its preferred size.
|
|
|
+ while (unfinished_regular_items && (uncommitted_size > 0 || loop_counter++ == 0)) {
|
|
|
+ VERIFY(loop_counter < 100);
|
|
|
+ int slice = uncommitted_size / unfinished_regular_items;
|
|
|
+ // If uncommitted_size does not divide evenly by unfinished_regular_items,
|
|
|
// there are some extra pixels that have to be distributed.
|
|
|
- int pixels = available_size - slice * unfinished_items;
|
|
|
- available_size = 0;
|
|
|
+ int pixels = uncommitted_size - slice * unfinished_regular_items;
|
|
|
+ uncommitted_size = 0;
|
|
|
|
|
|
for (auto& item : items) {
|
|
|
if (item.final)
|
|
|
continue;
|
|
|
+ if (!item.widget)
|
|
|
+ continue;
|
|
|
+ if (item.preferred_size == SpecialDimension::OpportunisticGrow)
|
|
|
+ continue;
|
|
|
|
|
|
int pixel = pixels ? 1 : 0;
|
|
|
pixels -= pixel;
|
|
|
int item_size_with_full_slice = item.size + slice + pixel;
|
|
|
- item.size = item_size_with_full_slice;
|
|
|
|
|
|
- if (item.max_size >= 0)
|
|
|
- item.size = min(item.max_size, item_size_with_full_slice);
|
|
|
+ UIDimension resulting_size { 0 };
|
|
|
+ resulting_size = max(item.size, item_size_with_full_slice);
|
|
|
+ resulting_size = min(resulting_size, item.preferred_size);
|
|
|
+ resulting_size = min(resulting_size, item.max_size);
|
|
|
+
|
|
|
+ if (resulting_size.is_shrink()) {
|
|
|
+ // FIXME: Propagate this error, so it is obvious where the mistake is actually made.
|
|
|
+ if (!item.min_size.is_int())
|
|
|
+ dbgln("BoxLayout: underconstrained widget set to zero size: {} {}", item.widget->class_name(), item.widget->name());
|
|
|
+ resulting_size = MUST(item.min_size.shrink_value());
|
|
|
+ item.final = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (resulting_size.is_grow())
|
|
|
+ resulting_size = item_size_with_full_slice;
|
|
|
+
|
|
|
+ item.size = resulting_size.as_int();
|
|
|
|
|
|
- // If the slice was more than we needed, return remained to available_size.
|
|
|
- int remainder_to_give_back = item_size_with_full_slice - item.size;
|
|
|
- available_size += remainder_to_give_back;
|
|
|
+ // If the slice was more than we needed, return remainder to available_size.
|
|
|
+ // Note that this will in some cases even return more than the slice size.
|
|
|
+ uncommitted_size += item_size_with_full_slice - item.size;
|
|
|
|
|
|
- if (item.max_size >= 0 && item.size == item.max_size) {
|
|
|
- // We've hit the item's max size. Don't give it any more space.
|
|
|
+ if (item.final
|
|
|
+ || (item.max_size.is_int() && item.max_size.as_int() == item.size)
|
|
|
+ || (item.preferred_size.is_int() && item.preferred_size.as_int() == item.size)) {
|
|
|
item.final = true;
|
|
|
- --unfinished_items;
|
|
|
+ --unfinished_regular_items;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Pass 3: Place the widgets.
|
|
|
+ // Pass 4: Even out min_size for opportunistically growing items, analogous to pass 2
|
|
|
+ if (uncommitted_size > 0 && opportunistic_growth_item_count > 0) {
|
|
|
+ int total_growth_if_not_overcommitted = opportunistic_growth_item_count * max_amongst_the_min_sizes_of_opportunistically_growing_items - opportunistic_growth_items_base_size_total;
|
|
|
+ int overcommitment_if_all_same_min_size = total_growth_if_not_overcommitted - uncommitted_size;
|
|
|
+ for (auto& item : items) {
|
|
|
+ if (item.final || item.preferred_size != SpecialDimension::OpportunisticGrow || !item.widget)
|
|
|
+ continue;
|
|
|
+ int extra_needed_space = max_amongst_the_min_sizes_of_opportunistically_growing_items - item.size;
|
|
|
+
|
|
|
+ if (overcommitment_if_all_same_min_size > 0 && total_growth_if_not_overcommitted > 0) {
|
|
|
+ extra_needed_space -= (overcommitment_if_all_same_min_size * extra_needed_space + (total_growth_if_not_overcommitted - 1)) / (total_growth_if_not_overcommitted);
|
|
|
+ }
|
|
|
+
|
|
|
+ VERIFY(extra_needed_space >= 0);
|
|
|
+ VERIFY(uncommitted_size >= extra_needed_space);
|
|
|
+
|
|
|
+ item.size += extra_needed_space;
|
|
|
+ if (item.max_size.is_int() && item.size > item.max_size.as_int())
|
|
|
+ item.size = item.max_size.as_int();
|
|
|
+ uncommitted_size -= item.size - MUST(item.min_size.shrink_value());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ loop_counter = 0;
|
|
|
+ // Pass 5: Determine the size for the opportunistically growing items.
|
|
|
+ while (opportunistic_growth_item_count > 0 && uncommitted_size > 0) {
|
|
|
+ VERIFY(loop_counter++ < 200);
|
|
|
+ int opportunistic_growth_item_extra_size = uncommitted_size / opportunistic_growth_item_count;
|
|
|
+ int pixels = uncommitted_size - opportunistic_growth_item_count * opportunistic_growth_item_extra_size;
|
|
|
+ VERIFY(pixels >= 0);
|
|
|
+ for (auto& item : items) {
|
|
|
+ if (item.preferred_size != SpecialDimension::OpportunisticGrow || item.final || !item.widget)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ int pixel = (pixels > 0 ? 1 : 0);
|
|
|
+ pixels -= pixel;
|
|
|
+ int previous_size = item.size;
|
|
|
+ item.size += opportunistic_growth_item_extra_size + pixel;
|
|
|
+ if (item.max_size.is_int() && item.size >= item.max_size.as_int()) {
|
|
|
+ item.size = item.max_size.as_int();
|
|
|
+ item.final = true;
|
|
|
+ opportunistic_growth_item_count--;
|
|
|
+ }
|
|
|
+ uncommitted_size -= item.size - previous_size;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Pass 6: Place the widgets.
|
|
|
int current_x = margins().left() + content_rect.x();
|
|
|
int current_y = margins().top() + content_rect.y();
|
|
|
|
|
@@ -242,15 +360,18 @@ void BoxLayout::run(Widget& widget)
|
|
|
|
|
|
if (item.widget) {
|
|
|
int secondary = widget.content_size().secondary_size_for_orientation(orientation());
|
|
|
-
|
|
|
- int min_secondary = item.widget->min_size().secondary_size_for_orientation(orientation());
|
|
|
- int max_secondary = item.widget->max_size().secondary_size_for_orientation(orientation());
|
|
|
- if (min_secondary >= 0)
|
|
|
- secondary = max(secondary, min_secondary);
|
|
|
- if (max_secondary >= 0)
|
|
|
- secondary = min(secondary, max_secondary);
|
|
|
secondary -= margins().secondary_total_for_orientation(orientation());
|
|
|
|
|
|
+ UIDimension min_secondary = item.widget->effective_min_size().secondary_size_for_orientation(orientation());
|
|
|
+ UIDimension max_secondary = item.widget->max_size().secondary_size_for_orientation(orientation());
|
|
|
+ UIDimension preferred_secondary = item.widget->effective_preferred_size().secondary_size_for_orientation(orientation());
|
|
|
+ if (preferred_secondary.is_int())
|
|
|
+ secondary = min(secondary, preferred_secondary.as_int());
|
|
|
+ if (min_secondary.is_int())
|
|
|
+ secondary = max(secondary, min_secondary.as_int());
|
|
|
+ if (max_secondary.is_int())
|
|
|
+ secondary = min(secondary, max_secondary.as_int());
|
|
|
+
|
|
|
rect.set_secondary_size_for_orientation(orientation(), secondary);
|
|
|
|
|
|
if (orientation() == Gfx::Orientation::Horizontal)
|