| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/views/layout/flex_layout.h" |
| |
| #include <algorithm> |
| #include <functional> |
| #include <numeric> |
| #include <sstream> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/notreached.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/ranges/algorithm.h" |
| #include "ui/base/class_property.h" |
| #include "ui/events/event_target.h" |
| #include "ui/events/event_target_iterator.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/views/controls/tabbed_pane/tabbed_pane.h" |
| #include "ui/views/layout/flex_layout_types.h" |
| #include "ui/views/layout/normalized_geometry.h" |
| #include "ui/views/layout/proposed_layout.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_class_properties.h" |
| |
| // Module-private declarations ------------------------------------------------- |
| |
| namespace views { |
| |
| namespace { |
| |
| // Layout information for a specific child view in a proposed layout. |
| struct FlexChildData { |
| explicit FlexChildData(const FlexSpecification& flex) : flex(flex) {} |
| |
| // Copying this struct would be expensive and they only ever live in a vector |
| // in Layout (see below) so we'll only allow move semantics. |
| FlexChildData(const FlexChildData&) = delete; |
| FlexChildData& operator=(const FlexChildData&) = delete; |
| |
| FlexChildData(FlexChildData&& other) = default; |
| |
| std::string ToString() const { |
| std::ostringstream oss; |
| oss << "{ preferred " << preferred_size.ToString() << " current " |
| << current_size.ToString() << " margins " << margins.ToString() |
| << (using_default_margins ? " (using default)" : "") << " padding " |
| << internal_padding.ToString() << " bounds " << actual_bounds.ToString() |
| << " }"; |
| return oss.str(); |
| } |
| |
| NormalizedSize preferred_size; |
| NormalizedSize current_size; |
| NormalizedInsets margins; |
| bool using_default_margins = true; |
| NormalizedInsets internal_padding; |
| NormalizedRect actual_bounds; |
| FlexSpecification flex; |
| }; |
| |
| template <typename T> |
| T GetViewProperty(const View* view, |
| const ui::PropertyHandler& defaults, |
| const ui::ClassProperty<T*>* property, |
| bool* is_default = nullptr) { |
| T* found_value = view->GetProperty(property); |
| if (found_value) { |
| if (is_default) |
| *is_default = false; |
| return *found_value; |
| } |
| if (is_default) |
| *is_default = true; |
| found_value = defaults.GetProperty(property); |
| if (found_value) |
| return *found_value; |
| return T(); |
| } |
| |
| template <typename T> |
| T MaybeReverse(const T& list, FlexAllocationOrder order) { |
| return order == FlexAllocationOrder::kReverse ? T(list.rbegin(), list.rend()) |
| : list; |
| } |
| |
| } // anonymous namespace |
| |
| // Private implementation ------------------------------------------------------ |
| |
| // These definitions are required due to the C++ spec. |
| constexpr LayoutAlignment FlexLayout::kDefaultMainAxisAlignment; |
| constexpr LayoutAlignment FlexLayout::kDefaultCrossAxisAlignment; |
| |
| // Calculates and maintains 1D spacing between a sequence of child views. |
| class FlexLayout::ChildViewSpacing { |
| public: |
| // Given the indices of two child views, returns the amount of space that |
| // should be placed between them if they were adjacent. If the first index is |
| // absent, uses the left edge of the parent container. If the second index is |
| // absent, uses the right edge of the parent container. |
| using GetViewSpacingCallback = |
| base::RepeatingCallback<int(absl::optional<size_t>, |
| absl::optional<size_t>)>; |
| |
| explicit ChildViewSpacing(GetViewSpacingCallback get_view_spacing); |
| ChildViewSpacing(const ChildViewSpacing& other) = default; |
| ChildViewSpacing& operator=(const ChildViewSpacing& other) = default; |
| |
| bool HasViewIndex(size_t view_index) const; |
| int GetLeadingInset() const; |
| int GetTrailingInset() const; |
| int GetLeadingSpace(size_t view_index) const; |
| int GetTotalSpace() const; |
| |
| // Returns the maximum size for the child at |view_index|, given its |
| // |current_size| and the amount of |available_space| for flex allocation. |
| SizeBound GetMaxSize(size_t view_index, |
| int current_size, |
| const SizeBound& available_space) const; |
| |
| // Returns the change in total allocated size if the child at |view_index| is |
| // resized from |current_size| to |new_size|. |
| int GetTotalSizeChangeForNewSize(size_t view_index, |
| int current_size, |
| int new_size) const; |
| |
| // Add the view at the specified index. |
| // |
| // If |new_leading| or |new_trailing| is specified, it will be set to the new |
| // leading/trailing space for the view at the index that was added. |
| void AddViewIndex(size_t view_index, |
| int* new_leading = nullptr, |
| int* new_trailing = nullptr); |
| |
| private: |
| absl::optional<size_t> GetPreviousViewIndex(size_t view_index) const; |
| absl::optional<size_t> GetNextViewIndex(size_t view_index) const; |
| |
| // Returns the change in space required if the specified view index were |
| // added. The view must not already be present. |
| int GetAddDelta(size_t view_index) const; |
| |
| GetViewSpacingCallback get_view_spacing_; |
| // Maps from view index to the leading spacing for that index. |
| std::map<size_t, int> leading_spacings_; |
| // The trailing space (space preceding the trailing margin). |
| int trailing_space_; |
| }; |
| |
| FlexLayout::ChildViewSpacing::ChildViewSpacing( |
| GetViewSpacingCallback get_view_spacing) |
| : get_view_spacing_(std::move(get_view_spacing)), |
| trailing_space_(get_view_spacing_.Run(absl::nullopt, absl::nullopt)) {} |
| |
| bool FlexLayout::ChildViewSpacing::HasViewIndex(size_t view_index) const { |
| return leading_spacings_.find(view_index) != leading_spacings_.end(); |
| } |
| |
| int FlexLayout::ChildViewSpacing::GetLeadingInset() const { |
| if (leading_spacings_.empty()) |
| return 0; |
| return leading_spacings_.begin()->second; |
| } |
| |
| int FlexLayout::ChildViewSpacing::GetTrailingInset() const { |
| return trailing_space_; |
| } |
| |
| int FlexLayout::ChildViewSpacing::GetLeadingSpace(size_t view_index) const { |
| auto it = leading_spacings_.find(view_index); |
| DCHECK(it != leading_spacings_.end()); |
| return it->second; |
| } |
| |
| int FlexLayout::ChildViewSpacing::GetTotalSpace() const { |
| return std::accumulate( |
| leading_spacings_.cbegin(), leading_spacings_.cend(), trailing_space_, |
| [](int total, const auto& value) { return total + value.second; }); |
| } |
| |
| SizeBound FlexLayout::ChildViewSpacing::GetMaxSize( |
| size_t view_index, |
| int current_size, |
| const SizeBound& available_space) const { |
| DCHECK_GE(available_space, 0); |
| |
| if (HasViewIndex(view_index)) |
| return current_size + available_space; |
| |
| DCHECK_EQ(0, current_size); |
| // Making the child visible may result in the addition of margin space, which |
| // 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, specify reasonable margins). |
| return std::max<SizeBound>(available_space - GetAddDelta(view_index), 0); |
| } |
| |
| int FlexLayout::ChildViewSpacing::GetTotalSizeChangeForNewSize( |
| size_t view_index, |
| int current_size, |
| int new_size) const { |
| return HasViewIndex(view_index) ? new_size - current_size |
| : new_size + GetAddDelta(view_index); |
| } |
| |
| void FlexLayout::ChildViewSpacing::AddViewIndex(size_t view_index, |
| int* new_leading, |
| int* new_trailing) { |
| DCHECK(!HasViewIndex(view_index)); |
| absl::optional<size_t> prev = GetPreviousViewIndex(view_index); |
| absl::optional<size_t> next = GetNextViewIndex(view_index); |
| |
| const int leading_space = get_view_spacing_.Run(prev, view_index); |
| const int trailing_space = get_view_spacing_.Run(view_index, next); |
| leading_spacings_[view_index] = leading_space; |
| if (next) |
| leading_spacings_[*next] = trailing_space; |
| else |
| trailing_space_ = trailing_space; |
| |
| if (new_leading) |
| *new_leading = leading_space; |
| if (new_trailing) |
| *new_trailing = trailing_space; |
| } |
| |
| absl::optional<size_t> FlexLayout::ChildViewSpacing::GetPreviousViewIndex( |
| size_t view_index) const { |
| const auto it = leading_spacings_.lower_bound(view_index); |
| if (it == leading_spacings_.begin()) |
| return absl::nullopt; |
| return std::prev(it)->first; |
| } |
| |
| absl::optional<size_t> FlexLayout::ChildViewSpacing::GetNextViewIndex( |
| size_t view_index) const { |
| const auto it = leading_spacings_.upper_bound(view_index); |
| if (it == leading_spacings_.end()) |
| return absl::nullopt; |
| return it->first; |
| } |
| |
| int FlexLayout::ChildViewSpacing::GetAddDelta(size_t view_index) const { |
| DCHECK(!HasViewIndex(view_index)); |
| absl::optional<size_t> prev = GetPreviousViewIndex(view_index); |
| absl::optional<size_t> next = GetNextViewIndex(view_index); |
| const int old_spacing = next ? GetLeadingSpace(*next) : GetTrailingInset(); |
| const int new_spacing = get_view_spacing_.Run(prev, view_index) + |
| get_view_spacing_.Run(view_index, next); |
| return new_spacing - old_spacing; |
| } |
| |
| // Represents a specific stored layout given a set of size bounds. |
| struct FlexLayout::FlexLayoutData { |
| FlexLayoutData() = default; |
| |
| FlexLayoutData(const FlexLayoutData&) = delete; |
| FlexLayoutData& operator=(const FlexLayoutData&) = delete; |
| |
| ~FlexLayoutData() = default; |
| |
| size_t num_children() const { return child_data.size(); } |
| |
| std::string ToString() const { |
| std::ostringstream oss; |
| oss << "{ " << total_size.ToString() << " " << layout.ToString() << " {"; |
| bool first = true; |
| for (const FlexChildData& flex_child : child_data) { |
| if (first) |
| first = false; |
| else |
| oss << ", "; |
| oss << flex_child.ToString(); |
| } |
| oss << "} margin " << interior_margin.ToString() << " insets " |
| << host_insets.ToString() << "}"; |
| return oss.str(); |
| } |
| |
| ProposedLayout layout; |
| |
| // Holds additional information about the child views of this layout. |
| std::vector<FlexChildData> child_data; |
| |
| // The total size of the layout (minus parent insets). |
| NormalizedSize total_size; |
| NormalizedInsets interior_margin; |
| NormalizedInsets host_insets; |
| }; |
| |
| FlexLayout::PropertyHandler::PropertyHandler(FlexLayout* layout) |
| : layout_(layout) {} |
| |
| void FlexLayout::PropertyHandler::AfterPropertyChange(const void* key, |
| int64_t old_value) { |
| layout_->InvalidateHost(true); |
| } |
| |
| // FlexLayout |
| // ------------------------------------------------------------------- |
| |
| FlexLayout::FlexLayout() { |
| // Ensure this property is always set and is never null. |
| SetDefault(kCrossAxisAlignmentKey, kDefaultCrossAxisAlignment); |
| } |
| |
| FlexLayout::~FlexLayout() = default; |
| |
| FlexLayout& FlexLayout::SetOrientation(LayoutOrientation orientation) { |
| if (orientation != orientation_) { |
| orientation_ = orientation; |
| InvalidateHost(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetIncludeHostInsetsInLayout( |
| bool include_host_insets_in_layout) { |
| if (include_host_insets_in_layout != include_host_insets_in_layout_) { |
| include_host_insets_in_layout_ = include_host_insets_in_layout; |
| InvalidateHost(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetCollapseMargins(bool collapse_margins) { |
| if (collapse_margins != collapse_margins_) { |
| collapse_margins_ = collapse_margins; |
| InvalidateHost(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetMainAxisAlignment( |
| LayoutAlignment main_axis_alignment) { |
| DCHECK_NE(main_axis_alignment, LayoutAlignment::kStretch) |
| << "Main axis stretch/justify is not yet supported."; |
| if (main_axis_alignment_ != main_axis_alignment) { |
| main_axis_alignment_ = main_axis_alignment; |
| InvalidateHost(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetCrossAxisAlignment( |
| LayoutAlignment cross_axis_alignment) { |
| return SetDefault(kCrossAxisAlignmentKey, cross_axis_alignment); |
| } |
| |
| FlexLayout& FlexLayout::SetInteriorMargin(const gfx::Insets& interior_margin) { |
| if (interior_margin_ != interior_margin) { |
| interior_margin_ = interior_margin; |
| InvalidateHost(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetIgnoreDefaultMainAxisMargins( |
| bool ignore_default_main_axis_margins) { |
| if (ignore_default_main_axis_margins_ != ignore_default_main_axis_margins) { |
| ignore_default_main_axis_margins_ = ignore_default_main_axis_margins; |
| InvalidateHost(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetMinimumCrossAxisSize(int size) { |
| if (minimum_cross_axis_size_ != size) { |
| minimum_cross_axis_size_ = size; |
| InvalidateHost(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetFlexAllocationOrder( |
| FlexAllocationOrder flex_allocation_order) { |
| if (flex_allocation_order_ != flex_allocation_order) { |
| flex_allocation_order_ = flex_allocation_order; |
| InvalidateHost(true); |
| } |
| return *this; |
| } |
| |
| FlexRule FlexLayout::GetDefaultFlexRule() const { |
| return base::BindRepeating(&FlexLayout::DefaultFlexRuleImpl, |
| base::Unretained(this)); |
| } |
| |
| ProposedLayout FlexLayout::CalculateProposedLayout( |
| const SizeBounds& size_bounds) const { |
| FlexLayoutData data; |
| |
| if (include_host_insets_in_layout()) { |
| // Combining the interior margin and host insets means we only have to set |
| // the margin value; we'll leave the insets at zero. |
| data.interior_margin = |
| Normalize(orientation(), interior_margin() + host_view()->GetInsets()); |
| } else { |
| data.host_insets = Normalize(orientation(), host_view()->GetInsets()); |
| data.interior_margin = Normalize(orientation(), interior_margin()); |
| } |
| NormalizedSizeBounds bounds = Normalize(orientation(), size_bounds); |
| bounds.Inset(data.host_insets); |
| bounds.set_cross( |
| std::max<SizeBound>(bounds.cross(), minimum_cross_axis_size())); |
| |
| // Populate the child layout data vectors and the order-to-index map. |
| FlexOrderToViewIndexMap order_to_view_index; |
| InitializeChildData(bounds, data, order_to_view_index); |
| |
| // Do the initial layout update, calculating spacing between children. |
| ChildViewSpacing child_spacing( |
| base::BindRepeating(&FlexLayout::CalculateChildSpacing, |
| base::Unretained(this), std::cref(data))); |
| UpdateLayoutFromChildren(bounds, data, child_spacing); |
| |
| // We now have a layout with all views at the absolute minimum size and with |
| // those able to drop out dropped out. Now apply flex rules. |
| // |
| // This is done in two primary phases: |
| // 1. If there is insufficient space to provide each view with its preferred |
| // size, the deficit will be spread across the views that can flex, with |
| // any views that bottom out getting their minimum and dropping out of the |
| // calculation. |
| // 2. If there is excess space after the first phase, it is spread across all |
| // of the remaining flex views that haven't dropped out. |
| // |
| // The result of this calculation is extremely *correct* but it is possible |
| // there are some pathological cases where the cost of one of the steps is |
| // quadratic in the number of views. Again, this is unlikely and numbers of |
| // child views tend to be small enough that it won't matter. |
| |
| CalculateNonFlexAvailableSpace( |
| std::max<SizeBound>(0, bounds.main() - data.total_size.main()), |
| order_to_view_index, child_spacing, data); |
| |
| // Flex up to preferred size. This will be a no-op if |order_to_view_index| |
| // is empty. |
| FlexOrderToViewIndexMap expandable_views; |
| AllocateFlexShortage(bounds, order_to_view_index, data, child_spacing, |
| expandable_views); |
| |
| // Flex views that can exceed their preferred size. This will be a no-op if |
| // |expandable_views| is empty. |
| AllocateFlexExcess(bounds, expandable_views, data, child_spacing); |
| |
| // Calculate the size of the host view. |
| NormalizedSize host_size = data.total_size; |
| host_size.Enlarge(data.host_insets.main_size(), |
| data.host_insets.cross_size()); |
| data.layout.host_size = Denormalize(orientation(), host_size); |
| |
| // Size and position the children in screen space. |
| CalculateChildBounds(size_bounds, data); |
| |
| return data.layout; |
| } |
| |
| NormalizedSize FlexLayout::GetPreferredSizeForRule( |
| const FlexRule& rule, |
| const View* child, |
| const SizeBound& available_cross) const { |
| const NormalizedSize default_size = |
| Normalize(orientation(), rule.Run(child, SizeBounds())); |
| if (!available_cross.is_bounded()) |
| return default_size; |
| |
| // Do the height-for-width calculation. |
| const NormalizedSize stretch_size = Normalize( |
| orientation(), |
| rule.Run(child, |
| Denormalize(orientation(), NormalizedSizeBounds( |
| SizeBound(), available_cross)))); |
| |
| NormalizedSize size = default_size; |
| |
| // For vertical layouts, allow changing the cross-axis to cause the main axis |
| // to grow - or in the case of "stretch" alignment where we can potentially |
| // force the cross-axis to be larger than the preferred size, allow the main |
| // axis to shrink. This best handles labels and other text controls in |
| // vertical layouts. (We don't do this in horizontal layouts for aesthetic |
| // reasons.) |
| if (orientation() == LayoutOrientation::kVertical) { |
| const LayoutAlignment cross_align = |
| GetViewProperty(child, layout_defaults_, kCrossAxisAlignmentKey); |
| if (cross_align == LayoutAlignment::kStretch) |
| return stretch_size; |
| size.set_main(std::max(size.main(), stretch_size.main())); |
| } |
| |
| // Always allow the cross axis to adjust to the available space if it's less |
| // than the preferred size in order to prevent unnecessary overhang. |
| size.set_cross(std::min(size.cross(), stretch_size.cross())); |
| return size; |
| } |
| |
| NormalizedSize FlexLayout::GetCurrentSizeForRule( |
| const FlexRule& rule, |
| const View* child, |
| const NormalizedSizeBounds& available) const { |
| return Normalize(orientation(), |
| rule.Run(child, Denormalize(orientation(), available))); |
| } |
| |
| void FlexLayout::InitializeChildData( |
| const NormalizedSizeBounds& bounds, |
| FlexLayoutData& data, |
| FlexOrderToViewIndexMap& flex_order_to_index) const { |
| // Step through the children, creating placeholder layout view elements |
| // and setting up initial minimal visibility. |
| const bool main_axis_bounded = bounds.main().is_bounded(); |
| for (View* child : host_view()->children()) { |
| if (!IsChildIncludedInLayout(child)) |
| continue; |
| |
| const size_t view_index = data.num_children(); |
| data.layout.child_layouts.emplace_back(ChildLayout{child}); |
| ChildLayout& child_layout = data.layout.child_layouts.back(); |
| data.child_data.emplace_back( |
| GetViewProperty(child, layout_defaults_, views::kFlexBehaviorKey)); |
| FlexChildData& flex_child = data.child_data.back(); |
| |
| flex_child.margins = |
| Normalize(orientation(), |
| GetViewProperty(child, layout_defaults_, views::kMarginsKey, |
| &flex_child.using_default_margins)); |
| flex_child.internal_padding = Normalize( |
| orientation(), |
| GetViewProperty(child, layout_defaults_, views::kInternalPaddingKey)); |
| |
| const SizeBound available_cross = |
| GetAvailableCrossAxisSize(data, view_index, bounds); |
| SetCrossAxis(&child_layout.available_size, orientation(), available_cross); |
| |
| flex_child.preferred_size = |
| GetPreferredSizeForRule(flex_child.flex.rule(), child, available_cross); |
| |
| // gfx::Size calculation depends on whether flex is allowed. |
| if (main_axis_bounded) { |
| flex_child.current_size = |
| GetCurrentSizeForRule(flex_child.flex.rule(), child, |
| NormalizedSizeBounds(0, available_cross)); |
| |
| DCHECK_GE(flex_child.preferred_size.main(), |
| flex_child.current_size.main()) |
| << " in " << child->GetClassName(); |
| } else { |
| // All non-flex or unbounded controls get preferred size. |
| flex_child.current_size = flex_child.preferred_size; |
| } |
| |
| // Keep track of non-hidden/ignored child views that can flex. We assume any |
| // view with a non-zero weight can flex, as can views with zero weight that |
| // have a minimum size smaller than their preferred size. |
| const int weight = flex_child.flex.weight(); |
| bool can_flex = weight > 0 || flex_child.current_size.main() < |
| flex_child.preferred_size.main(); |
| |
| // Do a spot check to see if a zero-weight view could expand in the space |
| // provided. Note that we can get some false positives here but they will |
| // invariably shake out in subsequent steps. |
| if (!can_flex && weight == 0) { |
| const NormalizedSize estimate = GetCurrentSizeForRule( |
| flex_child.flex.rule(), child, |
| NormalizedSizeBounds(bounds.main(), available_cross)); |
| can_flex = estimate.main() > flex_child.preferred_size.main(); |
| } |
| |
| // Add views that have the potential to flex to the appropriate order list. |
| if (can_flex) |
| flex_order_to_index[flex_child.flex.order()].push_back(view_index); |
| |
| child_layout.visible = flex_child.current_size.main() > 0; |
| } |
| } |
| |
| void FlexLayout::CalculateChildBounds(const SizeBounds& size_bounds, |
| FlexLayoutData& data) const { |
| // Apply main axis alignment (we've already done cross-axis alignment above). |
| const NormalizedSizeBounds normalized_bounds = |
| Normalize(orientation(), size_bounds); |
| const NormalizedSize normalized_host_size = |
| Normalize(orientation(), data.layout.host_size); |
| int available_main = normalized_bounds.main().is_bounded() |
| ? normalized_bounds.main().value() |
| : normalized_host_size.main(); |
| available_main = std::max(0, available_main - data.host_insets.main_size()); |
| const int excess_main = available_main - data.total_size.main(); |
| NormalizedPoint start(data.host_insets.main_leading(), |
| data.host_insets.cross_leading()); |
| switch (main_axis_alignment()) { |
| case LayoutAlignment::kStart: |
| break; |
| case LayoutAlignment::kCenter: |
| start.set_main(start.main() + excess_main / 2); |
| break; |
| case LayoutAlignment::kEnd: |
| start.set_main(start.main() + excess_main); |
| break; |
| case LayoutAlignment::kStretch: |
| case LayoutAlignment::kBaseline: |
| NOTIMPLEMENTED(); |
| break; |
| } |
| |
| // Calculate the actual child bounds. |
| for (size_t i = 0; i < data.num_children(); ++i) { |
| ChildLayout& child_layout = data.layout.child_layouts[i]; |
| if (child_layout.visible) { |
| FlexChildData& flex_child = data.child_data[i]; |
| NormalizedRect actual = flex_child.actual_bounds; |
| actual.Offset(start.main(), start.cross()); |
| if (actual.size_main() > flex_child.preferred_size.main() && |
| flex_child.flex.alignment() != LayoutAlignment::kStretch) { |
| Span container(actual.origin_main(), actual.size_main()); |
| Span new_main(0, flex_child.preferred_size.main()); |
| new_main.Align(container, flex_child.flex.alignment()); |
| actual.set_origin_main(new_main.start()); |
| actual.set_size_main(new_main.length()); |
| } |
| child_layout.bounds = Denormalize(orientation(), actual); |
| } |
| } |
| } |
| |
| void FlexLayout::CalculateNonFlexAvailableSpace( |
| const SizeBound& available_space, |
| const FlexOrderToViewIndexMap& flex_views, |
| const ChildViewSpacing& child_spacing, |
| FlexLayoutData& data) const { |
| // Add all views which are participating in flex (and will have their |
| // available space set later) to a lookup so we can skip them now. |
| std::set<size_t> all_flex_indices; |
| for (const auto& order_to_indices : flex_views) { |
| all_flex_indices.insert(order_to_indices.second.begin(), |
| order_to_indices.second.end()); |
| } |
| |
| // Work through the remaining views and set their available space. Since |
| // non-flex views get their space first, these views will have access to the |
| // entire budget of remaining space in the layout. |
| for (size_t index = 0; index < data.child_data.size(); ++index) { |
| if (base::Contains(all_flex_indices, index)) |
| continue; |
| |
| // Cross-axis available size is already set in InitializeChildData(), so |
| // just set the main axis here. |
| const SizeBound max_size = child_spacing.GetMaxSize( |
| index, data.child_data[index].current_size.main(), available_space); |
| SetMainAxis(&data.layout.child_layouts[index].available_size, orientation(), |
| max_size); |
| } |
| } |
| |
| Inset1D FlexLayout::GetCrossAxisMargins(const FlexLayoutData& layout, |
| size_t child_index) const { |
| const FlexChildData& child_data = layout.child_data[child_index]; |
| const int leading_margin = |
| CalculateMargin(layout.interior_margin.cross_leading(), |
| child_data.margins.cross_leading(), |
| child_data.internal_padding.cross_leading()); |
| const int trailing_margin = |
| CalculateMargin(layout.interior_margin.cross_trailing(), |
| child_data.margins.cross_trailing(), |
| child_data.internal_padding.cross_trailing()); |
| return Inset1D(leading_margin, trailing_margin); |
| } |
| |
| int FlexLayout::CalculateMargin(int margin1, |
| int margin2, |
| int internal_padding) const { |
| const int result = |
| collapse_margins() ? std::max(margin1, margin2) : margin1 + margin2; |
| return std::max(0, result - internal_padding); |
| } |
| |
| SizeBound FlexLayout::GetAvailableCrossAxisSize( |
| const FlexLayoutData& layout, |
| size_t child_index, |
| const NormalizedSizeBounds& bounds) const { |
| const Inset1D cross_margins = GetCrossAxisMargins(layout, child_index); |
| return std::max<SizeBound>(0, bounds.cross() - cross_margins.size()); |
| } |
| |
| int FlexLayout::CalculateChildSpacing( |
| const FlexLayoutData& layout, |
| absl::optional<size_t> child1_index, |
| absl::optional<size_t> child2_index) const { |
| const FlexChildData* const child1 = |
| child1_index ? &layout.child_data[*child1_index] : nullptr; |
| const FlexChildData* const child2 = |
| child2_index ? &layout.child_data[*child2_index] : nullptr; |
| |
| const int child1_trailing = |
| child1 && (child2 || !ignore_default_main_axis_margins() || |
| !child1->using_default_margins) |
| ? child1->margins.main_trailing() |
| : 0; |
| const int child2_leading = |
| child2 && (child1 || !ignore_default_main_axis_margins() || |
| !child2->using_default_margins) |
| ? child2->margins.main_leading() |
| : 0; |
| |
| const int left_margin = |
| child1 ? child1_trailing : layout.interior_margin.main_leading(); |
| const int right_margin = |
| child2 ? child2_leading : layout.interior_margin.main_trailing(); |
| |
| const int left_padding = |
| child1 ? child1->internal_padding.main_trailing() : 0; |
| const int right_padding = |
| child2 ? child2->internal_padding.main_leading() : 0; |
| |
| return CalculateMargin(left_margin, right_margin, |
| left_padding + right_padding); |
| } |
| |
| void FlexLayout::UpdateLayoutFromChildren( |
| const NormalizedSizeBounds& bounds, |
| FlexLayoutData& data, |
| ChildViewSpacing& child_spacing) const { |
| // Calculate starting minimum for cross-axis size. |
| int min_cross_size = |
| std::max(minimum_cross_axis_size(), |
| CalculateMargin(data.interior_margin.cross_leading(), |
| data.interior_margin.cross_trailing(), 0)); |
| data.total_size = NormalizedSize(0, min_cross_size); |
| |
| // For cases with a non-zero cross-axis bound, the objective is to fit the |
| // layout into that precise size, not to determine what size we need. |
| bool force_cross_size = false; |
| if (bounds.cross().is_bounded() && bounds.cross() > 0) { |
| data.total_size.SetToMax(0, bounds.cross().value()); |
| force_cross_size = true; |
| } |
| |
| std::vector<Inset1D> cross_spacings(data.num_children()); |
| for (size_t i = 0; i < data.num_children(); ++i) { |
| FlexChildData& flex_child = data.child_data[i]; |
| |
| const bool is_visible = data.layout.child_layouts[i].visible; |
| |
| // Update the cross-axis margins and if necessary, the size. |
| cross_spacings[i] = GetCrossAxisMargins(data, i); |
| if (!force_cross_size && |
| (is_visible || flex_child.preferred_size.main() == 0)) { |
| data.total_size.SetToMax( |
| 0, cross_spacings[i].size() + flex_child.current_size.cross()); |
| } |
| |
| // We don't have to deal with invisible children any further than this. |
| if (!is_visible) |
| continue; |
| |
| // Calculate main-axis size and upper-left main axis coordinate. |
| int leading_space; |
| if (child_spacing.HasViewIndex(i)) |
| leading_space = child_spacing.GetLeadingSpace(i); |
| else |
| child_spacing.AddViewIndex(i, &leading_space); |
| data.total_size.Enlarge(leading_space, 0); |
| |
| const int size_main = flex_child.current_size.main(); |
| flex_child.actual_bounds.set_origin_main(data.total_size.main()); |
| flex_child.actual_bounds.set_size_main(size_main); |
| data.total_size.Enlarge(size_main, 0); |
| } |
| |
| // Add the end margin. |
| data.total_size.Enlarge(child_spacing.GetTrailingInset(), 0); |
| |
| // Calculate cross-axis positioning based on the cross margins and size that |
| // were calculated above. |
| const Span cross_span(0, data.total_size.cross()); |
| for (size_t i = 0; i < data.num_children(); ++i) { |
| FlexChildData& flex_child = data.child_data[i]; |
| flex_child.actual_bounds.set_size_cross(flex_child.current_size.cross()); |
| const LayoutAlignment cross_align = |
| GetViewProperty(data.layout.child_layouts[i].child_view, |
| layout_defaults_, kCrossAxisAlignmentKey); |
| flex_child.actual_bounds.AlignCross(cross_span, cross_align, |
| cross_spacings[i]); |
| } |
| } |
| |
| void FlexLayout::AllocateFlexShortage( |
| const NormalizedSizeBounds& bounds, |
| const FlexOrderToViewIndexMap& order_to_index, |
| FlexLayoutData& data, |
| ChildViewSpacing& child_spacing, |
| FlexOrderToViewIndexMap& expandable_views) const { |
| // Step through each flex priority allocating shortage across child views that |
| // can flex. |
| for (const auto& flex_elem : order_to_index) { |
| const int order = flex_elem.first; |
| |
| // Record available space for each view at this flex order. |
| CalculateFlexAvailableSpace(bounds, flex_elem.second, child_spacing, data); |
| |
| // Get the list of views to process at this flex priority, in the desired |
| // order. Zero-preferred-size views are sorted directly onto the list of |
| // expandable views, because they're already at their preferred size. |
| ChildIndices view_indices; |
| for (size_t child_index : |
| MaybeReverse(flex_elem.second, flex_allocation_order())) { |
| const int size = data.child_data[child_index].preferred_size.main(); |
| auto& indices = (size == 0) ? expandable_views[order] : view_indices; |
| indices.push_back(child_index); |
| } |
| |
| // Allocate zero-weight child views at this order first. This removes them |
| // from |view_indices|. |
| AllocateZeroWeightFlex(bounds, order, view_indices, data, child_spacing, |
| &expandable_views); |
| |
| // Iterate until all views can be allocated or are dropped out. |
| for (SizeBound deficit; |
| !view_indices.empty() && |
| (deficit = TryAllocateAll(bounds, order, view_indices, data, |
| child_spacing, expandable_views)) > 0;) { |
| // Process flex views with weight, allocating any shortage of flex space |
| // below the views' minimum size based on weight, and dropping out any |
| // views that fall to zero size. |
| AllocateFlexShortageAtOrder(bounds, deficit, view_indices, data, |
| child_spacing); |
| } |
| |
| UpdateLayoutFromChildren(bounds, data, child_spacing); |
| } |
| } |
| |
| void FlexLayout::AllocateFlexExcess( |
| const NormalizedSizeBounds& bounds, |
| const FlexOrderToViewIndexMap& order_to_index, |
| FlexLayoutData& data, |
| ChildViewSpacing& child_spacing) const { |
| // Step through each flex priority allocating as much remaining space as |
| // possible to each remaining flex view. |
| for (const auto& flex_elem : order_to_index) { |
| const int order = flex_elem.first; |
| |
| // No need to reverse here because if we are reversed, then these values |
| // were added in reverse order. |
| ChildIndices view_indices = flex_elem.second; |
| |
| AllocateZeroWeightFlex(bounds, order, view_indices, data, child_spacing, |
| nullptr); |
| |
| // Allocate space to available children until all possible space is used up. |
| for (SizeBound remaining = |
| std::max<SizeBound>(0, bounds.main() - data.total_size.main()); |
| !view_indices.empty();) { |
| AllocateFlexExcessAtOrder(bounds, remaining, view_indices, data, |
| child_spacing); |
| } |
| |
| UpdateLayoutFromChildren(bounds, data, child_spacing); |
| } |
| } |
| |
| void FlexLayout::AllocateFlexShortageAtOrder( |
| const NormalizedSizeBounds& bounds, |
| SizeBound deficit, |
| ChildIndices& child_list, |
| FlexLayoutData& data, |
| ChildViewSpacing& child_spacing) const { |
| int flex_total = CalculateFlexTotal(data, child_list); |
| |
| // We'll process the views in reverse order so that views later in the order |
| // are more likely to drop out/be shorted, which is consistent with the zero |
| // weight behavior. That is, if the FlexAllocationOrder associated with this |
| // layout is kNormal, views will drop from the end; while if it's kReverse, |
| // views will drop from the beginning. |
| std::map<size_t, NormalizedSize> pending_updates; |
| for (auto it = child_list.rbegin(); it != child_list.rend(); ++it) { |
| const size_t view_index = *it; |
| FlexChildData& flex_child = data.child_data[view_index]; |
| ChildLayout& child_layout = data.layout.child_layouts[view_index]; |
| |
| const int weight = flex_child.flex.weight(); |
| DCHECK_GT(weight, 0); |
| DCHECK(deficit.is_bounded()); |
| const SizeBound to_deduct = base::ClampRound( |
| deficit.value() * weight / static_cast<float>(flex_total)); |
| const SizeBound new_main = flex_child.preferred_size.main() - to_deduct; |
| |
| // If a view would shrink smaller than its current size, go with that and |
| // eliminate it from the flex calculation. |
| if (new_main <= flex_child.current_size.main()) { |
| // Note that the iterator math ensures that the resulting forward iterator |
| // actually points to the element being removed. |
| child_list.erase(--it.base()); |
| return; |
| } |
| |
| // See how much space the child view wants within the reduced space |
| // remaining for it. |
| const NormalizedSizeBounds available( |
| new_main, GetCrossAxis(orientation(), child_layout.available_size)); |
| const NormalizedSize new_size = GetCurrentSizeForRule( |
| flex_child.flex.rule(), child_layout.child_view, available); |
| |
| if (new_size.main() < new_main) { |
| // Views that cap out below the allotted space can get their size set |
| // immediately and they will drop out of subsequent passes. |
| if (!new_size.is_empty() && |
| new_size.main() >= flex_child.current_size.main()) { |
| flex_child.current_size = new_size; |
| child_layout.visible = true; |
| if (!child_spacing.HasViewIndex(view_index)) |
| child_spacing.AddViewIndex(view_index); |
| } |
| |
| // Since the view has already been allocated, remove it from the |
| // candidates list. The iterator math ensures that the resulting forward |
| // iterator corresponds to the element being removed from the list. |
| child_list.erase(--it.base()); |
| return; |
| } |
| |
| // Changes to views that can take up the entire allotted space are held in |
| // case we need to do them on another pass (since they might get additional |
| // leftover space). |
| pending_updates.emplace(view_index, new_size); |
| |
| // These numbers are based on ideal and not actual values we'll calculate |
| // below, because we want views which cannot use all of their adjusted space |
| // to drop out together rather than be order-dependent. |
| flex_total -= weight; |
| deficit -= to_deduct; |
| } |
| |
| // We have successfully allocated all of the remaining space. Apply the |
| // pending updates and we're done. |
| for (size_t pending_index : child_list) { |
| FlexChildData& flex_child = data.child_data[pending_index]; |
| ChildLayout& child_layout = data.layout.child_layouts[pending_index]; |
| flex_child.current_size = pending_updates[pending_index]; |
| child_layout.visible = true; |
| if (!child_spacing.HasViewIndex(pending_index)) |
| child_spacing.AddViewIndex(pending_index); |
| } |
| child_list.clear(); |
| } |
| |
| void FlexLayout::AllocateFlexExcessAtOrder( |
| const NormalizedSizeBounds& bounds, |
| SizeBound& to_allocate, |
| ChildIndices& child_list, |
| FlexLayoutData& data, |
| ChildViewSpacing& child_spacing) const { |
| int flex_total = CalculateFlexTotal(data, child_list); |
| |
| // Collect views that have preferred size zero (and are therefore still not |
| // visible) and see if we can allocate the additional required margins for |
| // them. If we can, make them all visible. If not, none are visible. |
| ChildIndices zero_size_children; |
| ChildViewSpacing temp_spacing(child_spacing); |
| const int old_spacing = temp_spacing.GetTotalSpace(); |
| base::ranges::copy_if(child_list, std::back_inserter(zero_size_children), |
| [&child_spacing](auto index) { |
| return !child_spacing.HasViewIndex(index); |
| }); |
| for (auto index : zero_size_children) |
| temp_spacing.AddViewIndex(index); |
| |
| if (!zero_size_children.empty()) { |
| // Make sure there is enough space to show each of the affected views. If |
| // there is not, none of them appear, so remove them and bail out. |
| const int new_spacing = temp_spacing.GetTotalSpace(); |
| const int delta = new_spacing - old_spacing; |
| // We'll factor in |flex_total| so that each child view should be allocated |
| // at least 1dp of space. That doesn't mean the child's flex rule will allow |
| // it to take up that space (see note below). |
| if (delta + flex_total > to_allocate) { |
| child_list.remove_if([child_spacing](size_t index) { |
| return !child_spacing.HasViewIndex(index); |
| }); |
| return; |
| } |
| |
| // Make all of the views visible, though note that at this point they are |
| // still zero-size, which typically does not happen elsewhere in FlexLayout. |
| // TODO(dfried): We could add a second boolean that would allow these views |
| // to be set to not visible but still "take up space" in the layout, or |
| // do some kind of post-processing pass to change the visibility flag to |
| // false once all of the other computations are complete, but I don't think |
| // it's worth the extra complexity until we have an actual use case or bug. |
| to_allocate -= delta; |
| child_spacing = temp_spacing; |
| for (size_t view_index : zero_size_children) |
| data.layout.child_layouts[view_index].visible = true; |
| } |
| |
| // See if we can't get through the remaining views, allocating size for each. |
| std::map<size_t, NormalizedSize> pending_updates; |
| SizeBound remaining = to_allocate; |
| for (auto it = child_list.begin(); remaining > 0 && it != child_list.end(); |
| ++it) { |
| const size_t view_index = *it; |
| FlexChildData& flex_child = data.child_data[view_index]; |
| ChildLayout& child_layout = data.layout.child_layouts[view_index]; |
| // On the excess pass, all of the views we're considering should be visible |
| // (at least once we've cleared the bit above). We should have also handled |
| // flex weight zero views earlier. |
| DCHECK(child_layout.visible); |
| |
| const int weight = flex_child.flex.weight(); |
| DCHECK_GT(weight, 0); |
| // Round up so we give slightly greater weight to earlier views. |
| SizeBound flex_amount = remaining; |
| if (remaining.is_bounded()) { |
| flex_amount = base::ClampCeil(remaining.value() * weight / |
| static_cast<float>(flex_total)); |
| } |
| const int old_size = flex_child.current_size.main(); |
| const SizeBound new_main = flex_amount + old_size; |
| |
| const NormalizedSizeBounds available( |
| new_main, GetCrossAxis(orientation(), child_layout.available_size)); |
| const NormalizedSize new_size = GetCurrentSizeForRule( |
| flex_child.flex.rule(), child_layout.child_view, available); |
| |
| // In cases where a view does not take up its entire available size, we |
| // need to set aside the space it does want and bail out (if there are other |
| // views we'll repeat the allocation at this priority). |
| const int to_deduct = new_size.main() - old_size; |
| if (new_size.main() < new_main) { |
| flex_child.current_size = new_size; |
| to_allocate -= to_deduct; |
| |
| child_list.erase(it); |
| return; |
| } |
| |
| DCHECK_GE(to_deduct, 0); |
| DCHECK_LE(to_deduct, remaining); |
| pending_updates.emplace(view_index, new_size); |
| |
| flex_total -= weight; |
| remaining -= to_deduct; |
| } |
| |
| // If we get here, we successfully allocated all of the space, so update |
| // everything and we're done. |
| to_allocate = remaining; |
| for (const auto& update : pending_updates) |
| data.child_data[update.first].current_size = update.second; |
| child_list.clear(); |
| } |
| |
| void FlexLayout::CalculateFlexAvailableSpace( |
| const NormalizedSizeBounds& bounds, |
| const ChildIndices& child_indices, |
| const ChildViewSpacing& child_spacing, |
| FlexLayoutData& data) const { |
| const SizeBound remaining_at_priority = |
| std::max<SizeBound>(0, bounds.main() - data.total_size.main()); |
| for (size_t index : child_indices) { |
| // We'll save the maximum amount of main axis size first offered to the |
| // view so we can report the maximum available size later. We only need to |
| // do this the first time because the available space decreases |
| // monotonically as we allocate flex space. |
| ChildLayout& child_layout = data.layout.child_layouts[index]; |
| if (!GetMainAxis(orientation(), child_layout.available_size).is_bounded()) { |
| // Calculate how much space this child view could take based on the |
| // total remaining flex space at this priority. Note that this is not |
| // the actual remaining space at this step, which will be based on flex |
| // used by previous children at the same priority. |
| const FlexChildData& flex_child = data.child_data[index]; |
| const int old_size = |
| child_layout.visible ? flex_child.current_size.main() : 0; |
| const SizeBound available_size = std::max<SizeBound>( |
| flex_child.current_size.main(), |
| child_spacing.GetMaxSize(index, old_size, remaining_at_priority)); |
| SetMainAxis(&child_layout.available_size, orientation(), available_size); |
| } |
| } |
| } |
| |
| void FlexLayout::AllocateZeroWeightFlex( |
| const NormalizedSizeBounds& bounds, |
| int flex_order, |
| ChildIndices& child_list, |
| FlexLayoutData& data, |
| ChildViewSpacing& child_spacing, |
| FlexOrderToViewIndexMap* expandable_views) const { |
| SizeBound remaining = |
| std::max<SizeBound>(0, bounds.main() - data.total_size.main()); |
| const bool is_first_pass = expandable_views != nullptr; |
| bool need_to_update_layout = false; |
| |
| // Allocate space to views with zero flex weight. They get first priority at |
| // this priority order. |
| auto it = child_list.begin(); |
| while (it != child_list.end()) { |
| const size_t child_index = *it; |
| FlexChildData& flex_child = data.child_data[child_index]; |
| |
| // We don't care about weighted flex in this step. |
| if (flex_child.flex.weight() > 0) { |
| ++it; |
| continue; |
| } |
| |
| ChildLayout& child_layout = data.layout.child_layouts[child_index]; |
| DCHECK(is_first_pass || child_layout.visible || |
| flex_child.preferred_size.main() == 0); |
| |
| const int old_size = |
| child_layout.visible ? flex_child.current_size.main() : 0; |
| const SizeBound available_cross = |
| GetCrossAxis(orientation(), child_layout.available_size); |
| const SizeBound available_main = |
| child_spacing.GetMaxSize(child_index, old_size, remaining); |
| const NormalizedSizeBounds available(available_main, available_cross); |
| NormalizedSize new_size = GetCurrentSizeForRule( |
| flex_child.flex.rule(), child_layout.child_view, available); |
| |
| if (is_first_pass && new_size.main() > flex_child.preferred_size.main()) { |
| new_size.set_main(flex_child.preferred_size.main()); |
| (*expandable_views)[flex_order].push_back(child_index); |
| } |
| |
| if (new_size.main() > old_size) { |
| const int delta = child_spacing.GetTotalSizeChangeForNewSize( |
| child_index, old_size, new_size.main()); |
| remaining -= delta; |
| child_layout.visible = true; |
| flex_child.current_size = new_size; |
| if (!child_spacing.HasViewIndex(child_index)) |
| child_spacing.AddViewIndex(child_index); |
| need_to_update_layout = true; |
| } |
| it = child_list.erase(it); |
| } |
| |
| if (need_to_update_layout) |
| UpdateLayoutFromChildren(bounds, data, child_spacing); |
| } |
| |
| SizeBound FlexLayout::TryAllocateAll( |
| const NormalizedSizeBounds& bounds, |
| int flex_order, |
| const ChildIndices& child_list, |
| FlexLayoutData& data, |
| ChildViewSpacing& child_spacing, |
| FlexOrderToViewIndexMap& expandable_views) const { |
| // Compute a new proposed spacing resulting from adding all the remaining |
| // child views at this order at their preferred sizes. |
| ChildViewSpacing proposed_spacing(child_spacing); |
| int delta = 0; |
| for (size_t child_index : child_list) { |
| const FlexChildData& flex_child = data.child_data[child_index]; |
| delta += proposed_spacing.GetTotalSizeChangeForNewSize( |
| child_index, flex_child.current_size.main(), |
| flex_child.preferred_size.main()); |
| if (!proposed_spacing.HasViewIndex(child_index)) |
| proposed_spacing.AddViewIndex(child_index); |
| } |
| const int new_total_size = data.total_size.main() + delta; |
| const SizeBound deficit = |
| std::max<SizeBound>(0, new_total_size - bounds.main()); |
| |
| if (deficit == 0) { |
| // If there's enough space to add all of these views up to their preferred |
| // size then add them all, and if there's excess space, add the children |
| // to |expandable_views| as well. |
| for (size_t child_index : child_list) { |
| FlexChildData& flex_child = data.child_data[child_index]; |
| if (flex_child.current_size.main() != flex_child.preferred_size.main()) { |
| // Need to recalculate the ideal size in the given bounds, which might |
| // not always be the preferred size. |
| const ChildLayout& child_layout = |
| data.layout.child_layouts[child_index]; |
| const NormalizedSize new_size = GetCurrentSizeForRule( |
| flex_child.flex.rule(), child_layout.child_view, |
| NormalizedSizeBounds( |
| flex_child.preferred_size.main(), |
| GetCrossAxis(orientation(), child_layout.available_size))); |
| flex_child.current_size = |
| NormalizedSize(flex_child.preferred_size.main(), new_size.cross()); |
| data.layout.child_layouts[child_index].visible = true; |
| } |
| } |
| if (new_total_size < bounds.main()) { |
| base::ranges::copy(child_list, |
| std::back_inserter(expandable_views[flex_order])); |
| } |
| |
| // All children have been allocated for this step at this point. |
| child_spacing = proposed_spacing; |
| } |
| |
| return deficit; |
| } |
| |
| // static |
| int FlexLayout::CalculateFlexTotal(const FlexLayoutData& data, |
| const ChildIndices& child_indices) { |
| return std::accumulate(child_indices.begin(), child_indices.end(), 0, |
| [&data](int total, size_t index) { |
| return total + data.child_data[index].flex.weight(); |
| }); |
| } |
| |
| // static |
| gfx::Size FlexLayout::DefaultFlexRuleImpl(const FlexLayout* flex_layout, |
| const View* view, |
| const SizeBounds& size_bounds) { |
| if (size_bounds == SizeBounds()) |
| return flex_layout->GetPreferredSize(view); |
| if (size_bounds == SizeBounds(0, 0)) |
| return flex_layout->GetMinimumSize(view); |
| return flex_layout->CalculateProposedLayout(size_bounds).host_size; |
| } |
| |
| } // namespace views |