1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215
|
// 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 "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 (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,
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
|