| // 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/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/notimplemented.h" |
| #include "base/numerics/safe_conversions.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() << " min " << minimum_size.ToString() |
| << " base " << flex_base_content_size.ToString() << " max " |
| << maximum_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; |
| NormalizedSize pending_size; |
| NormalizedSize minimum_size; |
| NormalizedSize maximum_size; |
| NormalizedSize flex_base_content_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(std::optional<size_t>, |
| std::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: |
| std::optional<size_t> GetPreviousViewIndex(size_t view_index) const; |
| std::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(std::nullopt, std::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); |
| CHECK(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)); |
| std::optional<size_t> prev = GetPreviousViewIndex(view_index); |
| std::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; |
| } |
| } |
| |
| std::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 std::nullopt; |
| } |
| return std::prev(it)->first; |
| } |
| |
| std::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 std::nullopt; |
| } |
| return it->first; |
| } |
| |
| int FlexLayout::ChildViewSpacing::GetAddDelta(size_t view_index) const { |
| DCHECK(!HasViewIndex(view_index)); |
| std::optional<size_t> prev = GetPreviousViewIndex(view_index); |
| std::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() << "\n" << layout.ToString() << " {\n"; |
| bool first = true; |
| for (const FlexChildData& flex_child : child_data) { |
| if (first) { |
| first = false; |
| } else { |
| oss << ",\n"; |
| } |
| oss << flex_child.ToString(); |
| } |
| oss << "}\nmargin " << interior_margin.ToString() << " insets " |
| << host_insets.ToString() << "\n}"; |
| return oss.str(); |
| } |
| |
| void SetCurrentSize(size_t view_index, NormalizedSize size) { |
| child_data[view_index].current_size = size; |
| layout.child_layouts[view_index].visible = size.main() > 0; |
| } |
| |
| 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())); |
| |
| // The main idea of the new algorithm comes from css flexbox: |
| // https://www.w3.org/TR/css-flexbox-1/#box-manip Based on the css flexbox |
| // algorithm, combined with the old algorithm. Redesigned new algorithm. |
| // |
| // But there are some differences: |
| // 1. In css flex box, there is no situation where elements suddenly become |
| // invisible during layout. But in views it will. |
| // 2. CSS flex box does not have multiple layout orders. So we need to make |
| // special adjustments here |
| // |
| // Other more specific details will be explained in subsequent gazes. |
| |
| // 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); |
| |
| // If there are multiple orders. We need to first limit the maximum size to |
| // the preferred size. To ensure that subsequent views have a chance to reach |
| // the preferred size |
| if (order_to_view_index.size() > 1) { |
| std::vector<NormalizedSize> backup_size(data.num_children()); |
| for (size_t i = 0; i < data.num_children(); ++i) { |
| FlexChildData& flex_child = data.child_data[i]; |
| backup_size[i] = flex_child.maximum_size; |
| flex_child.maximum_size = flex_child.preferred_size; |
| } |
| AllocateFlexItem(bounds, order_to_view_index, data, child_spacing, true); |
| for (size_t i = 0; i < data.num_children(); ++i) { |
| FlexChildData& flex_child = data.child_data[i]; |
| flex_child.maximum_size = backup_size[i]; |
| } |
| } |
| AllocateFlexItem(bounds, order_to_view_index, data, child_spacing, true); |
| |
| // This is a different place too. Because css flexbox does not have dimensions |
| // that can be changed freely: Custom flex rules. |
| // |
| // So we may have unallocated space. |
| AllocateRemainingSpaceIfNeeded(bounds, order_to_view_index, 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); |
| |
| // According to css flexbox: |
| // https://www.w3.org/TR/css-flexbox-1/#algo-main-item $9.2.3 All layout |
| // algorithms in views should follow the rule listed in $9.2.3, subsection |
| // 'C'. So here the basic size is set according to the C rule. |
| flex_child.preferred_size = |
| GetPreferredSizeForRule(flex_child.flex.rule(), child, available_cross); |
| flex_child.minimum_size = |
| GetCurrentSizeForRule(flex_child.flex.rule(), child, |
| NormalizedSizeBounds(0, available_cross)); |
| flex_child.maximum_size = GetCurrentSizeForRule( |
| flex_child.flex.rule(), child, |
| NormalizedSizeBounds(bounds.main(), available_cross)); |
| |
| data.SetCurrentSize(view_index, main_axis_bounded |
| ? flex_child.minimum_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() || |
| (weight == 0 && |
| flex_child.maximum_size.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); |
| } |
| |
| if (main_axis_bounded) { |
| flex_child.flex_base_content_size = std::min<NormalizedSize>( |
| std::max<NormalizedSize>(flex_child.minimum_size, |
| flex_child.preferred_size), |
| flex_child.maximum_size); |
| } else { |
| flex_child.flex_base_content_size = flex_child.maximum_size; |
| } |
| } |
| } |
| |
| 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 (all_flex_indices.contains(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, |
| std::optional<size_t> child1_index, |
| std::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); |
| |
| 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 (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); |
| |
| // We only need to consider the cross axis size when aligning. But we |
| // should not let it affect total_size. Because this will affect the preferred |
| // size of the host view. |
| SizeBound cross_axis_size = |
| bounds.cross().is_bounded() && bounds.cross().value() > 0 |
| ? bounds.cross() |
| : data.total_size.cross(); |
| |
| // Calculate cross-axis positioning based on the cross margins and size that |
| // were calculated above. |
| const Span cross_span(0, cross_axis_size.value()); |
| 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]); |
| } |
| } |
| |
| NormalizedSize FlexLayout::ClampSizeToMinAndMax(FlexLayoutData& data, |
| const size_t view_index, |
| SizeBound size) const { |
| FlexChildData& flex_child = data.child_data[view_index]; |
| if (size.value() <= flex_child.minimum_size.main()) { |
| return flex_child.minimum_size; |
| } |
| |
| ChildLayout& child_layout = data.layout.child_layouts[view_index]; |
| |
| // See how much space the child view wants within the reduced space |
| // remaining for it. |
| const NormalizedSizeBounds available( |
| size, GetCrossAxis(orientation(), child_layout.available_size)); |
| const NormalizedSize new_size = GetCurrentSizeForRule( |
| flex_child.flex.rule(), child_layout.child_view, available); |
| |
| return std::min<NormalizedSize>( |
| std::max<NormalizedSize>(flex_child.minimum_size, new_size), |
| flex_child.maximum_size); |
| } |
| |
| void FlexLayout::AllocateFlexItem(const NormalizedSizeBounds& bounds, |
| const FlexOrderToViewIndexMap& order_to_index, |
| FlexLayoutData& data, |
| ChildViewSpacing& child_spacing, |
| bool skip_zero_preferred_size_view) const { |
| for (const auto& flex_elem : order_to_index) { |
| // Record available space for each view at this flex order. |
| CalculateFlexAvailableSpace(bounds, flex_elem.second, child_spacing, data); |
| |
| // Unlike css flexbox. Here we first deal with the view with 0 weight and |
| // the view with 0 preferred size. Because they have different meanings in |
| // views. |
| // We only need to allocate views with 0 preferred sizes if there are extra |
| // sizes left. |
| |
| // 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; |
| std::ranges::copy_if( |
| MaybeReverse(flex_elem.second, flex_allocation_order()), |
| std::back_inserter(view_indices), |
| [skip_zero_preferred_size_view, &data](size_t child_index) { |
| return !skip_zero_preferred_size_view || |
| data.child_data[child_index].preferred_size.main() > 0; |
| }); |
| |
| // Allocate zero-weight child views at this order first. This removes them |
| // from |view_indices|. |
| SizeBound remaining_free_space = |
| AllocateZeroWeightFlex(bounds, view_indices, data, child_spacing); |
| if (!skip_zero_preferred_size_view) { |
| FilterZeroSizeChildreIfNeeded(bounds, remaining_free_space, view_indices, |
| data, child_spacing); |
| } |
| |
| // Solve the problem of flexible size allocation. |
| while (ResolveFlexibleLengths(bounds, remaining_free_space, view_indices, |
| data, child_spacing)) { |
| continue; |
| } |
| |
| UpdateLayoutFromChildren(bounds, data, child_spacing); |
| } |
| } |
| |
| SizeBound FlexLayout::AllocateZeroWeightFlex( |
| const NormalizedSizeBounds& bounds, |
| ChildIndices& child_list, |
| FlexLayoutData& data, |
| ChildViewSpacing& child_spacing) const { |
| if (!bounds.main().is_bounded()) { |
| return SizeBound(); |
| } |
| |
| SizeBound remaining = |
| std::max<SizeBound>(0, bounds.main() - data.total_size.main()); |
| |
| // 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]; |
| |
| const int old_size = |
| child_layout.visible ? flex_child.current_size.main() : 0; |
| const SizeBound available_main = |
| child_spacing.GetMaxSize(child_index, old_size, remaining); |
| NormalizedSize new_size = |
| ClampSizeToMinAndMax(data, child_index, available_main); |
| |
| if (new_size.main() > old_size) { |
| const int delta = child_spacing.GetTotalSizeChangeForNewSize( |
| child_index, old_size, new_size.main()); |
| remaining -= delta; |
| data.SetCurrentSize(child_index, new_size); |
| if (!child_spacing.HasViewIndex(child_index)) { |
| child_spacing.AddViewIndex(child_index); |
| } |
| } |
| it = child_list.erase(it); |
| } |
| |
| return remaining; |
| } |
| |
| void FlexLayout::FilterZeroSizeChildreIfNeeded( |
| 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(); |
| std::ranges::copy_if(child_list, std::back_inserter(zero_size_children), |
| [&child_spacing, &data](auto index) { |
| return !child_spacing.HasViewIndex(index) && |
| data.child_data[index].preferred_size.main() == |
| 0; |
| }); |
| |
| if (zero_size_children.empty()) { |
| return; |
| } |
| |
| for (auto index : zero_size_children) { |
| temp_spacing.AddViewIndex(index); |
| } |
| |
| // 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, &data](size_t index) { |
| return !child_spacing.HasViewIndex(index) && |
| data.child_data[index].preferred_size.main() == 0; |
| }); |
| 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; |
| } |
| } |
| |
| bool FlexLayout::ResolveFlexibleLengths(const NormalizedSizeBounds& bounds, |
| SizeBound& remaining_free_space, |
| ChildIndices& child_list, |
| FlexLayoutData& data, |
| ChildViewSpacing& child_spacing) const { |
| if (!remaining_free_space.is_bounded()) { |
| return false; |
| } |
| |
| // Assume all subviews are visible. Calculate the total space change required |
| // to adjust from the current size to the main size. |
| 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.flex_base_content_size.main()); |
| if (!proposed_spacing.HasViewIndex(child_index)) { |
| proposed_spacing.AddViewIndex(child_index); |
| } |
| } |
| SizeBound temp_remaining_free_space = remaining_free_space - delta; |
| |
| // According to css flexbox: |
| // https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths $9.2.7 |
| // The following algorithm mainly comes from it. |
| |
| int flex_total = CalculateFlexTotal(data, child_list); |
| ChildIndices min_violations; |
| ChildIndices max_violations; |
| int total_violation = 0; |
| for (auto view_index : child_list) { |
| FlexChildData& flex_child = data.child_data[view_index]; |
| // We think it's already in the main sizes. Adjust according to remaining |
| // space. |
| SizeBound child_size = flex_child.flex_base_content_size.main(); |
| |
| const int weight = flex_child.flex.weight(); |
| DCHECK_GT(weight, 0); |
| const SizeBound extra_space = |
| base::ClampFloor(temp_remaining_free_space.value() * weight / |
| static_cast<float>(flex_total) + |
| 0.5f); |
| child_size += extra_space; |
| |
| // $9.2.7.4.C Constrain new dimensions under maximum and minimum dimensions. |
| const NormalizedSize new_size = |
| ClampSizeToMinAndMax(data, view_index, child_size); |
| flex_child.pending_size = new_size; |
| |
| int violation = new_size.main() - child_size.value(); |
| if (violation > 0) { |
| min_violations.push_back(view_index); |
| } else if (violation < 0) { |
| max_violations.push_back(view_index); |
| } |
| total_violation += violation; |
| temp_remaining_free_space -= extra_space; |
| flex_total -= weight; |
| } |
| |
| // $9.2.7.4.d: Fix min/max violations. |
| if (total_violation) { |
| FreezeViolations(child_list, remaining_free_space, |
| total_violation > 0 ? min_violations : max_violations, |
| child_spacing, data); |
| return true; |
| } else { |
| ChildIndices temp_list(child_list); |
| return FreezeViolations(child_list, remaining_free_space, temp_list, |
| child_spacing, data); |
| } |
| } |
| |
| bool FlexLayout::FreezeViolations(ChildIndices& child_list, |
| SizeBound& remaining_free_space, |
| ChildIndices& freeze_child_list, |
| ChildViewSpacing& child_spacing, |
| FlexLayoutData& data) const { |
| ChildViewSpacing new_spacing(child_spacing); |
| auto it = child_list.rbegin(); |
| bool force_relayout = false; |
| while (!freeze_child_list.empty() && it != child_list.rend()) { |
| const size_t view_index = *it; |
| if (view_index != freeze_child_list.back()) { |
| ++it; |
| continue; |
| } |
| |
| child_list.erase(--it.base()); |
| ChildLayout& child_layout = data.layout.child_layouts[view_index]; |
| FlexChildData& flex_child = data.child_data[view_index]; |
| NormalizedSize old_size = flex_child.current_size; |
| flex_child.current_size = flex_child.pending_size; |
| child_layout.visible = flex_child.current_size.main() > 0; |
| freeze_child_list.pop_back(); |
| |
| // If the view is not visible, the empty space itself is not given at this |
| // time. Just make the difference directly. |
| if (!child_layout.visible) { |
| remaining_free_space -= flex_child.current_size.main() - old_size.main(); |
| force_relayout = true; |
| break; |
| } |
| |
| remaining_free_space -= new_spacing.GetTotalSizeChangeForNewSize( |
| view_index, old_size.main(), flex_child.current_size.main()); |
| if (!new_spacing.HasViewIndex(view_index)) { |
| new_spacing.AddViewIndex(view_index); |
| } |
| } |
| |
| child_spacing = new_spacing; |
| return force_relayout; |
| } |
| |
| void FlexLayout::AllocateRemainingSpaceIfNeeded( |
| const NormalizedSizeBounds& bounds, |
| const FlexOrderToViewIndexMap& order_to_index, |
| FlexLayoutData& data, |
| ChildViewSpacing& child_spacing) const { |
| if (!bounds.main().is_bounded() || bounds.main() <= data.total_size.main()) { |
| return; |
| } |
| |
| // If there are any remaining sizes. We update the main size to the current |
| // size. Ensured that subsequent allocations are based on the current size. |
| for (size_t i = 0; i < data.num_children(); ++i) { |
| FlexChildData& flex_child = data.child_data[i]; |
| flex_child.flex_base_content_size = flex_child.current_size; |
| } |
| |
| AllocateFlexItem(bounds, order_to_index, data, child_spacing, false); |
| } |
| |
| 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); |
| } |
| } |
| } |
| |
| // 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 |