blob: a55819fb8763cbd07b4aa0439468101e0f84aab8 [file] [log] [blame]
// Copyright 2019 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/layout/flex_line.h"
#include <algorithm>
#include <list>
#include <vector>
#include "cobalt/cssom/css_computed_style_data.h"
#include "cobalt/cssom/keyword_value.h"
#include "cobalt/layout/used_style.h"
namespace cobalt {
namespace layout {
FlexLine::FlexLine(const LayoutParams& layout_params,
bool main_direction_is_horizontal,
bool direction_is_reversed, LayoutUnit main_size)
: layout_params_(layout_params),
main_direction_is_horizontal_(main_direction_is_horizontal),
direction_is_reversed_(direction_is_reversed),
main_size_(main_size) {
items_outer_main_size_ = LayoutUnit(0);
}
bool FlexLine::CanAddItem(const FlexItem& item) const {
LayoutUnit outer_main_size =
item.hypothetical_main_size() + item.GetContentToMarginMainAxis();
LayoutUnit next_main_size = items_outer_main_size_ + outer_main_size;
if (!items_.empty() && next_main_size > main_size_) {
return false;
}
return true;
}
void FlexLine::AddItem(std::unique_ptr<FlexItem>&& item) {
LayoutUnit outer_main_size =
item->hypothetical_main_size() + item->GetContentToMarginMainAxis();
items_outer_main_size_ += outer_main_size;
items_.emplace_back(std::move(item));
}
void FlexLine::ResolveFlexibleLengthsAndCrossSize() {
// Algorithm for resolving flexible lengths:
// https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths
// 1. Determine the used flex factor.
// If the sum is less than the flex container's inner main size, use the flex
// grow factor.
flex_factor_grow_ = items_outer_main_size_ < main_size_;
// 2. Size inflexible items.
// 3. Calculate initial free space.
LayoutUnit flex_space = LayoutUnit();
LayoutUnit frozen_space = LayoutUnit();
LayoutUnit scaled_flex_shrink_factor_sum = LayoutUnit();
// Items are removed from this container in random order.
std::list<FlexItem*> flexible_items;
for (auto& item : items_) {
item->DetermineFlexFactor(flex_factor_grow_);
// 2. Size inflexible items:
// - any item that has a flex factor of zero.
// - if using the flex grow factor: any item that has a flex base size
// greater than its hypothetical main size.
// - if using the flex shrink factor: any item that has a flex base size
// smaller than its hypothetical main size.
LayoutUnit hypothetical_main_size = item->hypothetical_main_size();
if (0 == item->flex_factor() ||
(flex_factor_grow_ &&
(item->flex_base_size() > hypothetical_main_size)) ||
(!flex_factor_grow_ &&
(item->flex_base_size() < hypothetical_main_size))) {
// Freeze, setting its target main size to its hypothetical main size.
base::Optional<LayoutUnit> content_based_minimum_size =
item->GetContentBasedMinimumSize(
layout_params_.containing_block_size);
if (content_based_minimum_size.has_value()) {
hypothetical_main_size =
std::max(hypothetical_main_size, *content_based_minimum_size);
}
item->set_target_main_size(hypothetical_main_size);
// 3. Calculate initial free space.
// For frozen items, use their outer target main size;
frozen_space +=
item->GetContentToMarginMainAxis() + item->target_main_size();
} else {
flexible_items.push_back(item.get());
// for other items, use their outer flex base size.
item->set_flex_space(item->GetContentToMarginMainAxis() +
item->flex_base_size());
item->set_target_main_size(item->hypothetical_main_size());
flex_space += item->flex_space();
// Precalculate the scaled flex shrink factor.
// If using the flex shrink factor, for every unfrozen item on the line,
if (!flex_factor_grow_) {
// multiply its flex shrink factor by its inner flex base size.
item->set_scaled_flex_shrink_factor(item->flex_base_size() *
item->flex_factor());
scaled_flex_shrink_factor_sum += item->scaled_flex_shrink_factor();
}
}
}
LayoutUnit initial_free_space = main_size_ - frozen_space - flex_space;
// 4. Loop
while (true) {
// a. Check for flexible items.
if (flexible_items.empty()) {
// If all the flex items on the line are frozen, free space has been
// distributed; exit this loop.
break;
}
// b. Calculate the remaining free space.
LayoutUnit remaining_free_space = main_size_ - frozen_space - flex_space;
// If the sum of the unfrozen flex items' flex factors ...
float unfrozen_flex_factor_sum = 0;
for (auto& item : flexible_items) {
unfrozen_flex_factor_sum += item->flex_factor();
}
// ... is less than one, multiply the initial free space by this sum.
if (unfrozen_flex_factor_sum < 1.0f) {
LayoutUnit free_space_magnitude =
initial_free_space * unfrozen_flex_factor_sum;
// If the magnitude of this value is less than the magnitude of the
// remaining free space, use this as the remaining free space.
if ((free_space_magnitude >= LayoutUnit() &&
free_space_magnitude < remaining_free_space) ||
(free_space_magnitude < LayoutUnit() &&
free_space_magnitude > remaining_free_space)) {
remaining_free_space = free_space_magnitude;
}
}
// c. Distribute free space proportional to the flex factors.
// If the remaining free space is zero, do nothing.
if (remaining_free_space != LayoutUnit()) {
if (flex_factor_grow_) {
// If using the flex grow factor.
for (auto& item : flexible_items) {
// Find the ratio of the item's flex grow factor to the sum of the
// flex grow factors of all unfrozen items on the line.
float ratio = item->flex_factor() / unfrozen_flex_factor_sum;
// Set the item's target main size to its flex base size plus a
// fraction of the remaining free space proportional to the ratio.
item->set_target_main_size(item->flex_base_size() +
remaining_free_space * ratio);
}
} else {
// If using the flex shrink factor,
for (auto& item : flexible_items) {
// Find the ratio of the item's scaled flex shrink factor to the sum
// of the scaled flex shrink factors.
float ratio = item->scaled_flex_shrink_factor().toFloat() /
scaled_flex_shrink_factor_sum.toFloat();
// Set the item's target main size to its flex base size minus a
// fraction of the absolute value of the remaining free space
// proportional to the ratio.
item->set_target_main_size(item->flex_base_size() +
remaining_free_space * ratio);
}
}
}
// d. Fix min/max violations.
// Clamp each non-frozen item's target main size by its used min and max
// main sizes. and floor its content-box size at zero.
LayoutUnit unclamped_size = LayoutUnit();
LayoutUnit clamped_size = LayoutUnit();
for (auto& item : flexible_items) {
base::Optional<LayoutUnit> maybe_used_min_space;
if (flex_factor_grow_) {
maybe_used_min_space = item->GetUsedMinMainAxisSizeIfNotAuto(
layout_params_.containing_block_size);
} else {
maybe_used_min_space = item->GetContentBasedMinimumSize(
layout_params_.containing_block_size);
}
base::Optional<LayoutUnit> used_max_space =
item->GetUsedMaxMainAxisSizeIfNotNone(
layout_params_.containing_block_size);
unclamped_size += item->target_main_size();
if (maybe_used_min_space &&
(item->target_main_size() < *maybe_used_min_space)) {
item->set_target_main_size(*maybe_used_min_space);
item->set_min_violation(true);
} else if (used_max_space && item->target_main_size() > *used_max_space) {
item->set_target_main_size(*used_max_space);
item->set_max_violation(true);
} else if (item->target_main_size() < LayoutUnit()) {
item->set_target_main_size(LayoutUnit());
item->set_min_violation(true);
}
clamped_size += item->target_main_size();
}
// e. Freeze over-flexed items.
// If the total violation is zero, freeze all items.
bool freeze_all = clamped_size == unclamped_size;
// If the total violation is positive, freeze all the items with min
// violations.
bool freeze_min_violations = clamped_size > unclamped_size;
// If the total violation is negative, freeze all the items with max
// violations.
bool freeze_max_violations = clamped_size < unclamped_size;
// Otherwise, do nothing.
if (!(freeze_all || freeze_min_violations || freeze_max_violations)) {
continue;
}
// Freeze the items by removing them from flexible_items and adding their
// outer main size to the frozen space.
for (auto item_iterator = flexible_items.begin();
item_iterator != flexible_items.end();) {
auto current_iterator = item_iterator++;
auto& item = *current_iterator;
if (freeze_all || (freeze_min_violations && item->min_violation()) ||
(freeze_max_violations && item->max_violation())) {
frozen_space +=
item->GetContentToMarginMainAxis() + item->target_main_size();
flex_space -= item->flex_space();
if (!flex_factor_grow_) {
scaled_flex_shrink_factor_sum -= item->scaled_flex_shrink_factor();
}
flexible_items.erase(current_iterator);
}
}
// f. return to the start of this loop.
}
items_outer_main_size_ = frozen_space;
// 5. Set each item's used main size to its target main size.
// https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths
// Also, algorithm for Flex Layout continued from step 7:
// https://www.w3.org/TR/css-flexbox-1/#layout-algorithm
// Cross Size Determination:
// 7. Determine the hypothetical cross size of each item
// By performing layout with the used main size and the available space.
for (auto& item : items_) {
item->DetermineHypotheticalCrossSize(layout_params_);
}
}
void FlexLine::CalculateCrossSize() {
// Algorithm for Calculate the cross size of each flex line.
// https://www.w3.org/TR/css-flexbox-1/#algo-cross-line
LayoutUnit max_baseline_to_bottom = LayoutUnit();
LayoutUnit max_hypothetical_cross_size = LayoutUnit();
for (auto& item : items_) {
// 1. Collect all the flex items whose inline-axis is parallel to the
// main-axis, whose align-self is baseline, and whose cross-axis margins
// are both non-auto.
if (main_direction_is_horizontal_ &&
item->GetUsedAlignSelfPropertyValue() ==
cssom::KeywordValue::GetBaseline() &&
!item->MarginCrossStartIsAuto() && !item->MarginCrossEndIsAuto()) {
// Find the largest of the distances between each item's baseline and its
// hypothetical outer cross-start edge,
LayoutUnit baseline_to_top =
item->box()->GetBaselineOffsetFromTopMarginEdge();
max_baseline_to_top_ = std::max(
max_baseline_to_top_.value_or(LayoutUnit()), baseline_to_top);
// and the largest of the distances between each item's baseline and its
// hypothetical outer cross-end edge,
LayoutUnit baseline_to_bottom = item->box()->height() - baseline_to_top;
max_baseline_to_bottom =
std::max(max_baseline_to_bottom, baseline_to_bottom);
} else {
// 2. Among all the items not collected by the previous step, find the
// largest outer hypothetical cross size.
LayoutUnit hypothetical_cross_size = item->GetMarginBoxCrossSize();
if (hypothetical_cross_size > max_hypothetical_cross_size) {
max_hypothetical_cross_size = hypothetical_cross_size;
}
}
}
// ... and sum these two values.
LayoutUnit max_baseline_cross_size =
max_baseline_to_top_.value_or(LayoutUnit()) + max_baseline_to_bottom;
// 3. The used cross-size of the flex line is the largest of the numbers
// found in the previous two steps and zero.
cross_size_ = std::max(max_hypothetical_cross_size, max_baseline_cross_size);
}
void FlexLine::DetermineUsedCrossSizes(LayoutUnit container_cross_size) {
// 11. Determine the used cross size of each flex item->
// https://www.w3.org/TR/css-flexbox-1/#algo-stretch
SizeLayoutUnit containing_block_size(LayoutUnit(), container_cross_size);
for (auto& item : items_) {
// If a flex item has align-self: stretch,
// its computed cross size property is auto,
// and neither of its cross-axis margins are auto,
if (item->GetUsedAlignSelfPropertyValue() ==
cssom::KeywordValue::GetStretch() &&
item->CrossSizeIsAuto() && !item->MarginCrossStartIsAuto() &&
!item->MarginCrossEndIsAuto()) {
// The used outer cross size is the used cross size of its flex line,
// clamped according to the item's used min and max cross sizes.
LayoutUnit cross_size = cross_size_ - item->GetContentToMarginCrossAxis();
base::Optional<LayoutUnit> min_cross_size =
item->GetUsedMinCrossAxisSizeIfNotAuto(containing_block_size);
if (min_cross_size && (*min_cross_size > cross_size)) {
cross_size = *min_cross_size;
}
base::Optional<LayoutUnit> max_cross_size =
item->GetUsedMaxCrossAxisSizeIfNotNone(containing_block_size);
if (max_cross_size && *max_cross_size < cross_size) {
cross_size = *max_cross_size;
}
item->SetCrossSize(cross_size);
// TODO: If the flex item has align-self: stretch, redo layout for its
// contents, treating this used size as its definite cross size so that
// percentage-sized children can be resolved.
}
}
}
void FlexLine::SetMainAxisPosition(LayoutUnit pos, FlexItem* item) {
if (direction_is_reversed_) {
item->SetMainAxisStart(main_size_ - pos - item->GetMarginBoxMainSize());
} else {
item->SetMainAxisStart(pos);
}
}
void FlexLine::DoMainAxisAlignment() {
// Algorithm for main axis alignment:
// https://www.w3.org/TR/css-flexbox-1/#main-alignment
// 12. Distribute any remaining free space.
// https://www.w3.org/TR/css-flexbox-1/#algo-main-align
// If the remaining free space is positive and at least one main-axis margin
// on this line is auto, distribute the free space equally among these
// margins.
std::vector<bool> auto_margins(items_.size() * 2);
int auto_margin_count = 0;
int margin_idx = 0;
for (auto& item : items_) {
bool auto_main_start = item->MarginMainStartIsAuto();
bool auto_main_end = item->MarginMainEndIsAuto();
auto_margins[margin_idx++] = auto_main_start;
auto_margins[margin_idx++] = auto_main_end;
auto_margin_count += (auto_main_start ? 1 : 0) + (auto_main_end ? 1 : 0);
}
LayoutUnit leftover_free_space = main_size_ - items_outer_main_size_;
if (auto_margin_count > 0) {
LayoutUnit free_space_between = leftover_free_space / auto_margin_count;
margin_idx = 0;
LayoutUnit pos = LayoutUnit();
for (auto& item : items_) {
pos += auto_margins[margin_idx++] ? free_space_between : LayoutUnit();
LayoutUnit main_size = item->GetMarginBoxMainSize();
SetMainAxisPosition(pos, item.get());
pos += main_size;
pos += auto_margins[margin_idx++] ? free_space_between : LayoutUnit();
}
return;
}
DCHECK(!items_.empty());
// Align the items along the main-axis per justify-content.
const scoped_refptr<cobalt::cssom::PropertyValue>& justify_content =
items_.front()->GetUsedJustifyContentPropertyValue();
bool leftover_free_space_is_negative = leftover_free_space < LayoutUnit();
if (justify_content == cssom::KeywordValue::GetFlexStart() ||
(leftover_free_space_is_negative &&
justify_content == cssom::KeywordValue::GetSpaceBetween())) {
// Flex items are packed toward the start of the line.
// https://www.w3.org/TR/css-flexbox-1/#valdef-justify-content-flex-start
// If the leftover free-space is negative, space-between is identical to
// flex-start.
// https://www.w3.org/TR/css-flexbox-1/#valdef-justify-content-space-between
LayoutUnit pos = LayoutUnit();
for (auto& item : items_) {
LayoutUnit main_size = item->GetMarginBoxMainSize();
SetMainAxisPosition(pos, item.get());
pos += main_size;
}
} else if (justify_content == cssom::KeywordValue::GetFlexEnd()) {
// Flex items are packed toward the end of the line.
// https://www.w3.org/TR/css-flexbox-1/#valdef-justify-content-flex-end
LayoutUnit pos = main_size_;
for (auto item_iterator = items_.rbegin(); item_iterator != items_.rend();
++item_iterator) {
auto& item = *item_iterator;
LayoutUnit main_size = item->GetMarginBoxMainSize();
pos -= main_size;
SetMainAxisPosition(pos, item.get());
}
} else if (justify_content == cssom::KeywordValue::GetCenter() ||
(leftover_free_space_is_negative &&
justify_content == cssom::KeywordValue::GetSpaceAround())) {
// Flex items are packed toward the center of the line.
// https://www.w3.org/TR/css-flexbox-1/#valdef-justify-content-center
// If the leftover free-space is negative, space-around is identical to
// center.
// https://www.w3.org/TR/css-flexbox-1/#valdef-justify-content-space-around
LayoutUnit pos = leftover_free_space / 2;
for (auto& item : items_) {
LayoutUnit main_size = item->GetMarginBoxMainSize();
SetMainAxisPosition(pos, item.get());
pos += main_size;
}
} else if (justify_content == cssom::KeywordValue::GetSpaceBetween()) {
// Flex items are evenly distributed in the line.
// https://www.w3.org/TR/css-flexbox-1/#valdef-justify-content-space-between
LayoutUnit free_space_between =
(items_.size() < 2)
? LayoutUnit()
: leftover_free_space / static_cast<int>(items_.size() - 1);
LayoutUnit pos = LayoutUnit();
for (auto& item : items_) {
LayoutUnit main_size = item->GetMarginBoxMainSize();
SetMainAxisPosition(pos, item.get());
pos += main_size + free_space_between;
}
} else if (justify_content == cssom::KeywordValue::GetSpaceAround()) {
// Flex items are evenly distributed in the line, with half-size spaces on
// either end.
// https://www.w3.org/TR/css-flexbox-1/#valdef-justify-content-space-around
LayoutUnit free_space_between =
leftover_free_space / static_cast<int>(items_.size());
LayoutUnit free_space_before = free_space_between / 2;
LayoutUnit pos = LayoutUnit();
for (auto& item : items_) {
LayoutUnit main_size = item->GetMarginBoxMainSize();
SetMainAxisPosition(pos + free_space_before, item.get());
pos += main_size + free_space_between;
}
} else {
// Added as sanity check for unsupported values.
NOTREACHED();
}
}
void FlexLine::DoCrossAxisAlignment(LayoutUnit line_cross_axis_start) {
// Algorithm for cross axis alignment:
// https://www.w3.org/TR/css-flexbox-1/#cross-alignment
// 13. Resolve cross-axis auto margins.
// 14. Align all flex items along the cross-axis per align-self.
for (auto& item : items_) {
LayoutUnit cross_axis_start = LayoutUnit();
bool auto_margin_cross_start = item->MarginCrossStartIsAuto();
bool auto_margin_cross_end = item->MarginCrossEndIsAuto();
LayoutUnit cross_size = item->GetMarginBoxCrossSize();
if (auto_margin_cross_start || auto_margin_cross_end) {
if (auto_margin_cross_start) {
cross_axis_start =
(cross_size_ - cross_size) / (auto_margin_cross_end ? 2 : 1);
}
} else {
const auto& align_self = item->GetUsedAlignSelfPropertyValue();
// Only flex-end, center, and baseline can result in a cross_axis_start
// that is not aligned to the line cross start edge.
if (align_self == cssom::KeywordValue::GetFlexEnd()) {
cross_axis_start = cross_size_ - cross_size;
} else if (align_self == cssom::KeywordValue::GetCenter()) {
cross_axis_start = (cross_size_ - cross_size) / 2;
} else if (align_self == cssom::KeywordValue::GetBaseline()) {
if (main_direction_is_horizontal_ && max_baseline_to_top_.has_value()) {
LayoutUnit baseline_to_top =
item->box()->GetBaselineOffsetFromTopMarginEdge();
cross_axis_start = *max_baseline_to_top_ - baseline_to_top;
}
} else {
DCHECK((align_self == cssom::KeywordValue::GetFlexStart()) ||
(align_self == cssom::KeywordValue::GetStretch()));
}
}
item->SetCrossAxisStart(line_cross_axis_start + cross_axis_start);
}
}
LayoutUnit FlexLine::GetBaseline() {
if (max_baseline_to_top_.has_value()) {
return *max_baseline_to_top_;
}
LayoutUnit baseline = cross_size_;
if (!items_.empty()) {
Box* box = items_.front()->box();
baseline = box->top() + box->GetBaselineOffsetFromTopMarginEdge();
}
return baseline;
}
} // namespace layout
} // namespace cobalt