| // Copyright 2015 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/line_box.h" |
| |
| #include <algorithm> |
| #include <limits> |
| |
| #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(); |
| |
| shrink_to_fit_width_ -= collapsed_white_space_width; |
| } |
| |
| void LineBox::RestoreTrailingWhiteSpace() { |
| if (!last_non_collapsed_child_box_index_) { |
| return; |
| } |
| 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; |
| |
| 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 |
| |
| // The static position for 'right' is the distance from the right edge of the |
| // containing block to the right margin edge of the same hypothetical box as |
| // above. The value is positive if the hypothetical box is to the left of the |
| // containing block's edge. |
| // 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 |
| |
| if (child_box->is_inline_before_blockification()) { |
| CollapseTrailingWhiteSpace(); |
| child_box->SetStaticPositionLeftFromParent(shrink_to_fit_width_); |
| RestoreTrailingWhiteSpace(); |
| } else { |
| child_box->SetStaticPositionLeftFromParent(LayoutUnit()); |
| } |
| child_box->SetStaticPositionRightFromParent(LayoutUnit()); |
| 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 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 |