blob: 92940d18b524fe89253e7bfe8493a7dfaf67f89b [file] [log] [blame]
/*
* Copyright 2015 Google Inc. 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/line_box.h"
#include <algorithm>
#include <limits>
#include "cobalt/base/math.h"
#include "cobalt/cssom/keyword_value.h"
#include "cobalt/layout/box.h"
#include "cobalt/layout/used_style.h"
namespace cobalt {
namespace layout {
// The left edge of a line box touches the left edge of its containing block.
// https://www.w3.org/TR/CSS21/visuren.html#inline-formatting
LineBox::LineBox(LayoutUnit top, bool position_children_relative_to_baseline,
const scoped_refptr<cssom::PropertyValue>& line_height,
const render_tree::FontMetrics& font_metrics,
bool should_collapse_leading_white_space,
bool should_collapse_trailing_white_space,
const LayoutParams& layout_params,
BaseDirection base_direction,
const scoped_refptr<cssom::PropertyValue>& text_align,
const scoped_refptr<cssom::PropertyValue>& font_size,
LayoutUnit indent_offset, LayoutUnit ellipsis_width)
: top_(top),
position_children_relative_to_baseline_(
position_children_relative_to_baseline),
line_height_(line_height),
font_metrics_(font_metrics),
should_collapse_leading_white_space_(should_collapse_leading_white_space),
should_collapse_trailing_white_space_(
should_collapse_trailing_white_space),
layout_params_(layout_params),
base_direction_(base_direction),
text_align_(text_align),
font_size_(font_size),
indent_offset_(indent_offset),
ellipsis_width_(ellipsis_width),
has_overflowed_(false),
at_end_(false),
num_absolutely_positioned_boxes_before_first_box_justifying_line_(0),
shrink_to_fit_width_(indent_offset_),
height_(0),
baseline_offset_from_top_(0),
is_ellipsis_placed_(false),
placed_ellipsis_offset_(0) {}
Box* LineBox::TryAddChildAndMaybeWrap(Box* child_box) {
DCHECK(!at_end_);
if (child_box->IsAbsolutelyPositioned()) {
BeginEstimateStaticPositionForAbsolutelyPositionedChild(child_box);
return NULL;
}
UpdateSizePreservingTrailingWhiteSpace(child_box);
// If the line box hasn't already overflowed the line, then attempt to add it
// within the available width.
if (!has_overflowed_ && !TryAddChildWithinAvailableWidth(child_box)) {
// If the attempt failed, then adding the full box would overflow the line.
// Attempt to find a wrap location within the available width, which will
// prevent overflow from occurring. The priority is as follows:
// 1. Attempt to find the last normal wrap opportunity in the current child
// within the available width.
// 2. Attempt to find the last normal wrap opportunity within the previously
// added children.
// 3. Attempt to find the last break-word wrap opportunity position in the
// current child within the available width. This will only be attempted
// when the overflow-wrap style of the box is break-word.
// 4. Attempt to find the last break-word wrap opportunity within the
// previously added children. This will only be attempted when the
// overflow-wrap style of the box is break-word.
// https://www.w3.org/TR/css-text-3/#line-breaking
// https://www.w3.org/TR/css-text-3/#overflow-wrap
if (TryWrapOverflowingBoxAndMaybeAddSplitChild(
kWrapAtPolicyLastOpportunityWithinWidth,
kWrapOpportunityPolicyNormal, child_box) ||
TryWrapChildrenAtLastOpportunity(kWrapOpportunityPolicyNormal) ||
TryWrapOverflowingBoxAndMaybeAddSplitChild(
kWrapAtPolicyLastOpportunityWithinWidth,
kWrapOpportunityPolicyBreakWord, child_box) ||
TryWrapChildrenAtLastOpportunity(kWrapOpportunityPolicyBreakWord)) {
// A wrap position was successfully found within the width. The line is
// wrapping and at its end.
at_end_ = true;
} else {
// If an inline box cannot be split (e.g., if the inline box contains
// a single character, or language specific word breaking rules disallow
// a break within the inline box), then the inline box overflows the line
// box.
// https://www.w3.org/TR/CSS21/visuren.html#inline-formatting
has_overflowed_ = true;
}
}
if (has_overflowed_) {
// If the line has overflowed, then the first wrap opportunity within the
// child box is preferred, thereby minimizing the size of the overflow. This
// can be either a break-word or normal wrap, depending on the overflow-wrap
// style of the box.
// https://www.w3.org/TR/css-text-3/#overflow-wrap
if (TryWrapOverflowingBoxAndMaybeAddSplitChild(
kWrapAtPolicyFirstOpportunity,
kWrapOpportunityPolicyBreakWordOrNormal, child_box)) {
// A wrap position was successfully found. The line is wrapping and at its
// end.
at_end_ = true;
} else {
// No wrap position was found within the child box. The box is allowed to
// overflow the line and additional boxes can be added until a wrappable
// box is found.
BeginAddChildInternal(child_box);
}
}
DCHECK(!child_boxes_.empty());
return at_end_ ? child_boxes_[child_boxes_.size() - 1] : NULL;
}
void LineBox::BeginAddChildAndMaybeOverflow(Box* child_box) {
if (child_box->IsAbsolutelyPositioned()) {
BeginEstimateStaticPositionForAbsolutelyPositionedChild(child_box);
return;
}
UpdateSizePreservingTrailingWhiteSpace(child_box);
BeginAddChildInternal(child_box);
}
void LineBox::EndUpdates() {
at_end_ = true;
// A sequence of collapsible spaces at the end of a line is removed.
// https://www.w3.org/TR/css3-text/#white-space-phase-2
if (should_collapse_trailing_white_space_) {
CollapseTrailingWhiteSpace();
}
// Set the leading and trailing white space flags now. This ensures that the
// values returned by HasLeadingWhiteSpace() and HasTrailingWhiteSpace()
// remain valid even after bidi reversals.
has_leading_white_space_ = HasLeadingWhiteSpace();
has_trailing_white_space_ = HasTrailingWhiteSpace();
ReverseChildBoxesByBidiLevels();
UpdateChildBoxLeftPositions();
SetLineBoxHeightFromChildBoxes();
UpdateChildBoxTopPositions();
MaybePlaceEllipsis();
}
bool LineBox::HasLeadingWhiteSpace() const {
// |has_leading_white_space_| should only ever be set by EndUpdates() after
// |at_end_| has been set to true;
DCHECK(at_end_ || !has_leading_white_space_);
// If |has_leading_white_space_| has been set, then use it. Otherwise, grab
// the leading white space state from the first non-collapsed child box.
return has_leading_white_space_
? *has_leading_white_space_
: first_non_collapsed_child_box_index_ &&
child_boxes_[*first_non_collapsed_child_box_index_]
->HasLeadingWhiteSpace();
}
bool LineBox::HasTrailingWhiteSpace() const {
// |has_trailing_white_space_| should only ever be set by EndUpdates() after
// |at_end_| has been set to true;
DCHECK(at_end_ || !has_trailing_white_space_);
// If |has_trailing_white_space_| has been set, then use it. Otherwise, grab
// the trailing white space state from the last non-collapsed child box.
return has_trailing_white_space_
? *has_trailing_white_space_
: last_non_collapsed_child_box_index_ &&
child_boxes_[*last_non_collapsed_child_box_index_]
->HasTrailingWhiteSpace();
}
bool LineBox::IsCollapsed() const {
return !first_non_collapsed_child_box_index_;
}
bool LineBox::LineExists() const {
return !!first_box_justifying_line_existence_index_;
}
size_t LineBox::GetFirstBoxJustifyingLineExistenceIndex() const {
return first_box_justifying_line_existence_index_.value_or(
child_boxes_.size()) +
num_absolutely_positioned_boxes_before_first_box_justifying_line_;
}
bool LineBox::IsEllipsisPlaced() const { return is_ellipsis_placed_; }
math::Vector2dF LineBox::GetEllipsisCoordinates() const {
return math::Vector2dF(placed_ellipsis_offset_.toFloat(),
(top_ + baseline_offset_from_top_).toFloat());
}
LayoutUnit LineBox::GetAvailableWidth() const {
return layout_params_.containing_block_size.width() - shrink_to_fit_width_;
}
void LineBox::UpdateSizePreservingTrailingWhiteSpace(Box* child_box) {
child_box->SetShouldCollapseLeadingWhiteSpace(
ShouldCollapseLeadingWhiteSpaceInNextChildBox());
child_box->SetShouldCollapseTrailingWhiteSpace(false);
child_box->UpdateSize(layout_params_);
}
bool LineBox::ShouldCollapseLeadingWhiteSpaceInNextChildBox() const {
return last_non_collapsed_child_box_index_
// Any space immediately following another collapsible space - even
// one outside the boundary of the inline containing that space,
// provided they are both within the same inline formatting context
// - is collapsed.
// https://www.w3.org/TR/css3-text/#white-space-phase-1
? child_boxes_[*last_non_collapsed_child_box_index_]
->HasTrailingWhiteSpace()
// A sequence of collapsible spaces at the beginning of a line is
// removed.
// https://www.w3.org/TR/css3-text/#white-space-phase-2
: should_collapse_leading_white_space_;
}
void LineBox::CollapseTrailingWhiteSpace() {
if (!HasTrailingWhiteSpace()) {
return;
}
// A white space between child boxes is already collapsed as a result
// of calling |UpdateSizePreservingTrailingWhiteSpace|. Collapse the
// trailing white space in the last non-collapsed child box (all fully
// collapsed child boxes at the end of the line are treated as
// non-existent for the purposes of collapsing).
Box* last_non_collapsed_child_box =
child_boxes_[*last_non_collapsed_child_box_index_];
LayoutUnit child_box_pre_collapse_width =
last_non_collapsed_child_box->width();
last_non_collapsed_child_box->SetShouldCollapseTrailingWhiteSpace(true);
last_non_collapsed_child_box->UpdateSize(layout_params_);
LayoutUnit collapsed_white_space_width =
child_box_pre_collapse_width - last_non_collapsed_child_box->width();
DCHECK(collapsed_white_space_width.GreaterThanOrNaN(LayoutUnit()));
shrink_to_fit_width_ -= collapsed_white_space_width;
}
void LineBox::RestoreTrailingWhiteSpace() {
Box* last_non_collapsed_child_box =
child_boxes_[*last_non_collapsed_child_box_index_];
LayoutUnit child_box_pre_restore_width =
last_non_collapsed_child_box->width();
last_non_collapsed_child_box->SetShouldCollapseTrailingWhiteSpace(false);
last_non_collapsed_child_box->UpdateSize(layout_params_);
LayoutUnit restored_white_space_width =
last_non_collapsed_child_box->width() - child_box_pre_restore_width;
DCHECK(restored_white_space_width.GreaterThanOrNaN(LayoutUnit()));
shrink_to_fit_width_ += restored_white_space_width;
}
bool LineBox::TryAddChildWithinAvailableWidth(Box* child_box) {
// Horizontal margins, borders, and padding are respected between boxes.
// https://www.w3.org/TR/CSS21/visuren.html#inline-formatting
// If the box fits within the available width, simply add it. Nothing more
// needs to be done.
if (child_box->GetMarginBoxWidth() <= GetAvailableWidth()) {
BeginAddChildInternal(child_box);
return true;
}
// Otherwise, the box currently does not fit, but if there is trailing
// whitespace that can be collapsed, then one more attempt must be made to fit
// the box within the available width.
if (should_collapse_trailing_white_space_ &&
(child_box->HasTrailingWhiteSpace() ||
(child_box->IsCollapsed() && HasTrailingWhiteSpace()))) {
bool child_has_trailing_white_space = child_box->HasTrailingWhiteSpace();
bool child_fits_after_collapsing_trailing_whitespace = false;
// A sequence of collapsible spaces at the end of a line is removed.
// https://www.w3.org/TR/css3-text/#white-space-phase-2
if (child_has_trailing_white_space) {
child_box->SetShouldCollapseTrailingWhiteSpace(true);
child_box->UpdateSize(layout_params_);
} else {
CollapseTrailingWhiteSpace();
}
// Check to see if the box now fits, as the white space collapsing may have
// freed up enough space for it.
if (child_box->GetMarginBoxWidth() <= GetAvailableWidth()) {
child_fits_after_collapsing_trailing_whitespace = true;
}
// Restore the collapsed trailing whitespace now that the space check is
// complete.
if (child_has_trailing_white_space) {
child_box->SetShouldCollapseTrailingWhiteSpace(false);
child_box->UpdateSize(layout_params_);
} else {
RestoreTrailingWhiteSpace();
}
// If there is enough space to add the child without overflowing the line,
// add it now. This does not end the line, as more boxes may be able to fit
// as well.
if (child_fits_after_collapsing_trailing_whitespace) {
BeginAddChildInternal(child_box);
return true;
}
}
// The child did not fit within the available width.
return false;
}
bool LineBox::TryWrapOverflowingBoxAndMaybeAddSplitChild(
WrapAtPolicy wrap_at_policy, WrapOpportunityPolicy wrap_opportunity_policy,
Box* child_box) {
// If none of the children justify the line's existence, then wrapping is
// unavailable. The wrap can't happen before the first child justifying the
// line.
if (!first_box_justifying_line_existence_index_ &&
!child_box->JustifiesLineExistence()) {
return false;
}
// Attempt to wrap the child based upon the passed in wrap policy.
WrapResult wrap_result = child_box->TryWrapAt(
wrap_at_policy, wrap_opportunity_policy, LineExists(),
GetAvailableWidth(), should_collapse_trailing_white_space_);
// If the wrap is occurring before the box, then simply return that a wrap
// occurred. This box is not being included within this line and does not need
// to be added. The line ends with the box before this one.
if (wrap_result == kWrapResultWrapBefore) {
return true;
// Otherwise, if a split wrap occurred, then the wrap location was found
// within the box and the box was split. The first part of the box needs to
// be added to the line. It is the last box included on this line. The
// second part of the box will be the first box included on the next line.
} else if (wrap_result == kWrapResultSplitWrap) {
// The portion of the child box being added needs to be re-measured prior to
// being added because the split invalidated its size.
UpdateSizePreservingTrailingWhiteSpace(child_box);
BeginAddChildInternal(child_box);
// TODO: Enable this logic.
// if (wrap_opportunity_policy == kWrapAtPolicyLastOpportunityWithinWidth) {
// If trailing white space is being collapsed, then the child box can
// exceed the available width prior to white space being collapsed. So the
// DCHECK is only valid if the box's whitespace is collapsed prior to it.
// if (should_collapse_trailing_white_space_) {
// CollapseTrailingWhiteSpace();
// }
// DCHECK(child_box->GetMarginBoxWidth() <= available_width);
// }
return true;
// Otherwise, no wrap location was found within the box.
} else {
return false;
}
}
bool LineBox::TryWrapChildrenAtLastOpportunity(
WrapOpportunityPolicy wrap_opportunity_policy) {
// If none of the children justify the line's existence, then wrapping is
// unavailable. The wrap can't happen before the first child justifying the
// line.
if (!first_box_justifying_line_existence_index_) {
return false;
}
LayoutUnit total_wrap_width;
// Walk the children in reverse order, since the last available wrap is
// preferred over earlier ones. However, do not attempt any children preceding
// the line justification index, as they are guaranteed to not be wrappable.
size_t wrap_index = child_boxes_.size();
size_t line_justification_index = *first_box_justifying_line_existence_index_;
while (wrap_index > line_justification_index) {
--wrap_index;
Box* child_box = child_boxes_[wrap_index];
total_wrap_width += child_box->GetMarginBoxWidth();
// Check to see if the line existence is already justified prior to this
// box. This will be the case if this isn't first box justifying the
// line's existence. If this is the first box justifying the line's
// existence, then justification occurs somewhere within this box.
bool is_line_existence_already_justified =
wrap_index != line_justification_index;
// Attempt to wrap within the child. Width is not taken into account, as
// the last wrappable location is always preferred, regardless of width.
WrapResult wrap_result = child_box->TryWrapAt(
kWrapAtPolicyLastOpportunity, wrap_opportunity_policy,
is_line_existence_already_justified, LayoutUnit(), false);
if (wrap_result != kWrapResultNoWrap) {
// If a wrap was successfully found, then the line needs to be updated to
// reflect that some of the previously added children are no longer being
// fully included on the line.
// Remove the wrap box and all subsequent boxes from the children, and
// subtract their width from the line. In the case where this is a split
// wrap, the portion of the split box being retained on the line will be
// re-added after its width is recalculated below.
child_boxes_.resize(wrap_index);
shrink_to_fit_width_ -= total_wrap_width;
// Update the non-collapsed indices to account for the boxes removed from
// the line.
if (first_non_collapsed_child_box_index_) {
if (*first_non_collapsed_child_box_index_ >= wrap_index) {
first_non_collapsed_child_box_index_ = base::nullopt;
last_non_collapsed_child_box_index_ = base::nullopt;
} else if (*last_non_collapsed_child_box_index_ >= wrap_index) {
last_non_collapsed_child_box_index_ =
first_non_collapsed_child_box_index_;
size_t check_index = wrap_index;
size_t last_check_index = *last_non_collapsed_child_box_index_ + 1;
while (check_index > last_check_index) {
--check_index;
if (!child_boxes_[check_index]->IsCollapsed()) {
last_non_collapsed_child_box_index_ = check_index;
break;
}
}
}
}
if (wrap_result == kWrapResultSplitWrap) {
// If a split occurs, then the portion of the child box being added
// needs to be re-measured prior to being added, as the split
// invalidated the box's size.
UpdateSizePreservingTrailingWhiteSpace(child_box);
BeginAddChildInternal(child_box);
}
return true;
}
}
return false;
}
void LineBox::BeginEstimateStaticPositionForAbsolutelyPositionedChild(
Box* child_box) {
if (!first_box_justifying_line_existence_index_) {
++num_absolutely_positioned_boxes_before_first_box_justifying_line_;
}
// The term "static position" (of an element) refers, roughly, to the position
// an element would have had in the normal flow. More precisely:
//
// The static-position containing block is the containing block of a
// hypothetical box that would have been the first box of the element if its
// specified 'position' value had been 'static'.
//
// The static position for 'left' is the distance from the left edge of the
// containing block to the left margin edge of a hypothetical box that would
// have been the first box of the element if its 'position' property had been
// 'static' and 'float' had been 'none'. The value is negative if the
// hypothetical box is to the left of the containing block.
// https://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width
// For the purposes of this section and the next, the term "static position"
// (of an element) refers, roughly, to the position an element would have had
// in the normal flow. More precisely, the static position for 'top' is the
// distance from the top edge of the containing block to the top margin edge
// of a hypothetical box that would have been the first box of the element if
// its specified 'position' value had been 'static'.
// https://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-height
switch (child_box->GetLevel()) {
case Box::kInlineLevel:
// NOTE: This case is never reached due to a bug.
child_box->SetStaticPositionLeftFromParent(shrink_to_fit_width_);
break;
case Box::kBlockLevel:
child_box->SetStaticPositionLeftFromParent(LayoutUnit());
break;
default:
NOTREACHED();
break;
}
child_box->SetStaticPositionTopFromParent(LayoutUnit());
}
void LineBox::BeginAddChildInternal(Box* child_box) {
if (!first_box_justifying_line_existence_index_ &&
child_box->JustifiesLineExistence()) {
first_box_justifying_line_existence_index_ = child_boxes_.size();
}
if (!child_box->IsCollapsed()) {
if (!first_non_collapsed_child_box_index_) {
first_non_collapsed_child_box_index_ = child_boxes_.size();
}
last_non_collapsed_child_box_index_ = child_boxes_.size();
}
// If this child has a trailing line break, then we've reached the end of this
// line. Nothing more can be added to it.
if (child_box->HasTrailingLineBreak()) {
at_end_ = true;
}
// Horizontal margins, borders, and padding are respected between boxes.
// https://www.w3.org/TR/CSS21/visuren.html#inline-formatting
shrink_to_fit_width_ += child_box->GetMarginBoxWidth();
child_boxes_.push_back(child_box);
}
void LineBox::ReverseChildBoxesByBidiLevels() {
// From the highest level found in the text to the lowest odd level on each
// line, including intermediate levels not actually present in the text,
// reverse any contiguous sequence of characters that are at that level or
// higher.
// http://unicode.org/reports/tr9/#L2
const int kInvalidLevel = -1;
int max_level = 0;
int min_level = std::numeric_limits<int>::max();
for (ChildBoxes::const_iterator child_box_iterator = child_boxes_.begin();
child_box_iterator != child_boxes_.end(); ++child_box_iterator) {
Box* child_box = *child_box_iterator;
int child_level = child_box->GetBidiLevel().value_or(kInvalidLevel);
if (child_level != kInvalidLevel) {
if (child_level > max_level) {
max_level = child_level;
}
if (child_level < min_level) {
min_level = child_level;
}
}
}
// Reversals only occur down to the lowest odd level.
if (min_level % 2 == 0) {
min_level += 1;
}
for (int i = max_level; i >= min_level; --i) {
ReverseChildBoxesMeetingBidiLevelThreshold(i);
}
}
void LineBox::ReverseChildBoxesMeetingBidiLevelThreshold(int level) {
// Walk all of the boxes in the line, looking for runs of boxes that have a
// bidi level greater than or equal to the passed in level. Every run of two
// or more boxes is reversed.
int run_count = 0;
ChildBoxes::iterator run_start;
int child_level = 0;
for (ChildBoxes::iterator child_box_iterator = child_boxes_.begin();
child_box_iterator != child_boxes_.end();) {
ChildBoxes::iterator current_iterator = child_box_iterator++;
Box* child_box = *current_iterator;
child_level = child_box->GetBidiLevel().value_or(child_level);
// The child's level is greater than or equal to the required level, so it
// qualifies for reversal.
if (child_level >= level) {
if (run_count == 0) {
run_start = current_iterator;
}
++run_count;
// The child's level didn't qualify it for reversal. If there was an
// active run, it has ended, so reverse it.
} else {
if (run_count > 1) {
std::reverse(run_start, current_iterator);
}
run_count = 0;
}
}
// A qualifying run was found that ran through the end of the children.
// Reverse it.
if (run_count > 1) {
std::reverse(run_start, child_boxes_.end());
}
}
void LineBox::UpdateChildBoxLeftPositions() {
LayoutUnit horizontal_offset;
// Determine the horizontal offset to apply to the child boxes from the
// horizontal alignment.
HorizontalAlignment horizontal_alignment = ComputeHorizontalAlignment();
switch (horizontal_alignment) {
case kLeftHorizontalAlignment:
// The horizontal alignment is to the left, so no offset needs to be
// applied based on the alignment. This places all of the available space
// to the right of the boxes.
break;
case kCenterHorizontalAlignment:
// The horizontal alignment is to the center, so offset by half of the
// available width. This places half of the available width on each side
// of the boxes.
horizontal_offset = GetAvailableWidth() / 2;
break;
case kRightHorizontalAlignment:
// The horizontal alignment is to the right, so offset by the full
// available width. This places all of the available space to the left of
// the boxes.
horizontal_offset = GetAvailableWidth();
break;
}
// Determine the horizontal offset to add to the child boxes from the indent
// offset (https://www.w3.org/TR/CSS21/text.html#propdef-text-indent), which
// is treated as a margin applied to the start edge of the line box. In the
// case where the start edge is on the right, there is no offset to add, as
// it was already included from the GetAvailableWidth() logic above.
//
// To add to this, the indent offset was added to |shrink_to_fit_width_| when
// the line box was created. The above logic serves to subtract half of the
// indent offset when the alignment is centered, and the full indent offset
// when the alignment is to the right. Re-adding the indent offset in the case
// where the base direction is LTR causes the indent to shift the boxes to the
// right. Not adding it in the case where the base direction is RTL causes the
// indent to shift the boxes to the left.
//
// Here are the 6 cases and the final indent offset they produce:
// Left Align + LTR => indent_offset
// Center Align + LTR => indent_offset / 2
// Right Align + LTR => 0
// Left Align + RTL => 0
// Center Align + RTL => -indent_offset / 2
// Right Align + RTL => -indent_offset
if (base_direction_ != kRightToLeftBaseDirection) {
horizontal_offset += indent_offset_;
}
// Set the first child box left position to the horizontal offset. This
// results in all boxes being shifted by that offset.
LayoutUnit child_box_left(horizontal_offset);
for (ChildBoxes::const_iterator child_box_iterator = child_boxes_.begin();
child_box_iterator != child_boxes_.end(); ++child_box_iterator) {
Box* child_box = *child_box_iterator;
child_box->set_left(child_box_left);
child_box_left =
child_box->GetMarginBoxRightEdgeOffsetFromContainingBlock();
}
}
// Loops over the child boxes and sets the |baseline_offset_from_top_|
// and |height_| such that all child boxes fit.
void LineBox::SetLineBoxHeightFromChildBoxes() {
// The minimum height consists of a minimum height above the baseline and
// a minimum depth below it, exactly as if each line box starts with
// a zero-width inline box with the element's font and line height properties.
// We call that imaginary box a "strut."
// https://www.w3.org/TR/CSS21/visudet.html#strut
UsedLineHeightProvider used_line_height_provider(font_metrics_, font_size_);
line_height_->Accept(&used_line_height_provider);
baseline_offset_from_top_ =
used_line_height_provider.baseline_offset_from_top();
LayoutUnit baseline_offset_from_bottom =
used_line_height_provider.baseline_offset_from_bottom();
LayoutUnit max_top_aligned_height;
LayoutUnit max_bottom_aligned_height;
// During this loop, the line box height above and below the baseline is
// established.
for (ChildBoxes::const_iterator child_box_iterator = child_boxes_.begin();
child_box_iterator != child_boxes_.end(); ++child_box_iterator) {
Box* child_box = *child_box_iterator;
// The child box influence on the line box depends on the vertical-align
// property.
// https://www.w3.org/TR/CSS21/visudet.html#propdef-vertical-align
const scoped_refptr<cssom::PropertyValue>& vertical_align =
child_box->computed_style()->vertical_align();
LayoutUnit baseline_offset_from_child_top_margin_edge;
bool update_height = false;
if (vertical_align == cssom::KeywordValue::GetMiddle()) {
// Align the vertical midpoint of the box with the baseline of the parent
// box plus half the x-height (height of the 'x' glyph) of the parent.
baseline_offset_from_child_top_margin_edge =
GetHeightAboveMiddleAlignmentPoint(child_box);
update_height = true;
} else if (vertical_align == cssom::KeywordValue::GetTop()) {
// Align the top of the aligned subtree with the top of the line box.
// That means it will never affect the height above the baseline, but it
// may affect the height below the baseline if this is the tallest child
// box. We measure the tallest top-aligned box to implement that after
// this loop.
LayoutUnit child_height = child_box->GetInlineLevelBoxHeight();
// If there previously was a taller bottom-aligned box, then this box does
// not influence the line box height or baseline.
if (child_height > max_bottom_aligned_height) {
max_top_aligned_height = std::max(max_top_aligned_height, child_height);
}
} else if (vertical_align == cssom::KeywordValue::GetBottom()) {
// Align the bottom of the aligned subtree with the bottom of the line
// box.
LayoutUnit child_height = child_box->GetInlineLevelBoxHeight();
// If there previously was a taller top-aligned box, then this box does
// not influence the line box height or baseline.
if (child_height > max_top_aligned_height) {
max_bottom_aligned_height =
std::max(max_bottom_aligned_height, child_height);
}
} else if (vertical_align == cssom::KeywordValue::GetBaseline()) {
// Align the baseline of the box with the baseline of the parent box.
baseline_offset_from_child_top_margin_edge =
child_box->GetBaselineOffsetFromTopMarginEdge();
update_height = true;
} else {
NOTREACHED() << "Unknown value of \"vertical-align\".";
}
if (update_height) {
baseline_offset_from_top_ =
std::max(baseline_offset_from_top_,
baseline_offset_from_child_top_margin_edge);
LayoutUnit baseline_offset_from_child_bottom_margin_edge =
child_box->GetInlineLevelBoxHeight() -
baseline_offset_from_child_top_margin_edge;
baseline_offset_from_bottom =
std::max(baseline_offset_from_bottom,
baseline_offset_from_child_bottom_margin_edge);
}
}
// The line box height is the distance between the uppermost box top and the
// lowermost box bottom.
// https://www.w3.org/TR/CSS21/visudet.html#line-height
height_ = baseline_offset_from_top_ + baseline_offset_from_bottom;
// In case they are aligned 'top' or 'bottom', they must be aligned so as to
// minimize the line box height. If such boxes are tall enough, there are
// multiple solutions and CSS 2.1 does not define the position of the line
// box's baseline.
// https://www.w3.org/TR/CSS21/visudet.html#line-height
// For the cases where CSS 2.1 does not specify the baseline position, the
// code below matches the behavior or WebKit and Blink.
if (max_top_aligned_height < max_bottom_aligned_height) {
if (max_top_aligned_height > height_) {
// The bottom aligned box is tallest, but there should also be enough
// space below the baseline for the shorter top aligned box.
baseline_offset_from_bottom =
max_top_aligned_height - baseline_offset_from_top_;
}
if (max_bottom_aligned_height > height_) {
// Increase the line box height above the baseline to make the largest
// bottom-aligned child box fit.
height_ = max_bottom_aligned_height;
baseline_offset_from_top_ = height_ - baseline_offset_from_bottom;
}
} else {
if (max_bottom_aligned_height > height_) {
// The top aligned box is tallest, but there should also be enough
// space above the baseline for the shorter bottom aligned box.
baseline_offset_from_top_ =
max_bottom_aligned_height - baseline_offset_from_bottom;
}
if (max_top_aligned_height > height_) {
// Increase the line box height below the baseline to make the largest
// top-aligned child box fit.
height_ = max_top_aligned_height;
baseline_offset_from_bottom = height_ - baseline_offset_from_top_;
}
}
}
void LineBox::UpdateChildBoxTopPositions() {
LayoutUnit top_offset = top_;
if (position_children_relative_to_baseline_) {
// For InlineContainerBoxes, the children have to be aligned to the baseline
// so that the vertical positioning can be consistent with the box position
// with line-height and different font sizes.
top_offset -= baseline_offset_from_top_;
}
// During this loop, the vertical positions of the child boxes are
// established.
for (ChildBoxes::const_iterator child_box_iterator = child_boxes_.begin();
child_box_iterator != child_boxes_.end(); ++child_box_iterator) {
Box* child_box = *child_box_iterator;
// The child box top position depends on the vertical-align property.
// https://www.w3.org/TR/CSS21/visudet.html#propdef-vertical-align
const scoped_refptr<cssom::PropertyValue>& vertical_align =
child_box->computed_style()->vertical_align();
LayoutUnit child_top;
if (vertical_align == cssom::KeywordValue::GetMiddle()) {
// Align the vertical midpoint of the box with the baseline of the parent
// box plus half the x-height (height of the 'x' glyph) of the parent.
child_top = baseline_offset_from_top_ -
GetHeightAboveMiddleAlignmentPoint(child_box);
} else if (vertical_align == cssom::KeywordValue::GetTop()) {
// Align the top of the aligned subtree with the top of the line box.
// Nothing to do child_top is already zero
} else if (vertical_align == cssom::KeywordValue::GetBottom()) {
// Align the bottom of the aligned subtree with the bottom of the line
// box.
child_top = height_ - child_box->GetInlineLevelBoxHeight();
} else if (vertical_align == cssom::KeywordValue::GetBaseline()) {
// Align the baseline of the box with the baseline of the parent box.
child_top = baseline_offset_from_top_ -
child_box->GetBaselineOffsetFromTopMarginEdge();
} else {
NOTREACHED() << "Unsupported vertical_align property value";
}
child_box->set_top(top_offset + child_top +
child_box->GetInlineLevelTopMargin());
}
}
void LineBox::MaybePlaceEllipsis() {
// Check to see if an ellipsis should be placed, which only occurs when the
// ellipsis has a positive width and the content has overflowed the line.
if (ellipsis_width_ <= LayoutUnit() ||
shrink_to_fit_width_ <= layout_params_.containing_block_size.width()) {
return;
}
// Determine the preferred start edge offset for the ellipsis, which is the
// offset at which the ellipsis begins clipping content on the line.
// - If the ellipsis fully fits on the line, then the preferred end edge for
// the ellipsis is the line's end edge. Therefore the preferred ellipsis
// start edge is simply the end edge offset by the ellipsis's width.
// - However, if there is insufficient space for the ellipsis to fully fit on
// the line, then the ellipsis should overflow the line's end edge, rather
// than its start edge. As a result, the preferred ellipsis start edge
// offset is simply the line's start edge.
// https://www.w3.org/TR/css3-ui/#propdef-text-overflow
LayoutUnit preferred_start_edge_offset;
if (ellipsis_width_ <= layout_params_.containing_block_size.width()) {
preferred_start_edge_offset =
base_direction_ == kRightToLeftBaseDirection
? ellipsis_width_
: layout_params_.containing_block_size.width() - ellipsis_width_;
} else {
preferred_start_edge_offset =
base_direction_ == kRightToLeftBaseDirection
? layout_params_.containing_block_size.width()
: LayoutUnit();
}
// Whether or not a character or atomic inline-level element has been
// encountered within the boxes already checked on the line. The ellipsis
// cannot be placed at an offset that precedes the first character or atomic
// inline-level element on a line.
// https://www.w3.org/TR/css3-ui/#propdef-text-overflow
bool is_placement_requirement_met = false;
// The start edge offset at which the ellipsis was eventually placed. This
// will be set by TryPlaceEllipsisOrProcessPlacedEllipsis() within one of the
// child boxes.
// NOTE: While this is is guaranteed to be set later, initializing it here
// keeps compilers from complaining about it being an uninitialized variable
// below.
LayoutUnit placed_start_edge_offset;
// Walk each box within the line in base direction order attempting to place
// the ellipsis and update the box's ellipsis state. Even after the ellipsis
// is placed, subsequent boxes must still be processed, as their state may
// change as a result of having an ellipsis preceding them on the line.
if (base_direction_ == kRightToLeftBaseDirection) {
for (ChildBoxes::reverse_iterator child_box_iterator =
child_boxes_.rbegin();
child_box_iterator != child_boxes_.rend(); ++child_box_iterator) {
Box* child_box = *child_box_iterator;
child_box->TryPlaceEllipsisOrProcessPlacedEllipsis(
base_direction_, preferred_start_edge_offset,
&is_placement_requirement_met, &is_ellipsis_placed_,
&placed_start_edge_offset);
}
} else {
for (ChildBoxes::iterator child_box_iterator = child_boxes_.begin();
child_box_iterator != child_boxes_.end(); ++child_box_iterator) {
Box* child_box = *child_box_iterator;
child_box->TryPlaceEllipsisOrProcessPlacedEllipsis(
base_direction_, preferred_start_edge_offset,
&is_placement_requirement_met, &is_ellipsis_placed_,
&placed_start_edge_offset);
}
}
// Set |placed_ellipsis_offset_|. This is the offset at which an ellipsis will
// be rendered and represents the left edge of the placed ellipsis.
// In the case where the line's base direction is right-to-left and the start
// edge is the right edge of the ellipsis, the width of the ellipsis must be
// subtracted to produce the left edge of the ellipsis.
placed_ellipsis_offset_ = base_direction_ == kRightToLeftBaseDirection
? placed_start_edge_offset - ellipsis_width_
: placed_start_edge_offset;
}
// Returns the height of half the given box above the 'middle' of the line box.
LayoutUnit LineBox::GetHeightAboveMiddleAlignmentPoint(Box* child_box) {
return (child_box->GetInlineLevelBoxHeight() +
LayoutUnit(font_metrics_.x_height())) /
2.0f;
}
LineBox::HorizontalAlignment LineBox::ComputeHorizontalAlignment() const {
// When the total width of the inline-level boxes on a line is less than
// the width of the line box containing them, their horizontal distribution
// within the line box is determined by the "text-align" property.
// https://www.w3.org/TR/CSS21/visuren.html#inline-formatting.
// text-align is vaguely specified by
// https://www.w3.org/TR/css-text-3/#text-align.
HorizontalAlignment horizontal_alignment;
if (layout_params_.containing_block_size.width() < shrink_to_fit_width_) {
// If the content has overflowed the line, then do not base horizontal
// alignment on the value of text-align. Instead, simply rely upon the base
// direction of the line, so that inline-level content begins at the
// starting edge of the line.
horizontal_alignment = base_direction_ == kRightToLeftBaseDirection
? kRightHorizontalAlignment
: kLeftHorizontalAlignment;
} else if (text_align_ == cssom::KeywordValue::GetStart()) {
// If the value of text-align is start, then inline-level content is aligned
// to the start edge of the line box.
horizontal_alignment = base_direction_ == kRightToLeftBaseDirection
? kRightHorizontalAlignment
: kLeftHorizontalAlignment;
} else if (text_align_ == cssom::KeywordValue::GetEnd()) {
// If the value of text-align is end, then inline-level content is aligned
// to the end edge of the line box.
horizontal_alignment = base_direction_ == kRightToLeftBaseDirection
? kLeftHorizontalAlignment
: kRightHorizontalAlignment;
} else if (text_align_ == cssom::KeywordValue::GetLeft()) {
// If the value of text-align is left, then inline-level content is aligned
// to the left line edge.
horizontal_alignment = kLeftHorizontalAlignment;
} else if (text_align_ == cssom::KeywordValue::GetRight()) {
// If the value of text-align is right, then inline-level content is aligned
// to the right line edge.
horizontal_alignment = kRightHorizontalAlignment;
} else if (text_align_ == cssom::KeywordValue::GetCenter()) {
// If the value of text-align is center, then inline-content is centered
// within the line box.
horizontal_alignment = kCenterHorizontalAlignment;
} else {
NOTREACHED() << "Unknown value of \"text-align\".";
horizontal_alignment = kLeftHorizontalAlignment;
}
return horizontal_alignment;
}
} // namespace layout
} // namespace cobalt