Commit 0efb0b20 authored by Dana Fried's avatar Dana Fried Committed by Commit Bot

Expand past preferred size only after allocating space for all children.

This would have been expected behavior for a flex layout, but was not
previously implemented. Along with re-implementing
LocationBar::CalculatePreferredSize(), the associated bug should be
addressed.

Bug: 935218
Change-Id: Ib003672a89636e7d4e45bd5dc509eb810a6112e5
Reviewed-on: https://chromium-review.googlesource.com/c/1490524
Commit-Queue: Dana Fried <dfried@chromium.org>
Reviewed-by: default avatarPeter Boström <pbos@chromium.org>
Cr-Commit-Position: refs/heads/master@{#635933}
parent 20f3f637
...@@ -339,6 +339,11 @@ class FlexLayoutInternal { ...@@ -339,6 +339,11 @@ class FlexLayoutInternal {
void DoLayout(const Layout& layout, const gfx::Rect& bounds); void DoLayout(const Layout& layout, const gfx::Rect& bounds);
private: private:
// Maps a flex order (lower = allocated first, and therefore higher priority)
// to the indices of child views within that order that can flex.
// See FlexSpecification::order().
using FlexOrderToViewIndexMap = std::map<int, std::vector<int>>;
LayoutOrientation orientation() const { return layout_.orientation(); } LayoutOrientation orientation() const { return layout_.orientation(); }
// Determines whether a layout is still valid. // Determines whether a layout is still valid.
...@@ -348,6 +353,23 @@ class FlexLayoutInternal { ...@@ -348,6 +353,23 @@ class FlexLayoutInternal {
// Call DoLayout() to actually apply the layout. // Call DoLayout() to actually apply the layout.
const Layout& CalculateNewLayout(const NormalizedSizeBounds& bounds); const Layout& CalculateNewLayout(const NormalizedSizeBounds& bounds);
// Applies flex rules to each view in a layout, updating |layout| and
// |child_spacing|.
//
// If |expandable_views| is specified, any view requesting more than its
// preferred size will be clamped to its preferred size and be added to
// |expandable_views| for later processing after all other flex space has been
// allocated.
//
// Typically, this method will be called once with |expandable_views| set and
// then again with it null to allocate the remaining space.
void AllocateFlexSpace(
Layout* layout,
ChildViewSpacing* child_spacing,
const NormalizedSizeBounds& bounds,
const FlexOrderToViewIndexMap& order_to_index,
FlexOrderToViewIndexMap* expandable_views = nullptr) const;
// Calculates the position of each child view and the size of the overall // Calculates the position of each child view and the size of the overall
// layout based on tentative visibilities and sizes for each child. // layout based on tentative visibilities and sizes for each child.
void UpdateLayoutFromChildren(Layout* layout, void UpdateLayoutFromChildren(Layout* layout,
...@@ -586,13 +608,154 @@ void FlexLayoutInternal::UpdateLayoutFromChildren( ...@@ -586,13 +608,154 @@ void FlexLayoutInternal::UpdateLayoutFromChildren(
} }
} }
void FlexLayoutInternal::AllocateFlexSpace(
Layout* layout,
ChildViewSpacing* child_spacing,
const NormalizedSizeBounds& bounds,
const FlexOrderToViewIndexMap& order_to_index,
FlexOrderToViewIndexMap* expandable_views) const {
// Step through each flex priority allocating as much remaining space as
// possible to each flex view.
for (auto flex_it = order_to_index.begin(); flex_it != order_to_index.end();
++flex_it) {
// Check to see we haven't filled available space.
int remaining = *bounds.main() - layout->total_size.main();
if (remaining <= 0)
break;
const int flex_order = flex_it->first;
// The flex algorithm we're using works as follows:
// * For each child view at a particular flex order:
// - Calculate the percentage of the remaining flex space to allocate
// based on the ratio of its weight to the total unallocated weight
// at that order.
// - If the child view is already visible (it will be at its minimum
// size, which may or may not be zero), add the space the child is
// already taking up.
// - If the child view is not visible and adding it would introduce
// additional margin space between child views, subtract that
// additional space from the amount available.
// - Ask the child view's flex rule how large it would like to be
// within the space available.
// - If the child view would like to be larger, make it so, and
// subtract the additional space consumed by the child and its
// margins from the total remaining flex space.
//
// Note that this algorithm isn't *perfect* for specific cases, which are
// noted below; namely when margins very asymmetrical the sizing of child
// views can be slightly different from what would otherwise be expected.
// We have a TODO to look at ways of making this algorithm more "fair" in
// the future (but in the meantime most issues can be resolved by setting
// reasonable margins and by using flex order).
// Flex children at this priority order.
int flex_total = 0;
std::for_each(flex_it->second.begin(), flex_it->second.end(),
[&](int index) {
auto weight = layout->child_layouts[index].flex.weight();
if (weight > 0)
flex_total += weight;
});
// Note: because the child views are evaluated in order, if preferred
// minimum sizes are not consistent across a single priority expanding
// the parent control could result in children swapping visibility.
// We currently consider this user error; if the behavior is not
// desired, prioritize the child views' flex.
bool dirty = false;
for (auto index_it = flex_it->second.begin();
remaining >= 0 && index_it != flex_it->second.end(); ++index_it) {
const int view_index = *index_it;
ChildLayout& child_layout = layout->child_layouts[view_index];
// Offer a share of the remaining space to the view.
int flex_amount;
if (child_layout.flex.weight() > 0) {
const int flex_weight = child_layout.flex.weight();
// Round up so we give slightly greater weight to earlier views.
flex_amount =
int{std::ceil((float{remaining} * flex_weight) / flex_total)};
flex_total -= flex_weight;
} else {
flex_amount = remaining;
}
// If the layout was previously invisible, then making it visible
// may result in the addition of margin space, so we'll have to
// recalculate the margins on either side of this view. The change in
// margin space (if any) counts against the child view's flex space
// allocation.
//
// Note: In cases where the layout's internal margins and/or the child
// views' margins are wildly different sizes, subtracting the full delta
// out of the available space can cause the first view to be smaller
// than we would expect (see TODOs in unit tests for examples). We
// should look into ways to make this "feel" better (but in the
// meantime, please try to specify reasonable margins).
const int margin_delta = child_spacing->HasViewIndex(view_index)
? 0
: child_spacing->GetAddDelta(view_index);
// This is the space on the main axis that was already allocated to the
// child view; it will be added to the total flex space for the child
// view since it is considered a fixed overhead of the layout if it is
// nonzero.
const int old_size =
child_layout.visible ? child_layout.current_size.main() : 0;
// Offer the modified flex space to the child view and see how large it
// wants to be (or if it wants to be visible at that size at all).
const NormalizedSizeBounds available(
flex_amount + old_size - margin_delta,
child_layout.available_size.cross());
NormalizedSize new_size = Normalize(
orientation(),
child_layout.flex.rule().Run(child_layout.view,
Denormalize(orientation(), available)));
if (new_size.main() <= 0)
continue;
// Limit the expansion of views past their preferred size in the first
// pass so that enough space is available for lower-priority views. Save
// them to |expandable_views| so that the remaining space can be allocated
// later.
if (expandable_views &&
new_size.main() >= child_layout.preferred_size.main()) {
(*expandable_views)[flex_order].push_back(view_index);
new_size.set_main(child_layout.preferred_size.main());
}
// If the amount of space claimed increases (but is still within
// bounds set by our flex rule) we can make the control visible and
// claim the additional space.
const int to_deduct = (new_size.main() - old_size) + margin_delta;
DCHECK_GE(to_deduct, 0);
if (to_deduct > 0 && to_deduct <= remaining) {
child_layout.available_size = available;
child_layout.current_size = new_size;
child_layout.visible = true;
remaining -= to_deduct;
if (!child_spacing->HasViewIndex(view_index))
child_spacing->AddViewIndex(view_index);
dirty = true;
}
}
// Reposition the child controls (taking margins into account) and
// calculate remaining space.
if (dirty)
UpdateLayoutFromChildren(layout, child_spacing, bounds);
}
}
const Layout& FlexLayoutInternal::CalculateNewLayout( const Layout& FlexLayoutInternal::CalculateNewLayout(
const NormalizedSizeBounds& bounds) { const NormalizedSizeBounds& bounds) {
DCHECK(!bounds.cross() || DCHECK(!bounds.cross() ||
*bounds.cross() >= layout_.minimum_cross_axis_size()); *bounds.cross() >= layout_.minimum_cross_axis_size());
std::unique_ptr<Layout> layout = std::make_unique<Layout>(layout_counter_); std::unique_ptr<Layout> layout = std::make_unique<Layout>(layout_counter_);
layout->interior_margin = Normalize(orientation(), layout_.interior_margin()); layout->interior_margin = Normalize(orientation(), layout_.interior_margin());
std::map<int, std::vector<int>> order_to_view_index; FlexOrderToViewIndexMap order_to_view_index;
const bool main_axis_bounded = bounds.main().has_value(); const bool main_axis_bounded = bounds.main().has_value();
// Step through the children, creating placeholder layout view elements // Step through the children, creating placeholder layout view elements
...@@ -658,126 +821,14 @@ const Layout& FlexLayoutInternal::CalculateNewLayout( ...@@ -658,126 +821,14 @@ const Layout& FlexLayoutInternal::CalculateNewLayout(
UpdateLayoutFromChildren(layout.get(), &child_spacing, bounds); UpdateLayoutFromChildren(layout.get(), &child_spacing, bounds);
if (main_axis_bounded && !order_to_view_index.empty()) { if (main_axis_bounded && !order_to_view_index.empty()) {
// Step through each flex priority allocating as much remaining space as // Flex up to preferred size.
// possible to each flex view. FlexOrderToViewIndexMap expandable_views;
for (auto flex_it = order_to_view_index.begin(); AllocateFlexSpace(layout.get(), &child_spacing, bounds, order_to_view_index,
flex_it != order_to_view_index.end(); ++flex_it) { &expandable_views);
// Check to see we haven't filled available space.
int remaining = *bounds.main() - layout->total_size.main(); // Flex views that can exceed their preferred size.
if (remaining <= 0) { if (!expandable_views.empty())
break; AllocateFlexSpace(layout.get(), &child_spacing, bounds, expandable_views);
}
// The flex algorithm we're using works as follows:
// * For each child view at a particular flex order:
// - Calculate the percentage of the remaining flex space to allocate
// based on the ratio of its weight to the total unallocated weight
// at that order.
// - If the child view is already visible (it will be at its minimum
// size, which may or may not be zero), add the space the child is
// already taking up.
// - If the child view is not visible and adding it would introduce
// additional margin space between child views, subtract that
// additional space from the amount available.
// - Ask the child view's flex rule how large it would like to be
// within the space available.
// - If the child view would like to be larger, make it so, and
// subtract the additional space consumed by the child and its
// margins from the total remaining flex space.
//
// Note that this algorithm isn't *perfect* for specific cases, which are
// noted below; namely when margins very asymmetrical the sizing of child
// views can be slightly different from what would otherwise be expected.
// We have a TODO to look at ways of making this algorithm more "fair" in
// the future (but in the meantime most issues can be resolved by setting
// reasonable margins and by using flex order).
// Build a list of the elements to flex.
int flex_total = 0;
std::for_each(flex_it->second.begin(), flex_it->second.end(),
[&](int index) {
auto weight = layout->child_layouts[index].flex.weight();
if (weight > 0)
flex_total += weight;
});
// Note: because the child views are evaluated in order, if preferred
// minimum sizes are not consistent across a single priority expanding
// the parent control could result in children swapping visibility.
// We currently consider this user error; if the behavior is not
// desired, prioritize the child views' flex.
for (auto index_it = flex_it->second.begin();
remaining >= 0 && index_it != flex_it->second.end(); ++index_it) {
const int view_index = *index_it;
ChildLayout& child_layout = layout->child_layouts[view_index];
// Offer a share of the remaining space to the view.
int flex_amount;
if (child_layout.flex.weight() > 0) {
const int flex_weight = child_layout.flex.weight();
// Round up so we give slightly greater weight to earlier views.
flex_amount =
int{std::ceil((float{remaining} * flex_weight) / flex_total)};
flex_total -= flex_weight;
} else {
flex_amount = remaining;
}
// If the layout was previously invisible, then making it visible
// may result in the addition of margin space, so we'll have to
// recalculate the margins on either side of this view. The change in
// margin space (if any) counts against the child view's flex space
// allocation.
//
// Note: In cases where the layout's internal margins and/or the child
// views' margins are wildly different sizes, subtracting the full delta
// out of the available space can cause the first view to be smaller
// than we would expect (see TODOs in unit tests for examples). We
// should look into ways to make this "feel" better (but in the
// meantime, please try to specify reasonable margins).
const int margin_delta = child_spacing.HasViewIndex(view_index)
? 0
: child_spacing.GetAddDelta(view_index);
// This is the space on the main axis that was already allocated to the
// child view; it will be added to the total flex space for the child
// view since it is considered a fixed overhead of the layout if it is
// nonzero.
const int old_size =
child_layout.visible ? child_layout.current_size.main() : 0;
// Offer the modified flex space to the child view and see how large it
// wants to be (or if it wants to be visible at that size at all).
const NormalizedSizeBounds available(
flex_amount + old_size - margin_delta,
child_layout.available_size.cross());
const NormalizedSize new_size = Normalize(
orientation(),
child_layout.flex.rule().Run(
child_layout.view, Denormalize(orientation(), available)));
if (new_size.main() <= 0)
continue;
// If the amount of space claimed increases (but is still within
// bounds set by our flex rule) we can make the control visible and
// claim the additional space.
const int to_deduct = (new_size.main() - old_size) + margin_delta;
DCHECK_GE(to_deduct, 0);
if (to_deduct > 0 && to_deduct <= remaining) {
child_layout.available_size = available;
child_layout.current_size = new_size;
child_layout.visible = true;
remaining -= to_deduct;
if (!child_spacing.HasViewIndex(view_index))
child_spacing.AddViewIndex(view_index);
}
}
// Reposition the child controls (taking margins into account) and
// calculate remaining space.
UpdateLayoutFromChildren(layout.get(), &child_spacing, bounds);
}
} }
const Layout& result = *layout; const Layout& result = *layout;
......
...@@ -137,6 +137,8 @@ class FlexLayoutTest : public testing::Test { ...@@ -137,6 +137,8 @@ class FlexLayoutTest : public testing::Test {
static const FlexSpecification kUnboundedSnapToMinimum; static const FlexSpecification kUnboundedSnapToMinimum;
static const FlexSpecification kUnboundedScaleToMinimumSnapToZero; static const FlexSpecification kUnboundedScaleToMinimumSnapToZero;
static const FlexSpecification kUnboundedScaleToZero; static const FlexSpecification kUnboundedScaleToZero;
static const FlexSpecification kUnboundedScaleToMinimum;
static const FlexSpecification kUnboundedScaleToMinimumHighPriority;
// Custom flex which scales step-wise. // Custom flex which scales step-wise.
static const FlexSpecification kCustomFlex; static const FlexSpecification kCustomFlex;
...@@ -184,6 +186,11 @@ const FlexSpecification FlexLayoutTest::kUnboundedScaleToZero = ...@@ -184,6 +186,11 @@ const FlexSpecification FlexLayoutTest::kUnboundedScaleToZero =
FlexSpecification::ForSizeRule(MinimumFlexSizeRule::kScaleToZero, FlexSpecification::ForSizeRule(MinimumFlexSizeRule::kScaleToZero,
MaximumFlexSizeRule::kUnbounded) MaximumFlexSizeRule::kUnbounded)
.WithOrder(2); .WithOrder(2);
const FlexSpecification FlexLayoutTest::kUnboundedScaleToMinimumHighPriority =
FlexSpecification::ForSizeRule(MinimumFlexSizeRule::kScaleToMinimum,
MaximumFlexSizeRule::kUnbounded);
const FlexSpecification FlexLayoutTest::kUnboundedScaleToMinimum =
kUnboundedScaleToMinimumHighPriority.WithOrder(2);
const FlexSpecification FlexLayoutTest::kCustomFlex = const FlexSpecification FlexLayoutTest::kCustomFlex =
FlexSpecification::ForCustomRule( FlexSpecification::ForCustomRule(
...@@ -1439,6 +1446,146 @@ TEST_F(FlexLayoutTest, Layout_FlexRule_UnboundedScaleToZero) { ...@@ -1439,6 +1446,146 @@ TEST_F(FlexLayoutTest, Layout_FlexRule_UnboundedScaleToZero) {
EXPECT_FALSE(child->visible()); EXPECT_FALSE(child->visible());
} }
// A higher priority view which can expand past its maximum size should displace
// a lower priority view up to the first view's preferred size.
TEST_F(FlexLayoutTest,
Layout_FlexRule_TwoPassScaling_PreferredSizeTakesPrecedence) {
constexpr Size kLargeSize(10, 10);
constexpr Size kSmallSize(5, 5);
layout_->SetOrientation(LayoutOrientation::kHorizontal);
layout_->SetCollapseMargins(true);
layout_->SetMainAxisAlignment(LayoutAlignment::kStart);
layout_->SetCrossAxisAlignment(LayoutAlignment::kStart);
View* child1 = AddChild(kLargeSize, kSmallSize);
layout_->SetFlexForView(child1, kUnboundedScaleToMinimumHighPriority);
View* child2 = AddChild(kSmallSize);
layout_->SetFlexForView(child2, kDropOut);
// When there is no room for the second view, it drops out.
host_->SetSize(Size(4, 5));
host_->Layout();
EXPECT_EQ(kSmallSize, child1->size());
EXPECT_FALSE(child2->visible());
// When the first view has less room than its preferred size, it should still
// take up all of the space.
constexpr Size kIntermediateSize(8, 7);
host_->SetSize(kIntermediateSize);
host_->Layout();
EXPECT_EQ(kIntermediateSize, child1->size());
EXPECT_FALSE(child2->visible());
// When the first view has more room than its preferred size, but not enough
// to make room for the second view, the second view still drops out.
constexpr Size kLargerSize(13, 8);
host_->SetSize(kLargerSize);
host_->Layout();
EXPECT_EQ(kLargerSize, child1->size());
EXPECT_FALSE(child2->visible());
}
// When a view is allowed to flex above its preferred size, it will still yield
// that additional space to a lower-priority view, if there is space for the
// second view.
TEST_F(FlexLayoutTest, Layout_FlexRule_TwoPassScaling_StopAtPreferredSize) {
constexpr Size kLargeSize(10, 10);
constexpr Size kSmallSize(5, 5);
layout_->SetOrientation(LayoutOrientation::kHorizontal);
layout_->SetCollapseMargins(true);
layout_->SetMainAxisAlignment(LayoutAlignment::kStart);
layout_->SetCrossAxisAlignment(LayoutAlignment::kStart);
View* child1 = AddChild(kLargeSize, kSmallSize);
layout_->SetFlexForView(child1, kUnboundedScaleToMinimumHighPriority);
View* child2 = AddChild(kSmallSize);
layout_->SetFlexForView(child2, kDropOut);
constexpr Size kEnoughSpace(kSmallSize.width() + kLargeSize.width(),
kLargeSize.height());
host_->SetSize(kEnoughSpace);
host_->Layout();
EXPECT_EQ(kLargeSize, child1->size());
EXPECT_EQ(kSmallSize, child2->size());
}
// Once lower-priority views have reached their preferred sizes, a
// higher-priority view which can expand past its preferred size should start to
// consume the remaining space.
TEST_F(FlexLayoutTest, Layout_FlexRule_TwoPassScaling_GrowPastPreferredSize) {
constexpr Size kLargeSize(10, 10);
constexpr Size kSmallSize(5, 5);
layout_->SetOrientation(LayoutOrientation::kHorizontal);
layout_->SetCollapseMargins(true);
layout_->SetMainAxisAlignment(LayoutAlignment::kStart);
layout_->SetCrossAxisAlignment(LayoutAlignment::kStart);
View* child1 = AddChild(kLargeSize, kSmallSize);
layout_->SetFlexForView(child1, kUnboundedScaleToMinimumHighPriority);
View* child2 = AddChild(kSmallSize);
layout_->SetFlexForView(child2, kDropOut);
constexpr int kExtra = 7;
constexpr Size kExtraSpace(kSmallSize.width() + kLargeSize.width() + kExtra,
kLargeSize.height() + kExtra);
host_->SetSize(kExtraSpace);
EXPECT_EQ(Size(kLargeSize.width() + kExtra, kLargeSize.height()),
child1->size());
EXPECT_EQ(kSmallSize, child2->size());
}
// If two views can both scale past their preferred size with the same priority,
// once space has been allocated for each's preferred size, additional space
// will be divided according to flex weight.
TEST_F(FlexLayoutTest,
Layout_FlexRule_GrowPastPreferredSize_TwoViews_SamePriority) {
constexpr Size kLargeSize(10, 10);
constexpr Size kSmallSize(5, 5);
layout_->SetOrientation(LayoutOrientation::kHorizontal);
layout_->SetCollapseMargins(true);
layout_->SetMainAxisAlignment(LayoutAlignment::kStart);
layout_->SetCrossAxisAlignment(LayoutAlignment::kStart);
// Because we are using a flex rule that scales all the way to zero, ensure
// that the child view's minimum size is *not* respected.
View* child1 = AddChild(kLargeSize, kSmallSize);
layout_->SetFlexForView(child1, kUnboundedScaleToMinimumHighPriority);
View* child2 = AddChild(kLargeSize, kSmallSize);
layout_->SetFlexForView(child2, kUnboundedScaleToMinimumHighPriority);
constexpr int kExtra = 8;
constexpr Size kExtraSpace(2 * kLargeSize.width() + kExtra,
kLargeSize.height());
host_->SetSize(kExtraSpace);
EXPECT_EQ(Size(kLargeSize.width() + kExtra / 2, kLargeSize.height()),
child1->size());
EXPECT_EQ(Size(kLargeSize.width() + kExtra / 2, kLargeSize.height()),
child2->size());
}
// If two views can both scale past their preferred size once space has been
// allocated for each's preferred size, additional space will be given to the
// higher-precedence view.
TEST_F(FlexLayoutTest,
Layout_FlexRule_GrowPastPreferredSize_TwoViews_DifferentPriority) {
constexpr Size kLargeSize(10, 10);
constexpr Size kSmallSize(5, 5);
layout_->SetOrientation(LayoutOrientation::kHorizontal);
layout_->SetCollapseMargins(true);
layout_->SetMainAxisAlignment(LayoutAlignment::kStart);
layout_->SetCrossAxisAlignment(LayoutAlignment::kStart);
// Because we are using a flex rule that scales all the way to zero, ensure
// that the child view's minimum size is *not* respected.
View* child1 = AddChild(kLargeSize, kSmallSize);
layout_->SetFlexForView(child1, kUnboundedScaleToMinimumHighPriority);
View* child2 = AddChild(kLargeSize, kSmallSize);
layout_->SetFlexForView(child2, kUnboundedScaleToMinimum);
constexpr int kExtra = 8;
constexpr Size kExtraSpace(2 * kLargeSize.width() + kExtra,
kLargeSize.height());
host_->SetSize(kExtraSpace);
EXPECT_EQ(Size(kLargeSize.width() + kExtra, kLargeSize.height()),
child1->size());
EXPECT_EQ(kLargeSize, child2->size());
}
TEST_F(FlexLayoutTest, Layout_FlexRule_CustomFlexRule) { TEST_F(FlexLayoutTest, Layout_FlexRule_CustomFlexRule) {
constexpr int kFullSize = 50; constexpr int kFullSize = 50;
constexpr int kHalfSize = 25; constexpr int kHalfSize = 25;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment