| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // 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 <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/logging.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "ui/events/event_target.h" |
| #include "ui/events/event_target_iterator.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/views/layout/flex_layout_types.h" |
| #include "ui/views/layout/flex_layout_types_internal.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_properties.h" |
| |
| // Module-private declarations ------------------------------------------------- |
| |
| namespace views { |
| |
| namespace internal { |
| |
| namespace { |
| |
| // Layout information for a specific child view in a proposed layout. |
| struct ChildLayout { |
| ChildLayout(View* view, const FlexSpecification& flex) |
| : view(view), flex(flex) {} |
| ChildLayout(ChildLayout&& other) = default; |
| |
| View* view = nullptr; |
| bool excluded = false; |
| // Indicates whether external code has called SetVisible(false) on the view. |
| bool hidden_by_owner = false; |
| // Indicates whether the layout has chosen to display this child view. |
| bool visible = false; |
| // Start with zero size rather than unspecified size bounds because we start |
| // all layouts with controls at their minimum allowed sizes. |
| NormalizedSizeBounds available_size{0, 0}; |
| NormalizedSize preferred_size; |
| NormalizedSize current_size; |
| NormalizedRect actual_bounds; |
| NormalizedInsets margins; |
| NormalizedInsets internal_padding; |
| FlexSpecification flex; |
| |
| private: |
| // 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. |
| DISALLOW_COPY_AND_ASSIGN(ChildLayout); |
| }; |
| |
| // Represents a specific stored layout given a set of size bounds. |
| struct Layout { |
| explicit Layout(int64_t layout_counter) : layout_id(layout_counter) {} |
| |
| // Indicates which layout pass this layout was created/last validated on. If |
| // this number disagrees with FlexLayoutInternal::layout_counter_, we need to |
| // re-validate the layout before using it. |
| mutable uint32_t layout_id; |
| std::vector<ChildLayout> child_layouts; |
| NormalizedInsets interior_margin; |
| NormalizedSizeBounds available_size; |
| NormalizedSize total_size; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(Layout); |
| }; |
| |
| // Preserves layouts for common layout requests, because we may be asked to |
| // recompute them many times but don't want to repeat the calculation. |
| // |
| // Specifically, we will be asked for: |
| // - The layout's minimum size (bounds = {0, 0}) |
| // - The layout's preferred size (neither bound specified) |
| // - The final layout at the dimensions of the host view (both bounds nonzero) |
| // |
| // There is also the possibility of a view with a flex layout being embedded in |
| // a view with a flex layout, where the inner view has a nontrivial flex |
| // specification. In that case, in order to handle e.g. labels in the inner |
| // view, the outer layout manager will ask for the inner view's preferred height |
| // for its width, which potentially involves another layout calculation. |
| // However, these calculations are transient - they only happen when the layout |
| // is recalculated or validated, and in the latter case only one set of bounds |
| // is evaluated. So we will keep exactly one of these layouts around. |
| // |
| // As a first pass, we'll store all of these specifically. In the future we can |
| // switch to using something more sophisticated, like an MRU cache, if we find |
| // we're thrashing for some other reason. |
| class LayoutCache { |
| public: |
| // Removes all saved layouts. |
| void Clear() { |
| minimum_.reset(); |
| preferred_.reset(); |
| intermediate_.layout.reset(); |
| current_.layout.reset(); |
| } |
| |
| // Gets a cached layout, or nullptr if no layout is cached for the specified |
| // bounds. |
| Layout* Get(const NormalizedSizeBounds& bounds) const { |
| if (bounds == NormalizedSizeBounds(0, 0)) |
| return minimum_.get(); |
| if (bounds == NormalizedSizeBounds()) |
| return preferred_.get(); |
| if (bounds == intermediate_.bounds) |
| return intermediate_.layout.get(); |
| if (bounds == current_.bounds) |
| return current_.layout.get(); |
| return nullptr; |
| } |
| |
| // Caches a layout associated with the specified bounds. |
| void Put(const NormalizedSizeBounds& bounds, std::unique_ptr<Layout> layout) { |
| if (bounds == NormalizedSizeBounds(0, 0)) { |
| minimum_ = std::move(layout); |
| } else if (bounds == NormalizedSizeBounds()) { |
| preferred_ = std::move(layout); |
| } else if (!bounds.cross() || !bounds.main()) { |
| intermediate_.bounds = bounds; |
| intermediate_.layout = std::move(layout); |
| } else { |
| current_.bounds = bounds; |
| current_.layout = std::move(layout); |
| } |
| } |
| |
| private: |
| struct CacheEntry { |
| NormalizedSizeBounds bounds; |
| std::unique_ptr<Layout> layout; |
| }; |
| |
| std::unique_ptr<Layout> minimum_; |
| std::unique_ptr<Layout> preferred_; |
| |
| // TODO(dfried): consider replacing these with an MRUCache if this ends up |
| // thrashing a lot (see note above). |
| CacheEntry intermediate_; |
| CacheEntry current_; |
| }; |
| |
| // Calculates and maintains 1D spacing between a sequence of child views. |
| class 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(base::Optional<int>, base::Optional<int>)>; |
| |
| explicit ChildViewSpacing(const GetViewSpacingCallback& get_view_spacing); |
| ChildViewSpacing(ChildViewSpacing&& other); |
| ChildViewSpacing& operator=(ChildViewSpacing&& other); |
| |
| bool HasViewIndex(int view_index) const; |
| int GetLeadingInset() const; |
| int GetTrailingInset() const; |
| int GetLeadingSpace(int view_index) const; |
| int GetTrailingSpace(int view_index) const; |
| int GetTotalSpace() const; |
| |
| // Returns the change in space required if the specified view index were |
| // added. |
| int GetAddDelta(int view_index) 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(int view_index, |
| int* new_leading = nullptr, |
| int* new_trailing = nullptr); |
| |
| private: |
| base::Optional<int> GetPreviousViewIndex(int view_index) const; |
| base::Optional<int> GetNextViewIndex(int view_index) const; |
| |
| GetViewSpacingCallback get_view_spacing_; |
| // Maps from view index to the leading spacing for that index. |
| std::map<int, int> leading_spacings_; |
| // The trailing space (space preceding the trailing margin). |
| int trailing_space_; |
| }; |
| |
| ChildViewSpacing::ChildViewSpacing( |
| const GetViewSpacingCallback& get_view_spacing) |
| : get_view_spacing_(get_view_spacing), |
| trailing_space_( |
| get_view_spacing.Run(base::Optional<int>(), base::Optional<int>())) {} |
| |
| ChildViewSpacing::ChildViewSpacing(ChildViewSpacing&& other) |
| : get_view_spacing_(std::move(other.get_view_spacing_)), |
| leading_spacings_(std::move(other.leading_spacings_)), |
| trailing_space_(other.trailing_space_) {} |
| |
| ChildViewSpacing& ChildViewSpacing::operator=(ChildViewSpacing&& other) { |
| if (this != &other) { |
| get_view_spacing_ = std::move(other.get_view_spacing_); |
| leading_spacings_ = std::move(other.leading_spacings_); |
| trailing_space_ = other.trailing_space_; |
| } |
| return *this; |
| } |
| |
| bool ChildViewSpacing::HasViewIndex(int view_index) const { |
| return leading_spacings_.find(view_index) != leading_spacings_.end(); |
| } |
| |
| int ChildViewSpacing::GetLeadingInset() const { |
| if (leading_spacings_.empty()) |
| return 0; |
| return leading_spacings_.begin()->second; |
| } |
| |
| int ChildViewSpacing::GetTrailingInset() const { |
| return trailing_space_; |
| } |
| |
| int ChildViewSpacing::GetLeadingSpace(int view_index) const { |
| auto it = leading_spacings_.find(view_index); |
| DCHECK(it != leading_spacings_.end()); |
| return it->second; |
| } |
| |
| int ChildViewSpacing::GetTrailingSpace(int view_index) const { |
| auto it = leading_spacings_.find(view_index); |
| DCHECK(it != leading_spacings_.end()); |
| if (++it == leading_spacings_.end()) |
| return trailing_space_; |
| return it->second; |
| } |
| |
| int ChildViewSpacing::GetTotalSpace() const { |
| int space = trailing_space_; |
| for (auto& pr : leading_spacings_) |
| space += pr.second; |
| return space; |
| } |
| |
| int ChildViewSpacing::GetAddDelta(int view_index) const { |
| DCHECK(!HasViewIndex(view_index)); |
| base::Optional<int> prev = GetPreviousViewIndex(view_index); |
| base::Optional<int> 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; |
| } |
| |
| void ChildViewSpacing::AddViewIndex(int view_index, |
| int* new_leading, |
| int* new_trailing) { |
| DCHECK(!HasViewIndex(view_index)); |
| base::Optional<int> prev = GetPreviousViewIndex(view_index); |
| base::Optional<int> 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; |
| } |
| |
| base::Optional<int> ChildViewSpacing::GetPreviousViewIndex( |
| int view_index) const { |
| auto it = leading_spacings_.lower_bound(view_index); |
| if (it == leading_spacings_.begin()) |
| return base::Optional<int>(); |
| return (--it)->first; |
| } |
| |
| base::Optional<int> ChildViewSpacing::GetNextViewIndex(int view_index) const { |
| auto it = leading_spacings_.upper_bound(view_index); |
| if (it == leading_spacings_.end()) |
| return base::Optional<int>(); |
| return it->first; |
| } |
| |
| // Utility functions ----------------------------------------------------------- |
| |
| gfx::Insets GetInternalPadding(const View* view) { |
| const gfx::Insets* const margins = |
| view->GetProperty(views::kInternalPaddingKey); |
| return margins ? *margins : gfx::Insets(); |
| } |
| |
| } // anonymous namespace |
| |
| // Private implementation ------------------------------------------------------ |
| |
| // Holds child-view-specific layout parameters that are not stored in the |
| // properties system. |
| // |
| // We should consider storing some or all of these in the properites system. |
| struct ChildLayoutParams { |
| bool excluded = false; |
| bool hidden_by_owner = false; |
| base::Optional<FlexSpecification> flex_specification; |
| }; |
| |
| // Internal data structure and functionality for FlexLayout so we don't have to |
| // declare a bunch of classes and data in the .h file. |
| class FlexLayoutInternal { |
| public: |
| explicit FlexLayoutInternal(FlexLayout* layout) |
| : layout_(*layout) {} //, cached_layouts_(kMaxCachedLayouts) {} |
| |
| // Suggests that the current layout needs to be recalculated. Setting |force| |
| // to true indicates that we know all of the cached layouts are invalid and we |
| // should discard them; otherwise we will keep them and re-validate on the |
| // next layout pass. |
| void InvalidateLayout(bool force); |
| |
| // Gets the proposed layout for a set of size bounds. Returns a cached layout |
| // if one is present and valid. |
| const Layout& CalculateLayout(const SizeBounds& bounds); |
| |
| // Applies an existing layout to all child views, with the appropriate |
| // current alignment. |
| void DoLayout(const Layout& layout, const gfx::Rect& bounds); |
| |
| private: |
| LayoutOrientation orientation() const { return layout_.orientation(); } |
| |
| // Determines whether a layout is still valid. |
| bool IsLayoutValid(const Layout& cached_layout) const; |
| |
| // Creates a brand new layout from the available |bounds|. |
| // Call DoLayout() to actually apply the layout. |
| const Layout& CalculateNewLayout(const NormalizedSizeBounds& bounds); |
| |
| // Calculates the position of each child view and the size of the overall |
| // layout based on tentative visibilities and sizes for each child. |
| void UpdateLayoutFromChildren(Layout* layout, |
| ChildViewSpacing* child_spacing, |
| const NormalizedSizeBounds& bounds) const; |
| |
| // Calculates a margin between two child views based on each's margin and any |
| // internal padding present in one or both elements. Uses properties of the |
| // layout, like whether adjacent margins should be collapsed. |
| int CalculateMargin(int margin1, int margin2, int internal_padding) const; |
| |
| // Calculates the preferred spacing between two child views, or between a |
| // view edge and the first or last visible child views. |
| int CalculateChildSpacing(const Layout& layout, |
| base::Optional<int> child1_index, |
| base::Optional<int> child2_index) const; |
| |
| const gfx::Insets& GetMargins(const View* view) const; |
| |
| FlexLayout& layout_; |
| |
| // Instead of marking each layout as dirty/needing validation, we instead keep |
| // a value that changes every time InvalidateLayout() is called. If the value |
| // stored in a cached layout doesn't match this value, we validate it and |
| // update the value. We use an int64_t because the odds of spurious collision |
| // (i.e. the counter wrapping back around to the *exact* same value before |
| // validating) are effectively zero. |
| uint32_t layout_counter_ = 0; |
| LayoutCache layout_cache_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FlexLayoutInternal); |
| }; |
| |
| void FlexLayoutInternal::InvalidateLayout(bool force) { |
| ++layout_counter_; |
| if (force) |
| layout_cache_.Clear(); |
| } |
| |
| const Layout& FlexLayoutInternal::CalculateLayout(const SizeBounds& bounds) { |
| // If bounds are smaller than the minimum cross axis size, expand them. |
| NormalizedSizeBounds normalized_bounds = Normalize(orientation(), bounds); |
| if (normalized_bounds.cross().has_value() && |
| normalized_bounds.cross().value() < layout_.minimum_cross_axis_size()) { |
| normalized_bounds = NormalizedSizeBounds{normalized_bounds.main(), |
| layout_.minimum_cross_axis_size()}; |
| } |
| |
| // See if we have a cached layout already that is still valid. |
| Layout* const cached_layout = layout_cache_.Get(normalized_bounds); |
| if (cached_layout && IsLayoutValid(*cached_layout)) |
| return *cached_layout; |
| |
| // Calculate a new layout from scratch. |
| return CalculateNewLayout(normalized_bounds); |
| } |
| |
| void FlexLayoutInternal::DoLayout(const Layout& layout, |
| const gfx::Rect& bounds) { |
| NormalizedPoint start = Normalize(orientation(), bounds.origin()); |
| |
| // Apply main axis alignment. |
| const int excess_main = |
| Normalize(orientation(), bounds.size()).main() - layout.total_size.main(); |
| switch (layout_.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: |
| NOTIMPLEMENTED() << "Main axis stretch/justify is not yet supported."; |
| break; |
| } |
| |
| // Position controls within the client area. |
| for (const ChildLayout& child_layout : layout.child_layouts) { |
| if (child_layout.excluded) |
| continue; |
| if (child_layout.visible != child_layout.view->visible()) |
| layout_.SetViewVisibility(child_layout.view, child_layout.visible); |
| if (child_layout.visible) { |
| NormalizedRect actual = child_layout.actual_bounds; |
| actual.Offset(start.main(), start.cross()); |
| gfx::Rect denormalized = Denormalize(orientation(), actual); |
| child_layout.view->SetBoundsRect(denormalized); |
| } |
| } |
| } |
| |
| int FlexLayoutInternal::CalculateMargin(int margin1, |
| int margin2, |
| int internal_padding) const { |
| const int result = layout_.collapse_margins() ? std::max(margin1, margin2) |
| : margin1 + margin2; |
| return std::max(0, result - internal_padding); |
| } |
| |
| int FlexLayoutInternal::CalculateChildSpacing( |
| const Layout& layout, |
| base::Optional<int> child1_index, |
| base::Optional<int> child2_index) const { |
| const int left_margin = |
| child1_index ? layout.child_layouts[*child1_index].margins.main_trailing() |
| : layout.interior_margin.main_leading(); |
| const int right_margin = |
| child2_index ? layout.child_layouts[*child2_index].margins.main_leading() |
| : layout.interior_margin.main_trailing(); |
| const int left_padding = |
| child1_index |
| ? layout.child_layouts[*child1_index].internal_padding.main_trailing() |
| : 0; |
| const int right_padding = |
| child2_index |
| ? layout.child_layouts[*child2_index].internal_padding.main_leading() |
| : 0; |
| |
| return CalculateMargin(left_margin, right_margin, |
| left_padding + right_padding); |
| } |
| |
| const gfx::Insets& FlexLayoutInternal::GetMargins(const View* view) const { |
| const gfx::Insets* const margins = view->GetProperty(views::kMarginsKey); |
| return margins ? *margins : layout_.default_child_margins(); |
| } |
| |
| void FlexLayoutInternal::UpdateLayoutFromChildren( |
| Layout* layout, |
| ChildViewSpacing* child_spacing, |
| const NormalizedSizeBounds& bounds) const { |
| // Calculate starting minimum for cross-axis size. |
| const int min_cross_size = |
| std::max(layout_.minimum_cross_axis_size(), |
| CalculateMargin(layout->interior_margin.cross_leading(), |
| layout->interior_margin.cross_trailing(), 0)); |
| layout->total_size = NormalizedSize(0, min_cross_size); |
| if (bounds.cross().has_value()) |
| layout->total_size.SetToMax(0, bounds.cross().value()); |
| |
| std::vector<Inset1D> cross_spacings(layout->child_layouts.size()); |
| for (size_t i = 0; i < layout->child_layouts.size(); ++i) { |
| ChildLayout& child_layout = layout->child_layouts[i]; |
| |
| // We don't have to deal with excluded or invisible children. |
| if (child_layout.excluded || !child_layout.visible) |
| continue; |
| |
| // Update the cross-axis size. |
| Inset1D& cross_spacing = cross_spacings[i]; |
| cross_spacing.set_leading( |
| CalculateMargin(layout->interior_margin.cross_leading(), |
| child_layout.margins.cross_leading(), |
| child_layout.internal_padding.cross_leading())); |
| cross_spacing.set_trailing( |
| CalculateMargin(layout->interior_margin.cross_trailing(), |
| child_layout.margins.cross_trailing(), |
| child_layout.internal_padding.cross_trailing())); |
| |
| const int cross_size = std::min(child_layout.current_size.cross(), |
| child_layout.preferred_size.cross()); |
| layout->total_size.SetToMax(0, cross_spacing.size() + cross_size); |
| |
| // 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); |
| layout->total_size.Enlarge(leading_space, 0); |
| |
| const int size_main = child_layout.current_size.main(); |
| child_layout.actual_bounds.set_origin_main(layout->total_size.main()); |
| child_layout.actual_bounds.set_size_main(size_main); |
| layout->total_size.Enlarge(size_main, 0); |
| } |
| |
| // Add the end margin. |
| layout->total_size.Enlarge(child_spacing->GetTrailingInset(), 0); |
| |
| // Calculate vertical positioning given the cross-axis size we've already |
| // calculated above. |
| const Span cross_span(0, layout->total_size.cross()); |
| for (size_t i = 0; i < layout->child_layouts.size(); ++i) { |
| ChildLayout& child_layout = layout->child_layouts[i]; |
| |
| // We don't have to deal with excluded or invisible children. |
| if (child_layout.excluded || !child_layout.visible) |
| continue; |
| |
| // Because we have an explicit kStretch option, regardless of what the |
| // control *says* we won't allow the cross-axis size to exceed the preferred |
| // size. kStretch may cause it to become larger. |
| const int cross_size = std::min(child_layout.current_size.cross(), |
| child_layout.preferred_size.cross()); |
| child_layout.actual_bounds.set_size_cross(cross_size); |
| child_layout.actual_bounds.AlignCross( |
| cross_span, layout_.cross_axis_alignment(), cross_spacings[i]); |
| } |
| } |
| |
| const Layout& FlexLayoutInternal::CalculateNewLayout( |
| const NormalizedSizeBounds& bounds) { |
| DCHECK(!bounds.cross().has_value() || |
| bounds.cross().value() >= layout_.minimum_cross_axis_size()); |
| std::unique_ptr<Layout> layout = std::make_unique<Layout>(layout_counter_); |
| layout->interior_margin = Normalize(orientation(), layout_.interior_margin()); |
| std::map<int, std::vector<int>> order_to_view_index; |
| const bool main_axis_bounded = bounds.main().has_value(); |
| |
| // Start with the smallest size the child view can occupy. |
| NormalizedSizeBounds effective_bounds{0, bounds.cross()}; |
| if (effective_bounds.cross()) { |
| effective_bounds.set_cross(std::max( |
| 0, *effective_bounds.cross() - layout->interior_margin.cross_size())); |
| } |
| |
| // Step through the children, creating placeholder layout view elements |
| // and setting up initial minimal visibility. |
| View* const view = layout_.host(); |
| for (int i = 0; i < view->child_count(); ++i) { |
| View* child = view->child_at(i); |
| layout->child_layouts.emplace_back(child, layout_.GetFlexForView(child)); |
| ChildLayout& child_layout = layout->child_layouts.back(); |
| |
| child_layout.excluded = layout_.IsViewExcluded(child); |
| if (child_layout.excluded) |
| continue; |
| |
| child_layout.margins = Normalize(orientation(), GetMargins(child)); |
| child_layout.internal_padding = |
| Normalize(orientation(), GetInternalPadding(child)); |
| child_layout.preferred_size = |
| Normalize(orientation(), child->GetPreferredSize()); |
| |
| child_layout.hidden_by_owner = layout_.IsHiddenByOwner(child); |
| child_layout.visible = !child_layout.hidden_by_owner; |
| if (child_layout.hidden_by_owner) |
| continue; |
| |
| // gfx::Size calculation depends on whether flex is allowed. |
| if (main_axis_bounded) { |
| child_layout.current_size = |
| Normalize(orientation(), |
| child_layout.flex.rule().Run( |
| child, Denormalize(orientation(), effective_bounds))); |
| |
| // We should revisit whether this is a valid assumption for text views |
| // in vertical layouts. |
| DCHECK_GE(child_layout.preferred_size.main(), |
| child_layout.current_size.main()); |
| |
| // Keep track of non-hidden flex controls. |
| const bool can_flex = |
| (child_layout.flex.weight() > 0 && |
| !child_layout.preferred_size.is_empty()) || |
| child_layout.current_size.main() < child_layout.preferred_size.main(); |
| if (can_flex) |
| order_to_view_index[child_layout.flex.order()].push_back(i); |
| |
| } else { |
| // All non-flex or unbounded controls get preferred size. |
| child_layout.current_size = child_layout.preferred_size; |
| } |
| |
| child_layout.visible = !child_layout.current_size.is_empty(); |
| |
| // Actual size and positioning will be set during the final layout |
| // calculation. |
| } |
| |
| // Do the initial layout update, calculating spacing between children. |
| ChildViewSpacing child_spacing( |
| base::BindRepeating(&FlexLayoutInternal::CalculateChildSpacing, |
| base::Unretained(this), base::ConstRef(*layout))); |
| UpdateLayoutFromChildren(layout.get(), &child_spacing, bounds); |
| |
| if (main_axis_bounded && !order_to_view_index.empty()) { |
| // Step through each flex priority allocating as much remaining space as |
| // possible to each flex view. |
| for (auto flex_it = order_to_view_index.begin(); |
| flex_it != order_to_view_index.end(); ++flex_it) { |
| // Check to see we haven't filled available space. |
| int remaining = *bounds.main() - layout->total_size.main(); |
| if (remaining <= 0) { |
| break; |
| } |
| |
| // The flex algorithm we're using works as follows: |
| // * For each child view at a particular flex order: |
| // - Calculate the percentage of the remaining flex space to allocate |
| // based on the ratio of its weight to the total unallocated weight |
| // at that order. |
| // - If the child view is already visible (it will be at its minimum |
| // size, which may or may not be zero), add the space the child is |
| // already taking up. |
| // - If the child view is not visible and adding it would introduce |
| // additional margin space between child views, subtract that |
| // additional space from the amount available. |
| // - Ask the child view's flex rule how large it would like to be |
| // within the space available. |
| // - If the child view would like to be larger, make it so, and |
| // subtract the additional space consumed by the child and its |
| // margins from the total remaining flex space. |
| // |
| // Note that this algorithm isn't *perfect* for specific cases, which are |
| // noted below; namely when margins very asymmetrical the sizing of child |
| // views can be slightly different from what would otherwise be expected. |
| // We have a TODO to look at ways of making this algorithm more "fair" in |
| // the future (but in the meantime most issues can be resolved by setting |
| // reasonable margins and by using flex order). |
| |
| // Build a list of the elements to flex. |
| int flex_total = 0; |
| std::for_each(flex_it->second.begin(), flex_it->second.end(), |
| [&](int index) { |
| auto weight = layout->child_layouts[index].flex.weight(); |
| if (weight > 0) |
| flex_total += weight; |
| }); |
| |
| // Note: because the child views are evaluated in order, if preferred |
| // minimum sizes are not consistent across a single priority expanding |
| // the parent control could result in children swapping visibility. |
| // We currently consider this user error; if the behavior is not |
| // desired, prioritize the child views' flex. |
| for (auto index_it = flex_it->second.begin(); |
| remaining >= 0 && index_it != flex_it->second.end(); ++index_it) { |
| const int view_index = *index_it; |
| |
| ChildLayout& child_layout = layout->child_layouts[view_index]; |
| |
| // Offer a share of the remaining space to the view. |
| int flex_amount; |
| if (child_layout.flex.weight() > 0) { |
| const int flex_weight = child_layout.flex.weight(); |
| // Round up so we give slightly greater weight to earlier views. |
| flex_amount = |
| int{std::ceil((float{remaining} * flex_weight) / flex_total)}; |
| flex_total -= flex_weight; |
| } else { |
| flex_amount = remaining; |
| } |
| |
| // If the layout was previously invisible, then making it visible |
| // may result in the addition of margin space, so we'll have to |
| // recalculate the margins on either side of this view. The change in |
| // margin space (if any) counts against the child view's flex space |
| // allocation. |
| // |
| // Note: In cases where the layout's internal margins and/or the child |
| // views' margins are wildly different sizes, subtracting the full delta |
| // out of the available space can cause the first view to be smaller |
| // than we would expect (see TODOs in unit tests for examples). We |
| // should look into ways to make this "feel" better (but in the |
| // meantime, please try to specify reasonable margins). |
| const int margin_delta = child_spacing.HasViewIndex(view_index) |
| ? 0 |
| : child_spacing.GetAddDelta(view_index); |
| |
| // This is the space on the main axis that was already allocated to the |
| // child view; it will be added to the total flex space for the child |
| // view since it is considered a fixed overhead of the layout if it is |
| // nonzero. |
| const int old_size = |
| child_layout.visible ? child_layout.current_size.main() : 0; |
| |
| // Offer the modified flex space to the child view and see how large it |
| // wants to be (or if it wants to be visible at that size at all). |
| const NormalizedSizeBounds available( |
| flex_amount + old_size - margin_delta, effective_bounds.cross()); |
| const NormalizedSize new_size = Normalize( |
| orientation(), |
| child_layout.flex.rule().Run( |
| child_layout.view, Denormalize(orientation(), available))); |
| if (new_size.is_empty()) |
| continue; |
| |
| // If the amount of space claimed increases (but is still within |
| // bounds set by our flex rule) we can make the control visible and |
| // claim the additional space. |
| const int to_deduct = (new_size.main() - old_size) + margin_delta; |
| DCHECK_GE(to_deduct, 0); |
| if (to_deduct > 0 && to_deduct <= remaining) { |
| child_layout.available_size = available; |
| child_layout.current_size = new_size; |
| child_layout.visible = true; |
| remaining -= to_deduct; |
| if (!child_spacing.HasViewIndex(view_index)) |
| child_spacing.AddViewIndex(view_index); |
| } |
| } |
| |
| // Reposition the child controls (taking margins into account) and |
| // calculate remaining space. |
| UpdateLayoutFromChildren(layout.get(), &child_spacing, bounds); |
| } |
| } |
| |
| const Layout& result = *layout; |
| layout_cache_.Put(bounds, std::move(layout)); |
| return result; |
| } |
| |
| bool FlexLayoutInternal::IsLayoutValid(const Layout& cached_layout) const { |
| // If we've already evaluated a layout for this size and not been |
| // invalidated since then, then this is a valid layout. |
| if (cached_layout.layout_id == layout_counter_) |
| return true; |
| |
| // Need to compare preferred child sizes with what we're seeing. |
| View* const view = layout_.host(); |
| int child_index = 0; |
| for (const ChildLayout& proposed_view_layout : cached_layout.child_layouts) { |
| // Check that there is another child and that it's the view we expect. |
| DCHECK(child_index < view->child_count()) |
| << "Child views should not be removed without clearing the cache."; |
| |
| const View* child = view->child_at(child_index++); |
| |
| // Ensure child views have not been reordered. |
| if (child != proposed_view_layout.view) |
| return false; |
| |
| // Ignore hidden and excluded views, unless their status has changed. |
| const bool excluded = layout_.IsViewExcluded(child); |
| if (proposed_view_layout.excluded != excluded) |
| return false; |
| if (excluded) |
| continue; |
| |
| const bool hidden_by_owner = layout_.IsHiddenByOwner(child); |
| if (proposed_view_layout.hidden_by_owner != hidden_by_owner) |
| return false; |
| if (hidden_by_owner) |
| continue; |
| |
| // Sanity check that a child's visibility hasn't been modified outside |
| // the layout manager. |
| if (proposed_view_layout.visible != child->visible()) |
| return false; |
| |
| if (proposed_view_layout.visible) { |
| // Check that view margins haven't changed for visible controls. |
| if (GetMargins(child) != |
| Denormalize(orientation(), proposed_view_layout.margins)) |
| return false; |
| |
| // Same for internal padding. |
| if (GetInternalPadding(child) != |
| Denormalize(orientation(), proposed_view_layout.internal_padding)) |
| return false; |
| } |
| |
| // Check that the control still has the same preferred size given its |
| // flex and visibility. |
| if (child->GetPreferredSize() != |
| Denormalize(orientation(), proposed_view_layout.preferred_size)) |
| return false; |
| |
| const gfx::Size preferred = proposed_view_layout.flex.rule().Run( |
| child, Denormalize(orientation(), proposed_view_layout.available_size)); |
| if (preferred != |
| Denormalize(orientation(), proposed_view_layout.current_size)) |
| return false; |
| } |
| |
| DCHECK_EQ(child_index, view->child_count()) << "Child views added without " |
| "clearing the cache - this " |
| "should not happen."; |
| |
| // This layout is still valid. Update the layout counter to show it's valid |
| // in the current layout context. |
| cached_layout.layout_id = layout_counter_; |
| return true; |
| } |
| |
| } // namespace internal |
| |
| // FlexLayout |
| // ------------------------------------------------------------------- |
| |
| FlexLayout::FlexLayout() |
| : internal_(std::make_unique<internal::FlexLayoutInternal>(this)) {} |
| |
| FlexLayout::~FlexLayout() {} |
| |
| FlexLayout& FlexLayout::SetOrientation(LayoutOrientation orientation) { |
| if (orientation != orientation_) { |
| orientation_ = orientation; |
| internal_->InvalidateLayout(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetCollapseMargins(bool collapse_margins) { |
| if (collapse_margins != collapse_margins_) { |
| collapse_margins_ = collapse_margins; |
| internal_->InvalidateLayout(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; |
| internal_->InvalidateLayout(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetCrossAxisAlignment( |
| LayoutAlignment cross_axis_alignment) { |
| if (cross_axis_alignment_ != cross_axis_alignment) { |
| cross_axis_alignment_ = cross_axis_alignment; |
| internal_->InvalidateLayout(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetInteriorMargin(const gfx::Insets& interior_margin) { |
| if (interior_margin_ != interior_margin) { |
| interior_margin_ = interior_margin; |
| internal_->InvalidateLayout(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetMinimumCrossAxisSize(int size) { |
| if (minimum_cross_axis_size_ != size) { |
| minimum_cross_axis_size_ = size; |
| internal_->InvalidateLayout(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetDefaultChildMargins(const gfx::Insets& margins) { |
| if (default_child_margins_ != margins) { |
| default_child_margins_ = margins; |
| internal_->InvalidateLayout(true); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetFlexForView( |
| const View* view, |
| const FlexSpecification& flex_specification) { |
| auto it = child_params_.find(view); |
| DCHECK(it != child_params_.end()); |
| it->second.flex_specification = flex_specification; |
| internal_->InvalidateLayout(true); |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::ClearFlexForView(const View* view) { |
| auto it = child_params_.find(view); |
| DCHECK(it != child_params_.end()); |
| it->second.flex_specification.reset(); |
| internal_->InvalidateLayout(true); |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetViewExcluded(const View* view, bool excluded) { |
| auto it = child_params_.find(view); |
| DCHECK(it != child_params_.end()); |
| if (excluded != it->second.excluded) { |
| it->second.excluded = excluded; |
| InvalidateLayout(); |
| } |
| return *this; |
| } |
| |
| FlexLayout& FlexLayout::SetDefaultFlex( |
| const FlexSpecification& flex_specification) { |
| default_flex_ = flex_specification; |
| internal_->InvalidateLayout(true); |
| return *this; |
| } |
| |
| const FlexSpecification& FlexLayout::GetFlexForView(const View* view) const { |
| auto it = child_params_.find(view); |
| DCHECK(it != child_params_.end()); |
| const base::Optional<FlexSpecification>& spec = it->second.flex_specification; |
| return spec ? *spec : default_flex_; |
| } |
| |
| bool FlexLayout::IsViewExcluded(const View* view) const { |
| auto it = child_params_.find(view); |
| DCHECK(it != child_params_.end()); |
| return it->second.excluded; |
| } |
| |
| bool FlexLayout::IsHiddenByOwner(const View* view) const { |
| auto it = child_params_.find(view); |
| DCHECK(it != child_params_.end()); |
| return it->second.hidden_by_owner; |
| } |
| |
| gfx::Size FlexLayout::GetPreferredSize(const View* host) const { |
| DCHECK_EQ(host_, host); |
| return GetPreferredSize(SizeBounds()); |
| } |
| |
| int FlexLayout::GetPreferredHeightForWidth(const View* host, int width) const { |
| DCHECK_EQ(host_, host); |
| return GetPreferredSize(SizeBounds{width, base::Optional<int>()}).height(); |
| } |
| |
| gfx::Size FlexLayout::GetMinimumSize(const View* host) const { |
| DCHECK_EQ(host_, host); |
| return GetPreferredSize(SizeBounds{0, 0}); |
| } |
| |
| void FlexLayout::InvalidateLayout() { |
| internal_->InvalidateLayout(false); |
| } |
| |
| void FlexLayout::Installed(View* host) { |
| DCHECK(!host_); |
| host_ = host; |
| // Add all the existing children when the layout manager is installed. |
| // If new children are added, ViewAdded() will be called and we'll add data |
| // there. |
| for (int i = 0; i < host->child_count(); ++i) { |
| View* const child = host->child_at(i); |
| internal::ChildLayoutParams child_layout_params; |
| child_layout_params.hidden_by_owner = !child->visible(); |
| child_params_.emplace(child, child_layout_params); |
| } |
| } |
| |
| void FlexLayout::ViewAdded(View* host, View* view) { |
| DCHECK_EQ(host_, host); |
| internal::ChildLayoutParams child_layout_params; |
| child_layout_params.hidden_by_owner = !view->visible(); |
| child_params_.emplace(view, child_layout_params); |
| internal_->InvalidateLayout(true); |
| } |
| |
| void FlexLayout::ViewRemoved(View* host, View* view) { |
| child_params_.erase(view); |
| internal_->InvalidateLayout(true); |
| } |
| |
| void FlexLayout::ViewVisibilitySet(View* host, View* view, bool visible) { |
| DCHECK_EQ(host, host_); |
| DCHECK_EQ(view->parent(), host); |
| auto it = child_params_.find(view); |
| DCHECK(it != child_params_.end()); |
| const bool hide = !visible; |
| if (it->second.hidden_by_owner != hide) { |
| it->second.hidden_by_owner = hide; |
| if (!it->second.excluded) { |
| // It's not always obvious to our host that an owner of a child view |
| // changing its visibility could change a layout. So we'll notify the host |
| // view that its layout is potentially invalid, and it will in turn call |
| // InvalidateLayout() on the layout manager (i.e. this object). |
| host_->InvalidateLayout(); |
| } |
| } |
| } |
| |
| // Retrieve the preferred size for the control in the given bounds. |
| gfx::Size FlexLayout::GetPreferredSize(const SizeBounds& bounds) const { |
| if (!host_) |
| return gfx::Size(); |
| |
| const gfx::Insets insets = host_->GetInsets(); |
| SizeBounds content_bounds = bounds; |
| content_bounds.Enlarge(-insets.width(), -insets.height()); |
| const internal::Layout& proposed_layout = |
| internal_->CalculateLayout(content_bounds); |
| gfx::Size result = Denormalize(orientation(), proposed_layout.total_size); |
| result.Enlarge(insets.width(), insets.height()); |
| return result; |
| } |
| |
| void FlexLayout::Layout(View* host) { |
| DCHECK_EQ(host, host_); |
| // TODO(dfried): for a handful of views which override GetInsets(), the way |
| // this method is implemented in View may be incorrect. Please revisit. |
| gfx::Rect bounds = host_->GetContentsBounds(); |
| const internal::Layout& layout = |
| internal_->CalculateLayout(SizeBounds(bounds.size())); |
| |
| internal_->DoLayout(layout, bounds); |
| } |
| |
| } // namespace views |