| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h" |
| |
| #include "third_party/blink/renderer/core/editing/position_with_affinity.h" |
| #include "third_party/blink/renderer/core/layout/geometry/writing_mode_converter.h" |
| #include "third_party/blink/renderer/core/layout/layout_block_flow.h" |
| #include "third_party/blink/renderer/core/layout/layout_text.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_block_break_token.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" |
| |
| namespace blink { |
| class HTMLBRElement; |
| |
| namespace { |
| |
| bool IsBidiControl(StringView string) { |
| return string.length() == 1 && Character::IsBidiControl(string[0]); |
| } |
| |
| LogicalRect ExpandedSelectionRectForSoftLineBreakIfNeeded( |
| const LogicalRect& rect, |
| const NGInlineCursor& cursor, |
| const LayoutSelectionStatus& selection_status) { |
| // Expand paint rect if selection covers multiple lines and |
| // this fragment is at the end of line. |
| if (selection_status.line_break == SelectSoftLineBreak::kNotSelected) |
| return rect; |
| const LayoutBlockFlow* const layout_block_flow = cursor.GetLayoutBlockFlow(); |
| if (layout_block_flow && layout_block_flow->ShouldTruncateOverflowingText()) |
| return rect; |
| // Copy from InlineTextBoxPainter::PaintSelection. |
| const LayoutUnit space_width(cursor.Current().Style().GetFont().SpaceWidth()); |
| return {rect.offset, |
| {rect.size.inline_size + space_width, rect.size.block_size}}; |
| } |
| |
| // Expands selection height so that the selection rect fills entire line. |
| LogicalRect ExpandSelectionRectToLineHeight( |
| const LogicalRect& rect, |
| const LogicalRect& line_logical_rect) { |
| // Unite the rect only in the block direction. |
| const LayoutUnit selection_top = |
| std::min(rect.offset.block_offset, line_logical_rect.offset.block_offset); |
| const LayoutUnit selection_bottom = |
| std::max(rect.BlockEndOffset(), line_logical_rect.BlockEndOffset()); |
| return {{rect.offset.inline_offset, selection_top}, |
| {rect.size.inline_size, selection_bottom - selection_top}}; |
| } |
| |
| LogicalRect ExpandSelectionRectToLineHeight(const LogicalRect& rect, |
| const NGInlineCursor& cursor) { |
| NGInlineCursor line(cursor); |
| line.MoveToContainingLine(); |
| const PhysicalRect line_physical_rect( |
| line.Current().OffsetInContainerFragment() - |
| cursor.Current().OffsetInContainerFragment(), |
| line.Current().Size()); |
| return ExpandSelectionRectToLineHeight( |
| rect, cursor.Current().ConvertChildToLogical(line_physical_rect)); |
| } |
| |
| bool IsLastBRInPage(const LayoutObject& layout_object) { |
| return layout_object.IsBR() && !layout_object.NextInPreOrder(); |
| } |
| |
| bool ShouldIgnoreForPositionForPoint(const NGFragmentItem& item) { |
| switch (item.Type()) { |
| case NGFragmentItem::kBox: |
| if (auto* box_fragment = item.BoxFragment()) { |
| if (box_fragment->IsInlineBox()) { |
| // We ignore inline box to avoid to call |PositionForPointInChild()| |
| // with empty inline box, e.g. <div>ab<b></b></div>. |
| // // All/LayoutViewHitTestTest.EmptySpan needs this. |
| return true; |
| } |
| // Skip pseudo element ::before/::after |
| // All/LayoutViewHitTestTest.PseudoElementAfter* needs this. |
| return !item.GetLayoutObject()->NonPseudoNode(); |
| } |
| // Skip virtually "culled" inline box, e.g. <span>foo</span> |
| // "editing/selection/shift-click.html" reaches here. |
| DCHECK(item.GetLayoutObject()->IsLayoutInline()) << item; |
| return true; |
| case NGFragmentItem::kGeneratedText: |
| return true; |
| case NGFragmentItem::kText: |
| // Returns true when |item.GetLayoutObject().IsStyleGenerated()|. |
| // All/LayoutViewHitTestTest.PseudoElementAfter* needs this. |
| return item.IsGeneratedText(); |
| case NGFragmentItem::kLine: |
| NOTREACHED(); |
| break; |
| } |
| return false; |
| } |
| |
| } // namespace |
| |
| inline void NGInlineCursor::MoveToItem(const ItemsSpan::iterator& iter) { |
| DCHECK(HasRoot()); |
| DCHECK(iter >= items_.begin() && iter <= items_.end()); |
| if (iter != items_.end()) { |
| current_.Set(iter); |
| return; |
| } |
| MakeNull(); |
| } |
| |
| void NGInlineCursor::SetRoot(const NGPhysicalBoxFragment& box_fragment, |
| const NGFragmentItems& fragment_items, |
| ItemsSpan items) { |
| DCHECK_EQ(box_fragment.Items(), &fragment_items); |
| DCHECK(items.data() || !items.size()); |
| root_box_fragment_ = &box_fragment; |
| fragment_items_ = &fragment_items; |
| items_ = items; |
| DCHECK(fragment_items_->IsSubSpan(items_)); |
| MoveToItem(items_.begin()); |
| } |
| |
| void NGInlineCursor::SetRoot(const NGPhysicalBoxFragment& box_fragment, |
| const NGFragmentItems& items) { |
| SetRoot(box_fragment, items, items.Items()); |
| } |
| |
| bool NGInlineCursor::TrySetRootFragmentItems() { |
| DCHECK(root_block_flow_); |
| DCHECK(!fragment_items_ || fragment_items_->Equals(items_)); |
| for (; fragment_index_ <= max_fragment_index_; AdvanceFragmentIndex()) { |
| const NGPhysicalBoxFragment* fragment = |
| root_block_flow_->GetPhysicalFragment(fragment_index_); |
| DCHECK(fragment); |
| if (const NGFragmentItems* items = fragment->Items()) { |
| SetRoot(*fragment, *items); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void NGInlineCursor::SetRoot(const LayoutBlockFlow& block_flow) { |
| DCHECK(&block_flow); |
| DCHECK(!HasRoot()); |
| |
| if (const wtf_size_t fragment_count = block_flow.PhysicalFragmentCount()) { |
| root_block_flow_ = &block_flow; |
| max_fragment_index_ = fragment_count - 1; |
| ResetFragmentIndex(); |
| if (TrySetRootFragmentItems()) |
| return; |
| } |
| |
| // We reach here in case of |ScrollANchor::NotifyBeforeLayout()| via |
| // |LayoutText::PhysicalLinesBoundingBox()| |
| // See external/wpt/css/css-scroll-anchoring/wrapped-text.html |
| } |
| |
| NGInlineCursor::NGInlineCursor(const LayoutBlockFlow& block_flow) { |
| SetRoot(block_flow); |
| } |
| |
| NGInlineCursor::NGInlineCursor(const NGPhysicalBoxFragment& box_fragment, |
| const NGFragmentItems& fragment_items, |
| ItemsSpan items) { |
| SetRoot(box_fragment, fragment_items, items); |
| } |
| |
| NGInlineCursor::NGInlineCursor(const NGPhysicalBoxFragment& box_fragment, |
| const NGFragmentItems& items) { |
| SetRoot(box_fragment, items); |
| } |
| |
| NGInlineCursor::NGInlineCursor(const NGPhysicalBoxFragment& box_fragment) { |
| if (const NGFragmentItems* items = box_fragment.Items()) |
| SetRoot(box_fragment, *items); |
| } |
| |
| NGInlineCursor::NGInlineCursor(const NGInlineBackwardCursor& backward_cursor) |
| : NGInlineCursor(backward_cursor.cursor_) { |
| MoveTo(backward_cursor.Current()); |
| } |
| |
| bool NGInlineCursor::operator==(const NGInlineCursor& other) const { |
| if (current_.item_ != other.current_.item_) |
| return false; |
| DCHECK_EQ(items_.data(), other.items_.data()); |
| DCHECK_EQ(items_.size(), other.items_.size()); |
| DCHECK_EQ(fragment_items_, other.fragment_items_); |
| DCHECK(current_.item_iter_ == other.current_.item_iter_); |
| return true; |
| } |
| |
| const LayoutBlockFlow* NGInlineCursor::GetLayoutBlockFlow() const { |
| DCHECK_EQ(HasRoot(), !!root_box_fragment_); |
| if (root_box_fragment_) { |
| const LayoutObject* layout_object = |
| root_box_fragment_->GetSelfOrContainerLayoutObject(); |
| DCHECK(layout_object); |
| DCHECK(!layout_object->IsLayoutFlowThread()); |
| return To<LayoutBlockFlow>(layout_object); |
| } |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| bool NGInlineCursorPosition::HasChildren() const { |
| if (item_) |
| return item_->HasChildren(); |
| NOTREACHED(); |
| return false; |
| } |
| |
| NGInlineCursor NGInlineCursor::CursorForDescendants() const { |
| if (current_.item_) { |
| unsigned descendants_count = current_.item_->DescendantsCount(); |
| if (descendants_count > 1) { |
| DCHECK(root_box_fragment_); |
| DCHECK(fragment_items_); |
| return NGInlineCursor( |
| *root_box_fragment_, *fragment_items_, |
| ItemsSpan(&*(current_.item_iter_ + 1), descendants_count - 1)); |
| } |
| return NGInlineCursor(); |
| } |
| NOTREACHED(); |
| return NGInlineCursor(); |
| } |
| |
| void NGInlineCursor::ExpandRootToContainingBlock() { |
| if (fragment_items_) { |
| const unsigned index_diff = items_.data() - fragment_items_->Items().data(); |
| DCHECK_LT(index_diff, fragment_items_->Items().size()); |
| const unsigned item_index = current_.item_iter_ - items_.begin(); |
| items_ = fragment_items_->Items(); |
| // Update the iterator to the one for the new span. |
| MoveToItem(items_.begin() + item_index + index_diff); |
| return; |
| } |
| NOTREACHED(); |
| } |
| |
| bool NGInlineCursorPosition::HasSoftWrapToNextLine() const { |
| DCHECK(IsLineBox()); |
| const NGInlineBreakToken* break_token = InlineBreakToken(); |
| return break_token && !break_token->IsForcedBreak(); |
| } |
| |
| bool NGInlineCursorPosition::IsInlineLeaf() const { |
| if (IsHiddenForPaint()) |
| return false; |
| if (IsText()) |
| return !IsLayoutGeneratedText(); |
| if (!IsAtomicInline()) |
| return false; |
| return !IsListMarker(); |
| } |
| |
| bool NGInlineCursorPosition::IsPartOfCulledInlineBox( |
| const LayoutInline& layout_inline) const { |
| DCHECK(!layout_inline.ShouldCreateBoxFragment()); |
| DCHECK(*this); |
| const LayoutObject* const layout_object = GetLayoutObject(); |
| // We use |IsInline()| to exclude floating and out-of-flow objects. |
| if (!layout_object || !layout_object->IsInline() || |
| layout_object->IsAtomicInlineLevel()) |
| return false; |
| DCHECK(!layout_object->IsFloatingOrOutOfFlowPositioned()); |
| DCHECK(!BoxFragment() || !BoxFragment()->IsFormattingContextRoot()); |
| for (const LayoutObject* parent = layout_object->Parent(); parent; |
| parent = parent->Parent()) { |
| // Children of culled inline should be included. |
| if (parent == &layout_inline) |
| return true; |
| // Grand children should be included only if children are also culled. |
| if (const auto* parent_layout_inline = DynamicTo<LayoutInline>(parent)) { |
| if (!parent_layout_inline->ShouldCreateBoxFragment()) |
| continue; |
| } |
| return false; |
| } |
| return false; |
| } |
| |
| bool NGInlineCursor::IsLastLineInInlineBlock() const { |
| DCHECK(Current().IsLineBox()); |
| if (!GetLayoutBlockFlow()->IsAtomicInlineLevel()) |
| return false; |
| NGInlineCursor next_sibling(*this); |
| for (;;) { |
| next_sibling.MoveToNextSkippingChildren(); |
| if (!next_sibling) |
| return true; |
| if (next_sibling.Current().IsLineBox()) |
| return false; |
| // There maybe other top-level objects such as floats, OOF, or list-markers. |
| } |
| } |
| |
| bool NGInlineCursor::IsBeforeSoftLineBreak() const { |
| if (Current().IsLineBreak()) |
| return false; |
| // Inline block is not be container line box. |
| // See paint/selection/text-selection-inline-block.html. |
| NGInlineCursor line(*this); |
| line.MoveToContainingLine(); |
| if (line.IsLastLineInInlineBlock()) { |
| // We don't paint a line break the end of inline-block |
| // because if an inline-block is at the middle of line, we should not paint |
| // a line break. |
| // Old layout paints line break if the inline-block is at the end of line, |
| // but since its complex to determine if the inline-block is at the end of |
| // line on NG, we just cancels block-end line break painting for any |
| // inline-block. |
| return false; |
| } |
| NGInlineCursor last_leaf(line); |
| last_leaf.MoveToLastLogicalLeaf(); |
| if (last_leaf != *this) |
| return false; |
| // Even If |fragment| is before linebreak, if its direction differs to line |
| // direction, we don't paint line break. See |
| // paint/selection/text-selection-newline-mixed-ltr-rtl.html. |
| return line.Current().BaseDirection() == Current().ResolvedDirection(); |
| } |
| |
| bool NGInlineCursorPosition::CanHaveChildren() const { |
| if (item_) { |
| return item_->Type() == NGFragmentItem::kLine || |
| (item_->Type() == NGFragmentItem::kBox && !item_->IsAtomicInline()); |
| } |
| NOTREACHED(); |
| return false; |
| } |
| |
| TextDirection NGInlineCursorPosition::BaseDirection() const { |
| DCHECK(IsLineBox()); |
| if (item_) |
| return item_->BaseDirection(); |
| NOTREACHED(); |
| return TextDirection::kLtr; |
| } |
| |
| UBiDiLevel NGInlineCursorPosition::BidiLevel() const { |
| if (IsText()) { |
| if (IsLayoutGeneratedText()) { |
| // TODO(yosin): Until we have clients, we don't support bidi-level for |
| // ellipsis and soft hyphens. |
| NOTREACHED() << this; |
| return 0; |
| } |
| const auto& layout_text = *To<LayoutText>(GetLayoutObject()); |
| DCHECK(!layout_text.NeedsLayout()) << this; |
| const auto* const items = layout_text.GetNGInlineItems(); |
| if (!items || items->size() == 0) { |
| // In case of <br>, <wbr>, text-combine-upright, etc. |
| return 0; |
| } |
| const NGTextOffset offset = TextOffset(); |
| const auto& item = std::find_if( |
| items->begin(), items->end(), [offset](const NGInlineItem& item) { |
| return item.StartOffset() <= offset.start && |
| item.EndOffset() >= offset.end; |
| }); |
| DCHECK(item != items->end()) << this; |
| return item->BidiLevel(); |
| } |
| |
| if (IsAtomicInline()) { |
| DCHECK(GetLayoutObject()->ContainingNGBlockFlow()); |
| const LayoutBlockFlow& block_flow = |
| *GetLayoutObject()->ContainingNGBlockFlow(); |
| const Vector<NGInlineItem> items = |
| block_flow.GetNGInlineNodeData()->ItemsData(UsesFirstLineStyle()).items; |
| const LayoutObject* const layout_object = GetLayoutObject(); |
| const auto* const item = std::find_if( |
| items.begin(), items.end(), [layout_object](const NGInlineItem& item) { |
| return item.GetLayoutObject() == layout_object; |
| }); |
| DCHECK(item != items.end()) << this; |
| return item->BidiLevel(); |
| } |
| |
| NOTREACHED(); |
| return 0; |
| } |
| |
| const DisplayItemClient* NGInlineCursorPosition::GetSelectionDisplayItemClient() |
| const { |
| if (const auto* client = GetLayoutObject()->GetSelectionDisplayItemClient()) |
| return client; |
| return GetDisplayItemClient(); |
| } |
| |
| const Node* NGInlineCursorPosition::GetNode() const { |
| if (const LayoutObject* layout_object = GetLayoutObject()) |
| return layout_object->GetNode(); |
| return nullptr; |
| } |
| |
| void NGInlineCursorPosition::RecalcInkOverflow( |
| const NGInlineCursor& cursor) const { |
| DCHECK(item_); |
| DCHECK_EQ(item_, cursor.Current().Item()); |
| PhysicalRect self_and_contents_rect; |
| item_->GetMutableForPainting().RecalcInkOverflow(cursor, |
| &self_and_contents_rect); |
| } |
| |
| StringView NGInlineCursorPosition::Text(const NGInlineCursor& cursor) const { |
| DCHECK(IsText()); |
| cursor.CheckValid(*this); |
| if (item_) |
| return item_->Text(cursor.Items()); |
| NOTREACHED(); |
| return ""; |
| } |
| |
| PhysicalRect NGInlineCursor::CurrentLocalRect(unsigned start_offset, |
| unsigned end_offset) const { |
| DCHECK(Current().IsText()); |
| if (current_.item_) { |
| return current_.item_->LocalRect(current_.item_->Text(*fragment_items_), |
| start_offset, end_offset); |
| } |
| NOTREACHED(); |
| return PhysicalRect(); |
| } |
| |
| PhysicalRect NGInlineCursor::CurrentLocalSelectionRectForText( |
| const LayoutSelectionStatus& selection_status) const { |
| const PhysicalRect selection_rect = |
| CurrentLocalRect(selection_status.start, selection_status.end); |
| LogicalRect logical_rect = Current().ConvertChildToLogical(selection_rect); |
| // Let LocalRect for line break have a space width to paint line break |
| // when it is only character in a line or only selected in a line. |
| if (selection_status.start != selection_status.end && |
| Current().IsLineBreak() && |
| // This is for old compatible that old doesn't paint last br in a page. |
| !IsLastBRInPage(*Current().GetLayoutObject())) { |
| DCHECK(!logical_rect.size.inline_size); |
| logical_rect.size.inline_size = |
| LayoutUnit(Current().Style().GetFont().SpaceWidth()); |
| } |
| const LogicalRect line_break_extended_rect = |
| Current().IsLineBreak() ? logical_rect |
| : ExpandedSelectionRectForSoftLineBreakIfNeeded( |
| logical_rect, *this, selection_status); |
| const LogicalRect line_height_expanded_rect = |
| ExpandSelectionRectToLineHeight(line_break_extended_rect, *this); |
| const PhysicalRect physical_rect = |
| Current().ConvertChildToPhysical(line_height_expanded_rect); |
| return physical_rect; |
| } |
| |
| PhysicalRect NGInlineCursor::CurrentLocalSelectionRectForReplaced() const { |
| DCHECK(Current().GetLayoutObject()->IsLayoutReplaced()); |
| const PhysicalRect selection_rect = PhysicalRect({}, Current().Size()); |
| LogicalRect logical_rect = Current().ConvertChildToLogical(selection_rect); |
| const LogicalRect line_height_expanded_rect = |
| ExpandSelectionRectToLineHeight(logical_rect, *this); |
| const PhysicalRect physical_rect = |
| Current().ConvertChildToPhysical(line_height_expanded_rect); |
| return physical_rect; |
| } |
| |
| PhysicalRect NGInlineCursor::CurrentRectInBlockFlow() const { |
| PhysicalRect rect = Current().RectInContainerFragment(); |
| // We'll now convert the offset from being relative to the containing fragment |
| // to being relative to the containing LayoutBlockFlow. For writing modes that |
| // don't flip the block direction, this is easy: just add the block-size |
| // consumed in previous fragments. |
| auto writing_direction = ContainerFragment().Style().GetWritingDirection(); |
| switch (writing_direction.GetWritingMode()) { |
| default: |
| rect.offset.top += previously_consumed_block_size_; |
| break; |
| case WritingMode::kVerticalLr: |
| rect.offset.left += previously_consumed_block_size_; |
| break; |
| case WritingMode::kVerticalRl: { |
| // For vertical-rl writing-mode it's a bit more complicated. We need to |
| // convert to logical coordinates in the containing box fragment, in order |
| // to add the consumed block-size to make it relative to the |
| // LayoutBlockFlow ("flow thread coordinate space"), and then we convert |
| // back to physical coordinates. |
| const LayoutBlock* containing_block = |
| Current().GetLayoutObject()->ContainingBlock(); |
| DCHECK_EQ(containing_block->StyleRef().GetWritingDirection(), |
| ContainerFragment().Style().GetWritingDirection()); |
| LogicalOffset logical_offset = rect.offset.ConvertToLogical( |
| writing_direction, ContainerFragment().Size(), rect.size); |
| LogicalOffset logical_offset_in_flow_thread( |
| logical_offset.inline_offset, |
| logical_offset.block_offset + previously_consumed_block_size_); |
| rect.offset = logical_offset_in_flow_thread.ConvertToPhysical( |
| writing_direction, PhysicalSize(containing_block->Size()), rect.size); |
| break; |
| } |
| }; |
| return rect; |
| } |
| |
| LayoutUnit NGInlineCursor::InlinePositionForOffset(unsigned offset) const { |
| DCHECK(Current().IsText()); |
| if (current_.item_) { |
| return current_.item_->InlinePositionForOffset( |
| current_.item_->Text(*fragment_items_), offset); |
| } |
| NOTREACHED(); |
| return LayoutUnit(); |
| } |
| |
| LogicalRect NGInlineCursorPosition::ConvertChildToLogical( |
| const PhysicalRect& physical_rect) const { |
| return WritingModeConverter( |
| {Style().GetWritingMode(), ResolvedOrBaseDirection()}, Size()) |
| .ToLogical(physical_rect); |
| } |
| |
| PhysicalRect NGInlineCursorPosition::ConvertChildToPhysical( |
| const LogicalRect& logical_rect) const { |
| return WritingModeConverter( |
| {Style().GetWritingMode(), ResolvedOrBaseDirection()}, Size()) |
| .ToPhysical(logical_rect); |
| } |
| |
| PositionWithAffinity NGInlineCursor::PositionForPointInInlineFormattingContext( |
| const PhysicalOffset& point, |
| const NGPhysicalBoxFragment& container) { |
| DCHECK(HasRoot()); |
| const auto writing_direction = container.Style().GetWritingDirection(); |
| const PhysicalSize& container_size = container.Size(); |
| const LayoutUnit point_block_offset = |
| point |
| .ConvertToLogical(writing_direction, container_size, |
| // |point| is actually a pixel with size 1x1. |
| PhysicalSize(LayoutUnit(1), LayoutUnit(1))) |
| .block_offset; |
| |
| // Stores the closest line box child after |point| in the block direction. |
| // Used if we can't find any child |point| falls in to resolve the position. |
| NGInlineCursorPosition closest_line_after; |
| LayoutUnit closest_line_after_block_offset = LayoutUnit::Min(); |
| |
| // Stores the closest line box child before |point| in the block direction. |
| // Used if we can't find any child |point| falls in to resolve the position. |
| NGInlineCursorPosition closest_line_before; |
| LayoutUnit closest_line_before_block_offset = LayoutUnit::Max(); |
| |
| while (*this) { |
| const NGFragmentItem* child_item = CurrentItem(); |
| DCHECK(child_item); |
| if (child_item->Type() == NGFragmentItem::kLine) { |
| if (!CursorForDescendants().TryToMoveToFirstInlineLeafChild()) { |
| // editing/selection/last-empty-inline.html requires this to skip |
| // empty <span> with padding. |
| MoveToNextSkippingChildren(); |
| continue; |
| } |
| // Try to resolve if |point| falls in a line box in block direction. |
| const LayoutUnit child_block_offset = |
| child_item->OffsetInContainerFragment() |
| .ConvertToLogical(writing_direction, container_size, |
| child_item->Size()) |
| .block_offset; |
| if (point_block_offset < child_block_offset) { |
| if (child_block_offset < closest_line_before_block_offset) { |
| closest_line_before_block_offset = child_block_offset; |
| closest_line_before = Current(); |
| } |
| MoveToNextSkippingChildren(); |
| continue; |
| } |
| |
| // Hitting on line bottom doesn't count, to match legacy behavior. |
| const LayoutUnit child_block_end_offset = |
| child_block_offset + |
| child_item->Size() |
| .ConvertToLogical(writing_direction.GetWritingMode()) |
| .block_size; |
| if (point_block_offset >= child_block_end_offset) { |
| if (child_block_end_offset > closest_line_after_block_offset) { |
| closest_line_after_block_offset = child_block_end_offset; |
| closest_line_after = Current(); |
| } |
| MoveToNextSkippingChildren(); |
| continue; |
| } |
| |
| if (const PositionWithAffinity child_position = |
| PositionForPointInInlineBox(point)) |
| return child_position; |
| MoveToNextSkippingChildren(); |
| continue; |
| } |
| DCHECK_NE(child_item->Type(), NGFragmentItem::kText); |
| MoveToNext(); |
| } |
| |
| // At here, |point| is not inside any line in |this|: |
| // |closest_line_before| |
| // |point| |
| // |closest_line_after| |
| if (closest_line_before) { |
| MoveTo(closest_line_before); |
| // Note: |move_caret_to_boundary| is true for Mac and Unix. |
| const bool move_caret_to_boundary = |
| To<LayoutBlockFlow>(Current().GetLayoutObject()) |
| ->ShouldMoveCaretToHorizontalBoundaryWhenPastTopOrBottom(); |
| if (move_caret_to_boundary) { |
| // Tests[1-3] reach here. |
| // [1] editing/selection/click-in-margins-inside-editable-div.html |
| // [2] fast/writing-mode/flipped-blocks-hit-test-line-edges.html |
| // [3] All/LayoutViewHitTestTest.HitTestHorizontal/4 |
| if (auto first_position = PositionForStartOfLine()) |
| return PositionWithAffinity(first_position.GetPosition()); |
| } else if (const PositionWithAffinity child_position = |
| PositionForPointInInlineBox(point)) |
| return child_position; |
| } |
| |
| if (closest_line_after) { |
| MoveTo(closest_line_after); |
| // Note: |move_caret_to_boundary| is true for Mac and Unix. |
| const bool move_caret_to_boundary = |
| To<LayoutBlockFlow>(Current().GetLayoutObject()) |
| ->ShouldMoveCaretToHorizontalBoundaryWhenPastTopOrBottom(); |
| if (move_caret_to_boundary) { |
| // Tests[1-3] reach here. |
| // [1] editing/selection/click-in-margins-inside-editable-div.html |
| // [2] fast/writing-mode/flipped-blocks-hit-test-line-edges.html |
| // [3] All/LayoutViewHitTestTest.HitTestHorizontal/4 |
| if (auto last_position = PositionForEndOfLine()) |
| return PositionWithAffinity(last_position.GetPosition()); |
| } else if (const PositionWithAffinity child_position = |
| PositionForPointInInlineBox(point)) { |
| // Test[1] reaches here. |
| // [1] editing/selection/last-empty-inline.html |
| return child_position; |
| } |
| } |
| |
| return PositionWithAffinity(); |
| } |
| |
| PositionWithAffinity NGInlineCursor::PositionForPointInInlineBox( |
| const PhysicalOffset& point) const { |
| const NGFragmentItem* container = CurrentItem(); |
| DCHECK(container); |
| DCHECK(container->Type() == NGFragmentItem::kLine || |
| container->Type() == NGFragmentItem::kBox); |
| const auto writing_direction = container->Style().GetWritingDirection(); |
| const PhysicalSize& container_size = container->Size(); |
| const LayoutUnit point_inline_offset = |
| point |
| .ConvertToLogical(writing_direction, container_size, |
| // |point| is actually a pixel with size 1x1. |
| PhysicalSize(LayoutUnit(1), LayoutUnit(1))) |
| .inline_offset; |
| |
| // Stores the closest child before |point| in the inline direction. Used if we |
| // can't find any child |point| falls in to resolve the position. |
| NGInlineCursorPosition closest_child_before; |
| LayoutUnit closest_child_before_inline_offset = LayoutUnit::Min(); |
| |
| // Stores the closest child after |point| in the inline direction. Used if we |
| // can't find any child |point| falls in to resolve the position. |
| NGInlineCursorPosition closest_child_after; |
| LayoutUnit closest_child_after_inline_offset = LayoutUnit::Max(); |
| |
| NGInlineCursor descendants = CursorForDescendants(); |
| for (; descendants; descendants.MoveToNext()) { |
| const NGFragmentItem* child_item = descendants.CurrentItem(); |
| DCHECK(child_item); |
| if (ShouldIgnoreForPositionForPoint(*child_item)) |
| continue; |
| const LayoutUnit child_inline_offset = |
| child_item->OffsetInContainerFragment() |
| .ConvertToLogical(writing_direction, container_size, |
| child_item->Size()) |
| .inline_offset; |
| if (point_inline_offset < child_inline_offset) { |
| if (child_item->IsFloating()) |
| continue; |
| if (child_inline_offset < closest_child_after_inline_offset) { |
| closest_child_after_inline_offset = child_inline_offset; |
| closest_child_after = descendants.Current(); |
| } |
| continue; |
| } |
| const LayoutUnit child_inline_end_offset = |
| child_inline_offset + |
| child_item->Size() |
| .ConvertToLogical(writing_direction.GetWritingMode()) |
| .inline_size; |
| if (point_inline_offset >= child_inline_end_offset) { |
| if (child_item->IsFloating()) |
| continue; |
| if (child_inline_end_offset > closest_child_before_inline_offset) { |
| closest_child_before_inline_offset = child_inline_end_offset; |
| closest_child_before = descendants.Current(); |
| } |
| continue; |
| } |
| |
| // |point_inline_offset| is in |child_item|. |
| if (const PositionWithAffinity child_position = |
| descendants.PositionForPointInChild(point)) |
| return child_position; |
| } |
| |
| // Note: We don't snap a point before/after of "float" to "float", |
| // |closest_child_after| and |closest_child_before| can not be a box for |
| // "float". |
| // Note: Float boxes are appeared in |NGFragmentItems| as DOM order, so, |
| // "float:right" can be placed anywhere instead of at end of items. |
| // See LayoutViewHitTest.Float{Left,Right}* |
| if (closest_child_after) { |
| descendants.MoveTo(closest_child_after); |
| if (const PositionWithAffinity child_position = |
| descendants.PositionForPointInChild(point)) |
| return child_position; |
| if (closest_child_after->BoxFragment()) { |
| DCHECK(!closest_child_after->IsFloating()); |
| // Hit test at left of "12"[1] and after "cd"[2] reache here. |
| // "<span dir="rtl">12<b>קא43</b></span>ab" |
| // [1] "editing/selection/caret-at-bidi-boundary.html" |
| // [2] HitTestingTest.PseudoElementAfter |
| if (const PositionWithAffinity child_position = |
| descendants.PositionForPointInInlineBox(point)) |
| return child_position; |
| } |
| } |
| |
| if (closest_child_before) { |
| descendants.MoveTo(closest_child_before); |
| if (const PositionWithAffinity child_position = |
| descendants.PositionForPointInChild(point)) |
| return child_position; |
| if (closest_child_before->BoxFragment()) { |
| DCHECK(!closest_child_before->IsFloating()); |
| // LayoutViewHitTest.HitTestHorizontal "Top-right corner (outside) of div" |
| // reach here. |
| if (const PositionWithAffinity child_position = |
| descendants.PositionForPointInInlineBox(point)) |
| return child_position; |
| } |
| } |
| |
| return PositionWithAffinity(); |
| } |
| |
| PositionWithAffinity NGInlineCursor::PositionForPointInChild( |
| const PhysicalOffset& point_in_container) const { |
| DCHECK(CurrentItem()); |
| const NGFragmentItem& child_item = *CurrentItem(); |
| switch (child_item.Type()) { |
| case NGFragmentItem::kText: |
| return child_item.PositionForPointInText( |
| point_in_container - child_item.OffsetInContainerFragment(), *this); |
| case NGFragmentItem::kGeneratedText: |
| break; |
| case NGFragmentItem::kBox: |
| if (const NGPhysicalBoxFragment* box_fragment = |
| child_item.BoxFragment()) { |
| if (!box_fragment->IsInlineBox()) { |
| // In case of inline block with with block formatting context that |
| // has block children[1]. |
| // Example: <b style="display:inline-block"><div>b</div></b> |
| // [1] NGInlineCursorTest.PositionForPointInChildBlockChildren |
| return child_item.GetLayoutObject()->PositionForPoint( |
| point_in_container - child_item.OffsetInContainerFragment()); |
| } |
| } else { |
| // |LayoutInline| used to be culled. |
| } |
| DCHECK(child_item.GetLayoutObject()->IsLayoutInline()) << child_item; |
| break; |
| case NGFragmentItem::kLine: |
| NOTREACHED(); |
| break; |
| } |
| return PositionWithAffinity(); |
| } |
| |
| PositionWithAffinity NGInlineCursor::PositionForPointInText( |
| unsigned text_offset) const { |
| DCHECK(Current().IsText()) << this; |
| if (HasRoot()) |
| return Current()->PositionForPointInText(text_offset, *this); |
| return PositionWithAffinity(); |
| } |
| |
| PositionWithAffinity NGInlineCursor::PositionForStartOfLine() const { |
| DCHECK(Current().IsLineBox()); |
| NGInlineCursor first_leaf = CursorForDescendants(); |
| if (IsLtr(Current().BaseDirection())) |
| first_leaf.MoveToFirstNonPseudoLeaf(); |
| else |
| first_leaf.MoveToLastNonPseudoLeaf(); |
| if (!first_leaf) |
| return PositionWithAffinity(); |
| Node* const node = first_leaf.Current().GetLayoutObject()->NonPseudoNode(); |
| if (!node) { |
| NOTREACHED() << "MoveToFirstLeaf returns invalid node: " << first_leaf; |
| return PositionWithAffinity(); |
| } |
| if (!IsA<Text>(node)) |
| return PositionWithAffinity(Position::BeforeNode(*node)); |
| const unsigned text_offset = |
| Current().BaseDirection() == first_leaf.Current().ResolvedDirection() |
| ? first_leaf.Current().TextOffset().start |
| : first_leaf.Current().TextOffset().end; |
| return first_leaf.PositionForPointInText(text_offset); |
| } |
| |
| PositionWithAffinity NGInlineCursor::PositionForEndOfLine() const { |
| DCHECK(Current().IsLineBox()); |
| NGInlineCursor last_leaf = CursorForDescendants(); |
| if (IsLtr(Current().BaseDirection())) |
| last_leaf.MoveToLastNonPseudoLeaf(); |
| else |
| last_leaf.MoveToFirstNonPseudoLeaf(); |
| if (!last_leaf) |
| return PositionWithAffinity(); |
| Node* const node = last_leaf.Current().GetLayoutObject()->NonPseudoNode(); |
| if (!node) { |
| NOTREACHED() << "MoveToLastLeaf returns invalid node: " << last_leaf; |
| return PositionWithAffinity(); |
| } |
| if (IsA<HTMLBRElement>(node)) |
| return PositionWithAffinity(Position::BeforeNode(*node)); |
| if (!IsA<Text>(node)) |
| return PositionWithAffinity(Position::AfterNode(*node)); |
| const unsigned text_offset = |
| Current().BaseDirection() == last_leaf.Current().ResolvedDirection() |
| ? last_leaf.Current().TextOffset().end |
| : last_leaf.Current().TextOffset().start; |
| return last_leaf.PositionForPointInText(text_offset); |
| } |
| |
| void NGInlineCursor::MoveTo(const NGInlineCursorPosition& position) { |
| CheckValid(position); |
| current_ = position; |
| } |
| |
| inline wtf_size_t NGInlineCursor::SpanBeginItemIndex() const { |
| DCHECK(HasRoot()); |
| DCHECK(!items_.empty()); |
| DCHECK(fragment_items_->IsSubSpan(items_)); |
| const wtf_size_t delta = items_.data() - fragment_items_->Items().data(); |
| DCHECK_LT(delta, fragment_items_->Items().size()); |
| return delta; |
| } |
| |
| inline wtf_size_t NGInlineCursor::SpanIndexFromItemIndex(unsigned index) const { |
| DCHECK(HasRoot()); |
| DCHECK(!items_.empty()); |
| DCHECK(fragment_items_->IsSubSpan(items_)); |
| if (items_.data() == fragment_items_->Items().data()) |
| return index; |
| const wtf_size_t span_index = |
| fragment_items_->Items().data() - items_.data() + index; |
| DCHECK_LT(span_index, items_.size()); |
| return span_index; |
| } |
| |
| void NGInlineCursor::MoveTo(const NGFragmentItem& fragment_item) { |
| MoveTo(*fragment_item.GetLayoutObject()); |
| while (IsNotNull()) { |
| if (CurrentItem() == &fragment_item) |
| return; |
| MoveToNext(); |
| } |
| NOTREACHED(); |
| } |
| |
| void NGInlineCursor::MoveTo(const NGInlineCursor& cursor) { |
| if (cursor.current_.item_) { |
| if (!fragment_items_) |
| SetRoot(*cursor.root_box_fragment_, *cursor.fragment_items_); |
| // Note: We use address instead of iterator because we can't compare |
| // iterators in different span. See |base::CheckedContiguousIterator<T>|. |
| const ptrdiff_t index = &*cursor.current_.item_iter_ - &*items_.begin(); |
| DCHECK_GE(index, 0); |
| DCHECK_LT(static_cast<size_t>(index), items_.size()); |
| MoveToItem(items_.begin() + index); |
| return; |
| } |
| *this = cursor; |
| } |
| |
| void NGInlineCursor::MoveToContainingLine() { |
| DCHECK(!Current().IsLineBox()); |
| if (current_.item_) { |
| while (current_.item_ && !Current().IsLineBox()) |
| MoveToPrevious(); |
| return; |
| } |
| NOTREACHED(); |
| } |
| |
| bool NGInlineCursor::IsAtFirst() const { |
| if (const NGFragmentItem* item = Current().Item()) |
| return item == &items_.front(); |
| return false; |
| } |
| |
| void NGInlineCursor::MoveToFirst() { |
| if (HasRoot()) { |
| MoveToItem(items_.begin()); |
| return; |
| } |
| NOTREACHED(); |
| } |
| |
| void NGInlineCursor::MoveToFirstChild() { |
| DCHECK(Current().CanHaveChildren()); |
| if (!TryToMoveToFirstChild()) |
| MakeNull(); |
| } |
| |
| void NGInlineCursor::MoveToFirstLine() { |
| if (HasRoot()) { |
| auto iter = std::find_if( |
| items_.begin(), items_.end(), |
| [](const auto& item) { return item.Type() == NGFragmentItem::kLine; }); |
| if (iter != items_.end()) { |
| MoveToItem(iter); |
| return; |
| } |
| MakeNull(); |
| return; |
| } |
| NOTREACHED(); |
| } |
| |
| void NGInlineCursor::MoveToFirstLogicalLeaf() { |
| DCHECK(Current().IsLineBox()); |
| // TODO(yosin): This isn't correct for mixed Bidi. Fix it. Besides, we |
| // should compute and store it during layout. |
| // TODO(yosin): We should check direction of each container instead of line |
| // box. |
| if (IsLtr(Current().Style().Direction())) { |
| while (TryToMoveToFirstChild()) |
| continue; |
| return; |
| } |
| while (TryToMoveToLastChild()) |
| continue; |
| } |
| |
| void NGInlineCursor::MoveToFirstNonPseudoLeaf() { |
| for (NGInlineCursor cursor = *this; cursor; cursor.MoveToNext()) { |
| if (!cursor.Current().GetLayoutObject()->NonPseudoNode()) |
| continue; |
| if (cursor.Current().IsText()) { |
| // Note: We should not skip bidi control only text item to return |
| // position after bibi control character, e.g. |
| // <p dir=rtl>‫xyz ABC.‬</p> |
| // See "editing/selection/home-end.html". |
| DCHECK(!cursor.Current().IsLayoutGeneratedText()) << cursor; |
| if (cursor.Current().IsLineBreak()) { |
| // We ignore line break character, e.g. newline with white-space:pre, |
| // like |MoveToLastNonPseudoLeaf()| as consistency. |
| // See |ParameterizedVisibleUnitsLineTest.EndOfLineWithWhiteSpacePre| |
| continue; |
| } |
| *this = cursor; |
| return; |
| } |
| if (cursor.Current().IsInlineLeaf()) { |
| *this = cursor; |
| return; |
| } |
| } |
| MakeNull(); |
| } |
| |
| void NGInlineCursor::MoveToLastChild() { |
| DCHECK(Current().CanHaveChildren()); |
| if (!TryToMoveToLastChild()) |
| MakeNull(); |
| } |
| |
| void NGInlineCursor::MoveToLastLine() { |
| DCHECK(HasRoot()); |
| auto iter = std::find_if( |
| items_.rbegin(), items_.rend(), |
| [](const auto& item) { return item.Type() == NGFragmentItem::kLine; }); |
| if (iter != items_.rend()) |
| MoveToItem(std::next(iter).base()); |
| else |
| MakeNull(); |
| } |
| |
| void NGInlineCursor::MoveToLastLogicalLeaf() { |
| DCHECK(Current().IsLineBox()); |
| // TODO(yosin): This isn't correct for mixed Bidi. Fix it. Besides, we |
| // should compute and store it during layout. |
| // TODO(yosin): We should check direction of each container instead of line |
| // box. |
| if (IsLtr(Current().Style().Direction())) { |
| while (TryToMoveToLastChild()) |
| continue; |
| return; |
| } |
| while (TryToMoveToFirstChild()) |
| continue; |
| } |
| |
| void NGInlineCursor::MoveToLastNonPseudoLeaf() { |
| // TODO(yosin): We should introduce |IsTruncated()| to avoid to use |
| // |in_hidden_for_paint|. See also |LayoutText::GetTextBoxInfo()|. |
| // When "text-overflow:ellipsis" specified, items are: |
| // [i+0] original non-truncated text (IsHiddenForPaint()=true) |
| // [i+1] truncated text |
| // [i+2] ellipsis (IsLayoutGeneratedText()) |
| NGInlineCursor last_leaf; |
| bool in_hidden_for_paint = false; |
| for (NGInlineCursor cursor = *this; cursor; cursor.MoveToNext()) { |
| if (!cursor.Current().GetLayoutObject()->NonPseudoNode()) |
| continue; |
| if (cursor.Current().IsLineBreak() && last_leaf) |
| break; |
| if (cursor.Current().IsText()) { |
| DCHECK(!cursor.Current().IsLayoutGeneratedText()); |
| if (in_hidden_for_paint && !cursor.Current().IsHiddenForPaint()) { |
| // |cursor| is at truncated text. |
| break; |
| } |
| in_hidden_for_paint = cursor.Current().IsHiddenForPaint(); |
| // Exclude bidi control only fragment, e.g. |
| // <p dir=ltr>‫xyz ABC.‬</p> has |
| // [0] "\u202Bxyz " |
| // [1] "ABC" |
| // [2] "." |
| // [3] "\u202C" |
| // See "editing/selection/home-end.html" |
| if (IsBidiControl(cursor.Current().Text(cursor))) |
| continue; |
| last_leaf = cursor; |
| continue; |
| } |
| if (cursor.Current().IsInlineLeaf()) |
| last_leaf = cursor; |
| } |
| *this = last_leaf; |
| } |
| |
| void NGInlineCursor::MoveToNextInlineLeaf() { |
| if (Current() && Current().IsInlineLeaf()) |
| MoveToNext(); |
| while (Current() && !Current().IsInlineLeaf()) |
| MoveToNext(); |
| } |
| |
| void NGInlineCursor::MoveToNextInlineLeafIgnoringLineBreak() { |
| do { |
| MoveToNextInlineLeaf(); |
| } while (Current() && Current().IsLineBreak()); |
| } |
| |
| void NGInlineCursor::MoveToNextInlineLeafOnLine() { |
| MoveToLastForSameLayoutObject(); |
| if (IsNull()) |
| return; |
| NGInlineCursor last_item = *this; |
| MoveToContainingLine(); |
| NGInlineCursor cursor = CursorForDescendants(); |
| cursor.MoveTo(last_item); |
| // Note: AX requires this for AccessibilityLayoutTest.NextOnLine. |
| if (!cursor.Current().IsInlineLeaf()) |
| cursor.MoveToNextInlineLeaf(); |
| cursor.MoveToNextInlineLeaf(); |
| MoveTo(cursor); |
| } |
| |
| void NGInlineCursor::MoveToNextLine() { |
| DCHECK(Current().IsLineBox()); |
| if (current_.item_) { |
| do { |
| MoveToNext(); |
| } while (Current() && !Current().IsLineBox()); |
| return; |
| } |
| NOTREACHED(); |
| } |
| |
| void NGInlineCursor::MoveToPreviousInlineLeaf() { |
| if (Current() && Current().IsInlineLeaf()) |
| MoveToPrevious(); |
| while (Current() && !Current().IsInlineLeaf()) |
| MoveToPrevious(); |
| } |
| |
| void NGInlineCursor::MoveToPreviousInlineLeafIgnoringLineBreak() { |
| do { |
| MoveToPreviousInlineLeaf(); |
| } while (Current() && Current().IsLineBreak()); |
| } |
| |
| void NGInlineCursor::MoveToPreviousInlineLeafOnLine() { |
| if (IsNull()) |
| return; |
| NGInlineCursor first_item = *this; |
| MoveToContainingLine(); |
| NGInlineCursor cursor = CursorForDescendants(); |
| cursor.MoveTo(first_item); |
| // Note: AX requires this for AccessibilityLayoutTest.NextOnLine. |
| if (!cursor.Current().IsInlineLeaf()) |
| cursor.MoveToPreviousInlineLeaf(); |
| cursor.MoveToPreviousInlineLeaf(); |
| MoveTo(cursor); |
| } |
| |
| void NGInlineCursor::MoveToPreviousLine() { |
| // Note: List marker is sibling of line box. |
| DCHECK(Current().IsLineBox()); |
| if (current_.item_) { |
| do { |
| MoveToPrevious(); |
| } while (Current() && !Current().IsLineBox()); |
| return; |
| } |
| NOTREACHED(); |
| } |
| |
| bool NGInlineCursor::TryToMoveToFirstChild() { |
| if (!Current().HasChildren()) |
| return false; |
| MoveToItem(current_.item_iter_ + 1); |
| return true; |
| } |
| |
| bool NGInlineCursor::TryToMoveToFirstInlineLeafChild() { |
| while (IsNotNull()) { |
| if (Current().IsInlineLeaf()) |
| return true; |
| MoveToNext(); |
| } |
| return false; |
| } |
| |
| bool NGInlineCursor::TryToMoveToLastChild() { |
| if (!Current().HasChildren()) |
| return false; |
| const auto end = current_.item_iter_ + CurrentItem()->DescendantsCount(); |
| MoveToNext(); // Move to the first child. |
| DCHECK(!IsNull()); |
| while (true) { |
| ItemsSpan::iterator previous = Current().item_iter_; |
| DCHECK(previous < end); |
| MoveToNextSkippingChildren(); |
| if (!Current() || Current().item_iter_ == end) { |
| MoveToItem(previous); |
| break; |
| } |
| } |
| return true; |
| } |
| |
| void NGInlineCursor::MoveToNext() { |
| DCHECK(HasRoot()); |
| if (UNLIKELY(!current_.item_)) |
| return; |
| DCHECK(current_.item_iter_ != items_.end()); |
| if (++current_.item_iter_ != items_.end()) { |
| current_.item_ = &*current_.item_iter_; |
| return; |
| } |
| MakeNull(); |
| } |
| |
| void NGInlineCursor::MoveToNextSkippingChildren() { |
| DCHECK(HasRoot()); |
| if (UNLIKELY(!current_.item_)) |
| return; |
| // If the current item has |DescendantsCount|, add it to move to the next |
| // sibling, skipping all children and their descendants. |
| if (wtf_size_t descendants_count = current_.item_->DescendantsCount()) |
| return MoveToItem(current_.item_iter_ + descendants_count); |
| return MoveToNext(); |
| } |
| |
| void NGInlineCursor::MoveToPrevious() { |
| DCHECK(HasRoot()); |
| if (UNLIKELY(!current_.item_)) |
| return; |
| if (current_.item_iter_ == items_.begin()) |
| return MakeNull(); |
| --current_.item_iter_; |
| current_.item_ = &*current_.item_iter_; |
| } |
| |
| void NGInlineCursor::MoveToFirstIncludingFragmentainer() { |
| if (!fragment_index_) { |
| MoveToFirst(); |
| return; |
| } |
| |
| ResetFragmentIndex(); |
| if (!TrySetRootFragmentItems()) |
| MakeNull(); |
| } |
| |
| void NGInlineCursor::MoveToNextFragmentainer() { |
| DCHECK(CanMoveAcrossFragmentainer()); |
| if (fragment_index_ < max_fragment_index_) { |
| AdvanceFragmentIndex(); |
| if (TrySetRootFragmentItems()) |
| return; |
| } |
| MakeNull(); |
| } |
| |
| void NGInlineCursor::MoveToNextIncludingFragmentainer() { |
| MoveToNext(); |
| if (!Current() && max_fragment_index_ && CanMoveAcrossFragmentainer()) |
| MoveToNextFragmentainer(); |
| } |
| |
| void NGInlineCursor::SlowMoveToForIfNeeded(const LayoutObject& layout_object) { |
| while (Current() && Current().GetLayoutObject() != &layout_object) |
| MoveToNextIncludingFragmentainer(); |
| } |
| |
| void NGInlineCursor::SlowMoveToFirstFor(const LayoutObject& layout_object) { |
| MoveToFirstIncludingFragmentainer(); |
| SlowMoveToForIfNeeded(layout_object); |
| } |
| |
| void NGInlineCursor::SlowMoveToNextForSameLayoutObject( |
| const LayoutObject& layout_object) { |
| MoveToNextIncludingFragmentainer(); |
| SlowMoveToForIfNeeded(layout_object); |
| } |
| |
| void NGInlineCursor::MoveTo(const LayoutObject& layout_object) { |
| DCHECK(layout_object.IsInLayoutNGInlineFormattingContext()); |
| if (UNLIKELY(layout_object.IsOutOfFlowPositioned())) { |
| NOTREACHED(); |
| MakeNull(); |
| return; |
| } |
| |
| // If this cursor is rootless, find the root of the inline formatting context. |
| bool is_descendants_cursor = false; |
| if (!HasRoot()) { |
| const LayoutBlockFlow* root = layout_object.FragmentItemsContainer(); |
| DCHECK(root); |
| SetRoot(*root); |
| if (UNLIKELY(!HasRoot())) { |
| MakeNull(); |
| return; |
| } |
| DCHECK(!IsDescendantsCursor()); |
| } else { |
| is_descendants_cursor = IsDescendantsCursor(); |
| } |
| |
| wtf_size_t item_index = layout_object.FirstInlineFragmentItemIndex(); |
| if (UNLIKELY(!item_index)) { |
| #if DCHECK_IS_ON() |
| const LayoutBlockFlow* root = layout_object.FragmentItemsContainer(); |
| NGInlineCursor check_cursor(*root); |
| check_cursor.SlowMoveToFirstFor(layout_object); |
| DCHECK(!check_cursor); |
| #endif |
| MakeNull(); |
| return; |
| } |
| // |FirstInlineFragmentItemIndex| is 1-based. Convert to 0-based index. |
| --item_index; |
| |
| // Find |NGFragmentItems| that contains |item_index|. |
| DCHECK_EQ(is_descendants_cursor, IsDescendantsCursor()); |
| if (root_block_flow_) { |
| DCHECK(!is_descendants_cursor); |
| while (item_index >= fragment_items_->EndItemIndex()) { |
| MoveToNextFragmentainer(); |
| if (!Current()) { |
| NOTREACHED(); |
| return; |
| } |
| } |
| item_index -= fragment_items_->SizeOfEarlierFragments(); |
| #if DCHECK_IS_ON() |
| NGInlineCursor check_cursor(*root_block_flow_); |
| check_cursor.SlowMoveToFirstFor(layout_object); |
| DCHECK_EQ(check_cursor.Current().Item(), |
| &fragment_items_->Items()[item_index]); |
| #endif |
| } else { |
| // If |this| is not rooted at |LayoutBlockFlow|, iterate |NGFragmentItems| |
| // from |LayoutBlockFlow|. |
| if (fragment_items_->HasItemIndex(item_index)) { |
| item_index -= fragment_items_->SizeOfEarlierFragments(); |
| } else { |
| NGInlineCursor cursor; |
| for (cursor.MoveTo(layout_object);; |
| cursor.MoveToNextForSameLayoutObject()) { |
| if (!cursor || cursor.fragment_items_->SizeOfEarlierFragments() > |
| fragment_items_->SizeOfEarlierFragments()) { |
| MakeNull(); |
| return; |
| } |
| if (cursor.fragment_items_ == fragment_items_) { |
| item_index = |
| cursor.Current().Item() - fragment_items_->Items().data(); |
| break; |
| } |
| } |
| } |
| #if DCHECK_IS_ON() |
| const LayoutBlockFlow* root = layout_object.FragmentItemsContainer(); |
| NGInlineCursor check_cursor(*root); |
| check_cursor.SlowMoveToFirstFor(layout_object); |
| while (check_cursor && fragment_items_ != check_cursor.fragment_items_) |
| check_cursor.SlowMoveToNextForSameLayoutObject(layout_object); |
| DCHECK_EQ(check_cursor.Current().Item(), |
| &fragment_items_->Items()[item_index]); |
| #endif |
| |
| // Skip items before |items_|, in case |this| is part of IFC. |
| if (UNLIKELY(is_descendants_cursor)) { |
| const wtf_size_t span_begin_item_index = SpanBeginItemIndex(); |
| while (UNLIKELY(item_index < span_begin_item_index)) { |
| const NGFragmentItem& item = fragment_items_->Items()[item_index]; |
| const wtf_size_t next_delta = item.DeltaToNextForSameLayoutObject(); |
| if (!next_delta) { |
| MakeNull(); |
| return; |
| } |
| item_index += next_delta; |
| } |
| if (UNLIKELY(item_index >= span_begin_item_index + items_.size())) { |
| MakeNull(); |
| return; |
| } |
| item_index -= span_begin_item_index; |
| } |
| } |
| |
| DCHECK_LT(item_index, items_.size()); |
| current_.Set(items_.begin() + item_index); |
| } |
| |
| void NGInlineCursor::MoveToNextForSameLayoutObjectExceptCulledInline() { |
| if (!Current()) |
| return; |
| if (wtf_size_t delta = current_.item_->DeltaToNextForSameLayoutObject()) { |
| while (true) { |
| // Return if the next index is in the current range. |
| const wtf_size_t delta_to_end = items_.end() - current_.item_iter_; |
| if (delta < delta_to_end) { |
| MoveToItem(current_.item_iter_ + delta); |
| return; |
| } |
| |
| // |this| is |IsDescendantsCursor| and the next item is out of the |
| // specified range, or the next item is in following fragmentainers. |
| if (!CanMoveAcrossFragmentainer()) |
| break; |
| |
| MoveToNextFragmentainer(); |
| if (!Current()) { |
| NOTREACHED(); |
| break; |
| } |
| DCHECK_GE(delta, delta_to_end); |
| delta -= delta_to_end; |
| } |
| } |
| MakeNull(); |
| } |
| |
| void NGInlineCursor::MoveToLastForSameLayoutObject() { |
| if (!Current()) |
| return; |
| NGInlineCursorPosition last; |
| do { |
| last = Current(); |
| MoveToNextForSameLayoutObject(); |
| } while (Current()); |
| MoveTo(last); |
| } |
| |
| // |
| // Functions to enumerate fragments that contribute to a culled inline. |
| // |
| |
| // Traverse the |LayoutObject| tree in pre-order DFS and find a |LayoutObject| |
| // that contributes to the culled inline. |
| const LayoutObject* NGInlineCursor::CulledInlineTraversal::Find( |
| const LayoutObject* child) const { |
| while (child) { |
| if (child->IsText()) |
| return child; |
| |
| if (child->IsBox()) { |
| if (!child->IsFloatingOrOutOfFlowPositioned()) |
| return child; |
| child = child->NextInPreOrderAfterChildren(layout_inline_); |
| continue; |
| } |
| |
| if (const auto* child_layout_inline = DynamicTo<LayoutInline>(child)) { |
| if (child_layout_inline->ShouldCreateBoxFragment()) |
| return child; |
| |
| // A culled inline can be computed from its direct children, but when the |
| // child is also culled, traverse its grand children. |
| if (const LayoutObject* grand_child = child_layout_inline->FirstChild()) { |
| child = grand_child; |
| continue; |
| } |
| } |
| |
| child = child->NextInPreOrderAfterChildren(layout_inline_); |
| } |
| return nullptr; |
| } |
| |
| void NGInlineCursor::CulledInlineTraversal::SetUseFragmentTree( |
| const LayoutInline& layout_inline) { |
| layout_inline_ = &layout_inline; |
| use_fragment_tree_ = true; |
| } |
| |
| const LayoutObject* NGInlineCursor::CulledInlineTraversal::MoveToFirstFor( |
| const LayoutInline& layout_inline) { |
| layout_inline_ = &layout_inline; |
| use_fragment_tree_ = false; |
| current_object_ = Find(layout_inline.FirstChild()); |
| return current_object_; |
| } |
| |
| const LayoutObject* NGInlineCursor::CulledInlineTraversal::MoveToNext() { |
| if (!current_object_) |
| return nullptr; |
| current_object_ = |
| Find(current_object_->NextInPreOrderAfterChildren(layout_inline_)); |
| return current_object_; |
| } |
| |
| void NGInlineCursor::MoveToFirstForCulledInline( |
| const LayoutInline& layout_inline) { |
| // When |this| is a descendant cursor, |this| may be limited to a very small |
| // subset of the |LayoutObject| descendants, and that traversing |
| // |LayoutObject| descendants is much more expensive. Prefer checking every |
| // fragment in that case. |
| if (IsDescendantsCursor()) { |
| culled_inline_.SetUseFragmentTree(layout_inline); |
| DCHECK(!CanMoveAcrossFragmentainer()); |
| MoveToFirst(); |
| while (Current() && !Current().IsPartOfCulledInlineBox(layout_inline)) |
| MoveToNext(); |
| return; |
| } |
| |
| if (const LayoutObject* layout_object = |
| culled_inline_.MoveToFirstFor(layout_inline)) { |
| MoveTo(*layout_object); |
| // This |MoveTo| may fail if |this| is a descendant cursor. Try the next |
| // |LayoutObject|. |
| MoveToNextCulledInlineDescendantIfNeeded(); |
| } |
| } |
| |
| void NGInlineCursor::MoveToNextForCulledInline() { |
| DCHECK(culled_inline_); |
| if (culled_inline_.UseFragmentTree()) { |
| const LayoutInline* layout_inline = culled_inline_.GetLayoutInline(); |
| DCHECK(layout_inline); |
| DCHECK(!CanMoveAcrossFragmentainer()); |
| do { |
| MoveToNext(); |
| } while (Current() && !Current().IsPartOfCulledInlineBox(*layout_inline)); |
| return; |
| } |
| |
| MoveToNextForSameLayoutObjectExceptCulledInline(); |
| // If we're at the end of fragments for the current |LayoutObject| that |
| // contributes to the current culled inline, find the next |LayoutObject|. |
| MoveToNextCulledInlineDescendantIfNeeded(); |
| } |
| |
| void NGInlineCursor::MoveToNextCulledInlineDescendantIfNeeded() { |
| DCHECK(culled_inline_); |
| if (Current()) |
| return; |
| |
| while (const LayoutObject* layout_object = culled_inline_.MoveToNext()) { |
| MoveTo(*layout_object); |
| if (Current()) |
| return; |
| } |
| } |
| |
| void NGInlineCursor::ResetFragmentIndex() { |
| fragment_index_ = 0; |
| previously_consumed_block_size_ = LayoutUnit(); |
| } |
| |
| void NGInlineCursor::AdvanceFragmentIndex() { |
| fragment_index_++; |
| if (!root_box_fragment_) |
| return; |
| if (const auto* break_token = |
| To<NGBlockBreakToken>(root_box_fragment_->BreakToken())) |
| previously_consumed_block_size_ = break_token->ConsumedBlockSize(); |
| } |
| |
| void NGInlineCursor::MoveToIncludingCulledInline( |
| const LayoutObject& layout_object) { |
| DCHECK(layout_object.IsInLayoutNGInlineFormattingContext()) << layout_object; |
| |
| culled_inline_.Reset(); |
| MoveTo(layout_object); |
| if (Current() || !HasRoot()) |
| return; |
| |
| // If this is a culled inline, find fragments for descendant |LayoutObject|s |
| // that contribute to the culled inline. |
| if (const auto* layout_inline = DynamicTo<LayoutInline>(layout_object)) { |
| if (!layout_inline->ShouldCreateBoxFragment()) |
| MoveToFirstForCulledInline(*layout_inline); |
| } |
| } |
| |
| void NGInlineCursor::MoveToNextForSameLayoutObject() { |
| if (UNLIKELY(culled_inline_)) { |
| MoveToNextForCulledInline(); |
| return; |
| } |
| MoveToNextForSameLayoutObjectExceptCulledInline(); |
| } |
| |
| // |
| // |NGInlineBackwardCursor| functions. |
| // |
| NGInlineBackwardCursor::NGInlineBackwardCursor(const NGInlineCursor& cursor) |
| : cursor_(cursor) { |
| if (cursor.HasRoot()) { |
| DCHECK(!cursor || cursor.items_.begin() == cursor.Current().item_iter_); |
| for (NGInlineCursor sibling(cursor); sibling; |
| sibling.MoveToNextSkippingChildren()) |
| sibling_item_iterators_.push_back(sibling.Current().item_iter_); |
| current_index_ = sibling_item_iterators_.size(); |
| if (current_index_) |
| current_.Set(sibling_item_iterators_[--current_index_]); |
| return; |
| } |
| DCHECK(!cursor); |
| } |
| |
| NGInlineCursor NGInlineBackwardCursor::CursorForDescendants() const { |
| if (current_.item_) { |
| NGInlineCursor cursor(cursor_); |
| cursor.MoveToItem(sibling_item_iterators_[current_index_]); |
| return cursor.CursorForDescendants(); |
| } |
| NOTREACHED(); |
| return NGInlineCursor(); |
| } |
| |
| void NGInlineBackwardCursor::MoveToPreviousSibling() { |
| if (current_index_) { |
| if (current_.item_) { |
| current_.Set(sibling_item_iterators_[--current_index_]); |
| return; |
| } |
| NOTREACHED(); |
| } |
| current_.Clear(); |
| } |
| |
| std::ostream& operator<<(std::ostream& ostream, const NGInlineCursor& cursor) { |
| if (!cursor) |
| return ostream << "NGInlineCursor()"; |
| DCHECK(cursor.HasRoot()); |
| return ostream << "NGInlineCursor(" << *cursor.CurrentItem() << ")"; |
| } |
| |
| std::ostream& operator<<(std::ostream& ostream, const NGInlineCursor* cursor) { |
| if (!cursor) |
| return ostream << "<null>"; |
| return ostream << *cursor; |
| } |
| |
| #if DCHECK_IS_ON() |
| void NGInlineCursor::CheckValid(const NGInlineCursorPosition& position) const { |
| if (position.Item()) { |
| DCHECK(HasRoot()); |
| DCHECK_EQ(position.item_, &*position.item_iter_); |
| const unsigned index = position.item_iter_ - items_.begin(); |
| DCHECK_LT(index, items_.size()); |
| } |
| } |
| #endif |
| |
| } // namespace blink |