blob: bc9cd358664533aa43548d1d57bc12eee8353927 [file] [log] [blame]
/*
* Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// Copyright 2017 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/editing/selection_modifier.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/inline_box_position.h"
#include "third_party/blink/renderer/core/editing/visible_position.h"
#include "third_party/blink/renderer/core/editing/visible_units.h"
#include "third_party/blink/renderer/core/layout/api/line_layout_api_shim.h"
#include "third_party/blink/renderer/core/layout/api/line_layout_block_flow.h"
#include "third_party/blink/renderer/core/layout/geometry/logical_rect.h"
#include "third_party/blink/renderer/core/layout/line/root_inline_box.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_line_utils.h"
namespace blink {
namespace {
// Abstracts similarities between RootInlineBox and NGPhysicalLineBoxFragment
class AbstractLineBox {
STACK_ALLOCATED();
public:
AbstractLineBox() = default;
static AbstractLineBox CreateFor(const PositionInFlatTreeWithAffinity&);
bool IsNull() const { return type_ == Type::kNull; }
bool CanBeCaretContainer() const {
DCHECK(!IsNull());
// We want to skip zero height boxes.
// This could happen in case it is a TrailingFloatsRootInlineBox.
if (IsOldLayout()) {
return GetRootInlineBox().LogicalHeight() &&
GetRootInlineBox().FirstLeafChild();
}
if (cursor_.Current().IsEmptyLineBox())
return false;
const PhysicalSize physical_size = cursor_.Current().Size();
const LogicalSize logical_size = physical_size.ConvertToLogical(
cursor_.Current().Style().GetWritingMode());
if (!logical_size.block_size)
return false;
for (NGInlineCursor cursor(cursor_); cursor; cursor.MoveToNext()) {
const NGInlineCursorPosition& current = cursor.Current();
if (current.GetLayoutObject() && current.IsInlineLeaf())
return true;
}
return false;
}
AbstractLineBox PreviousLine() const {
DCHECK(!IsNull());
if (IsOldLayout()) {
const RootInlineBox* previous_root = GetRootInlineBox().PrevRootBox();
return previous_root ? AbstractLineBox(*previous_root)
: AbstractLineBox();
}
NGInlineCursor previous_line = cursor_;
previous_line.MoveToPreviousLine();
return previous_line ? AbstractLineBox(previous_line) : AbstractLineBox();
}
AbstractLineBox NextLine() const {
DCHECK(!IsNull());
if (IsOldLayout()) {
const RootInlineBox* next_root = GetRootInlineBox().NextRootBox();
return next_root ? AbstractLineBox(*next_root) : AbstractLineBox();
}
NGInlineCursor next_line = cursor_;
next_line.MoveToNextLine();
return next_line ? AbstractLineBox(next_line) : AbstractLineBox();
}
PhysicalOffset AbsoluteLineDirectionPointToLocalPointInBlock(
LayoutUnit line_direction_point) {
DCHECK(!IsNull());
const LayoutBlockFlow& containing_block = GetBlock();
// TODO(yosin): Is kIgnoreTransforms correct here?
PhysicalOffset absolute_block_point = containing_block.LocalToAbsolutePoint(
PhysicalOffset(), kIgnoreTransforms);
if (containing_block.IsScrollContainer()) {
absolute_block_point -=
PhysicalOffset(containing_block.ScrolledContentOffset());
}
if (containing_block.IsHorizontalWritingMode()) {
return PhysicalOffset(line_direction_point - absolute_block_point.left,
PhysicalBlockOffset());
}
return PhysicalOffset(PhysicalBlockOffset(),
line_direction_point - absolute_block_point.top);
}
PositionInFlatTreeWithAffinity PositionForPoint(
const PhysicalOffset& point_in_container,
bool only_editable_leaves) const {
if (IsOldLayout()) {
const LayoutObject* closest_leaf_child =
GetRootInlineBox().ClosestLeafChildForPoint(
GetBlock().FlipForWritingMode(point_in_container),
only_editable_leaves);
if (!closest_leaf_child)
return PositionInFlatTreeWithAffinity();
const Node* node = closest_leaf_child->GetNode();
if (node && EditingIgnoresContent(*node)) {
return PositionInFlatTreeWithAffinity(
PositionInFlatTree::InParentBeforeNode(*node));
}
return ToPositionInFlatTreeWithAffinity(
closest_leaf_child->PositionForPoint(point_in_container));
}
return PositionForPoint(cursor_, point_in_container, only_editable_leaves);
}
private:
explicit AbstractLineBox(const RootInlineBox& root_inline_box)
: root_inline_box_(&root_inline_box), type_(Type::kOldLayout) {}
explicit AbstractLineBox(const NGInlineCursor& cursor)
: cursor_(cursor), type_(Type::kLayoutNG) {
DCHECK(cursor_.Current().IsLineBox());
}
const LayoutBlockFlow& GetBlock() const {
DCHECK(!IsNull());
if (IsOldLayout()) {
return *To<LayoutBlockFlow>(
LineLayoutAPIShim::LayoutObjectFrom(GetRootInlineBox().Block()));
}
return *cursor_.GetLayoutBlockFlow();
}
LayoutUnit PhysicalBlockOffset() const {
DCHECK(!IsNull());
if (IsOldLayout()) {
return GetBlock().FlipForWritingMode(
GetRootInlineBox().BlockDirectionPointInLine());
}
const PhysicalOffset physical_offset =
cursor_.Current().OffsetInContainerFragment();
return cursor_.Current().Style().IsHorizontalWritingMode()
? physical_offset.top
: physical_offset.left;
}
bool IsOldLayout() const { return type_ == Type::kOldLayout; }
bool IsLayoutNG() const { return type_ == Type::kLayoutNG; }
const RootInlineBox& GetRootInlineBox() const {
DCHECK(IsOldLayout());
return *root_inline_box_;
}
static bool IsEditable(const NGInlineCursor& cursor) {
const LayoutObject* const layout_object =
cursor.Current().GetLayoutObject();
return layout_object && layout_object->GetNode() &&
HasEditableStyle(*layout_object->GetNode());
}
static PositionInFlatTreeWithAffinity PositionForPoint(
const NGInlineCursor& line,
const PhysicalOffset& point,
bool only_editable_leaves) {
DCHECK(line.Current().IsLineBox());
const PhysicalSize unit_square(LayoutUnit(1), LayoutUnit(1));
const LogicalOffset logical_point =
point.ConvertToLogical({line.Current().Style().GetWritingMode(),
line.Current().BaseDirection()},
line.Current().Size(), unit_square);
const LayoutUnit inline_offset = logical_point.inline_offset;
NGInlineCursor closest_leaf_child;
LayoutUnit closest_leaf_distance;
for (NGInlineCursor cursor = line.CursorForDescendants(); cursor;
cursor.MoveToNext()) {
if (!cursor.Current().GetLayoutObject())
continue;
if (!cursor.Current().IsInlineLeaf())
continue;
if (only_editable_leaves && !IsEditable(cursor)) {
// This condition allows us to move editable to editable with skipping
// non-editable element.
// [1] editing/selection/modify_move/move_backward_line_table.html
continue;
}
const LogicalRect fragment_logical_rect =
line.Current().ConvertChildToLogical(
cursor.Current().RectInContainerFragment());
const LayoutUnit inline_min = fragment_logical_rect.offset.inline_offset;
const LayoutUnit inline_max = fragment_logical_rect.offset.inline_offset +
fragment_logical_rect.size.inline_size;
if (inline_offset >= inline_min && inline_offset < inline_max) {
closest_leaf_child = cursor;
break;
}
const LayoutUnit distance =
inline_offset < inline_min
? inline_min - inline_offset
: inline_offset - inline_max + LayoutUnit(1);
if (!closest_leaf_child || distance < closest_leaf_distance) {
closest_leaf_child = cursor;
closest_leaf_distance = distance;
}
}
if (!closest_leaf_child)
return PositionInFlatTreeWithAffinity();
const Node* const node = closest_leaf_child.Current().GetNode();
if (!node)
return PositionInFlatTreeWithAffinity();
if (EditingIgnoresContent(*node)) {
return PositionInFlatTreeWithAffinity(
PositionInFlatTree::BeforeNode(*node));
}
return ToPositionInFlatTreeWithAffinity(
closest_leaf_child.PositionForPointInChild(point));
}
enum class Type { kNull, kOldLayout, kLayoutNG };
const RootInlineBox* root_inline_box_ = nullptr;
NGInlineCursor cursor_;
Type type_ = Type::kNull;
};
// static
AbstractLineBox AbstractLineBox::CreateFor(
const PositionInFlatTreeWithAffinity& position) {
if (position.IsNull() ||
!position.GetPosition().AnchorNode()->GetLayoutObject()) {
return AbstractLineBox();
}
const PositionWithAffinity adjusted =
ToPositionInDOMTreeWithAffinity(ComputeInlineAdjustedPosition(position));
if (adjusted.IsNull())
return AbstractLineBox();
const NGInlineCursor& line = NGContainingLineBoxOf(adjusted);
if (line)
return AbstractLineBox(line);
const InlineBox* box =
ComputeInlineBoxPositionForInlineAdjustedPosition(adjusted).inline_box;
if (!box)
return AbstractLineBox();
return AbstractLineBox(box->Root());
}
ContainerNode* HighestEditableRootOfNode(const Node& node) {
return HighestEditableRoot(FirstPositionInOrBeforeNode(node));
}
Node* PreviousNodeConsideringAtomicNodes(const Node& start) {
if (Node* previous_sibling = FlatTreeTraversal::PreviousSibling(start)) {
Node* node = previous_sibling;
while (!IsAtomicNodeInFlatTree(node)) {
if (Node* last_child = FlatTreeTraversal::LastChild(*node))
node = last_child;
}
return node;
}
return FlatTreeTraversal::Parent(start);
}
Node* NextNodeConsideringAtomicNodes(const Node& start) {
if (!IsAtomicNodeInFlatTree(&start) && FlatTreeTraversal::HasChildren(start))
return FlatTreeTraversal::FirstChild(start);
if (Node* next_sibling = FlatTreeTraversal::NextSibling(start))
return next_sibling;
const Node* node = &start;
while (node && !FlatTreeTraversal::NextSibling(*node))
node = FlatTreeTraversal::Parent(*node);
if (node)
return FlatTreeTraversal::NextSibling(*node);
return nullptr;
}
// Returns the previous leaf node or nullptr if there are no more. Delivers leaf
// nodes as if the whole DOM tree were a linear chain of its leaf nodes.
Node* PreviousAtomicLeafNode(const Node& start) {
Node* node = PreviousNodeConsideringAtomicNodes(start);
while (node) {
if (IsAtomicNodeInFlatTree(node))
return node;
node = PreviousNodeConsideringAtomicNodes(*node);
}
return nullptr;
}
// Returns the next leaf node or nullptr if there are no more. Delivers leaf
// nodes as if the whole DOM tree were a linear chain of its leaf nodes.
Node* NextAtomicLeafNode(const Node& start) {
Node* node = NextNodeConsideringAtomicNodes(start);
while (node) {
if (IsAtomicNodeInFlatTree(node))
return node;
node = NextNodeConsideringAtomicNodes(*node);
}
return nullptr;
}
Node* PreviousLeafWithSameEditability(const Node& node) {
const bool editable = HasEditableStyle(node);
for (Node* runner = PreviousAtomicLeafNode(node); runner;
runner = PreviousAtomicLeafNode(*runner)) {
if (editable == HasEditableStyle(*runner))
return runner;
}
return nullptr;
}
Node* NextLeafWithGivenEditability(Node* node, bool editable) {
if (!node)
return nullptr;
for (Node* runner = NextAtomicLeafNode(*node); runner;
runner = NextAtomicLeafNode(*runner)) {
if (editable == HasEditableStyle(*runner))
return runner;
}
return nullptr;
}
bool InSameLine(const Node& node,
const PositionInFlatTreeWithAffinity& position) {
if (!node.GetLayoutObject())
return true;
return InSameLine(CreateVisiblePosition(
PositionInFlatTree::FirstPositionInOrBeforeNode(node))
.ToPositionWithAffinity(),
position);
}
Node* FindNodeInPreviousLine(const Node& start_node,
const PositionInFlatTreeWithAffinity& position) {
for (Node* runner = PreviousLeafWithSameEditability(start_node); runner;
runner = PreviousLeafWithSameEditability(*runner)) {
if (!InSameLine(*runner, position))
return runner;
}
return nullptr;
}
// FIXME: consolidate with code in previousLinePosition.
PositionInFlatTree PreviousRootInlineBoxCandidatePosition(
Node* node,
const PositionInFlatTreeWithAffinity& position) {
ContainerNode* highest_root = HighestEditableRoot(position.GetPosition());
Node* const previous_node = FindNodeInPreviousLine(*node, position);
for (Node* runner = previous_node; runner && !runner->IsShadowRoot();
runner = PreviousLeafWithSameEditability(*runner)) {
if (HighestEditableRootOfNode(*runner) != highest_root)
break;
const PositionInFlatTree& candidate =
IsA<HTMLBRElement>(*runner) ? PositionInFlatTree::BeforeNode(*runner)
: PositionInFlatTree::EditingPositionOf(
runner, CaretMaxOffset(runner));
if (IsVisuallyEquivalentCandidate(candidate))
return candidate;
}
return PositionInFlatTree();
}
PositionInFlatTree NextRootInlineBoxCandidatePosition(
Node* node,
const PositionInFlatTreeWithAffinity& position) {
ContainerNode* highest_root = HighestEditableRoot(position.GetPosition());
// TODO(xiaochengh): We probably also need to pass in the starting editability
// to |PreviousLeafWithSameEditability|.
const bool is_editable =
HasEditableStyle(*position.GetPosition().ComputeContainerNode());
Node* next_node = NextLeafWithGivenEditability(node, is_editable);
while (next_node && InSameLine(*next_node, position)) {
next_node = NextLeafWithGivenEditability(next_node, is_editable);
}
for (Node* runner = next_node; runner && !runner->IsShadowRoot();
runner = NextLeafWithGivenEditability(runner, is_editable)) {
if (HighestEditableRootOfNode(*runner) != highest_root)
break;
const PositionInFlatTree& candidate =
PositionInFlatTree::EditingPositionOf(runner, CaretMinOffset(runner));
if (IsVisuallyEquivalentCandidate(candidate))
return candidate;
}
return PositionInFlatTree();
}
} // namespace
// static
PositionInFlatTreeWithAffinity SelectionModifier::PreviousLinePosition(
const PositionInFlatTreeWithAffinity& position,
LayoutUnit line_direction_point) {
// TODO(xiaochengh): Make all variables |const|.
PositionInFlatTree p = position.GetPosition();
Node* node = p.AnchorNode();
if (!node)
return PositionInFlatTreeWithAffinity();
LayoutObject* layout_object = node->GetLayoutObject();
if (!layout_object)
return PositionInFlatTreeWithAffinity();
AbstractLineBox line = AbstractLineBox::CreateFor(position);
if (!line.IsNull()) {
line = line.PreviousLine();
if (line.IsNull() || !line.CanBeCaretContainer())
line = AbstractLineBox();
}
if (line.IsNull()) {
PositionInFlatTree candidate =
PreviousRootInlineBoxCandidatePosition(node, position);
if (candidate.IsNotNull()) {
line = AbstractLineBox::CreateFor(
CreateVisiblePosition(candidate).ToPositionWithAffinity());
if (line.IsNull()) {
// TODO(editing-dev): Investigate if this is correct for null
// |CreateVisiblePosition(candidate)|.
return PositionInFlatTreeWithAffinity(candidate);
}
}
}
if (!line.IsNull()) {
// FIXME: Can be wrong for multi-column layout and with transforms.
PhysicalOffset point_in_line =
line.AbsoluteLineDirectionPointToLocalPointInBlock(
line_direction_point);
if (auto candidate =
line.PositionForPoint(point_in_line, IsEditablePosition(p))) {
// If the current position is inside an editable position, then the next
// shouldn't end up inside non-editable as that would cross the editing
// boundaries which would be an invalid selection.
if (IsEditablePosition(p) &&
!IsEditablePosition(candidate.GetPosition())) {
return AdjustBackwardPositionToAvoidCrossingEditingBoundaries(candidate,
p);
}
return candidate;
}
}
// Could not find a previous line. This means we must already be on the first
// line. Move to the start of the content in this block, which effectively
// moves us to the start of the line we're on.
Element* root_element = HasEditableStyle(*node)
? RootEditableElement(*node)
: node->GetDocument().documentElement();
if (!root_element)
return PositionInFlatTreeWithAffinity();
return PositionInFlatTreeWithAffinity(
PositionInFlatTree::FirstPositionInNode(*root_element));
}
// static
PositionInFlatTreeWithAffinity SelectionModifier::NextLinePosition(
const PositionInFlatTreeWithAffinity& position,
LayoutUnit line_direction_point) {
// TODO(xiaochengh): Make all variables |const|.
PositionInFlatTree p = position.GetPosition();
Node* node = p.AnchorNode();
if (!node)
return PositionInFlatTreeWithAffinity();
LayoutObject* layout_object = node->GetLayoutObject();
if (!layout_object)
return PositionInFlatTreeWithAffinity();
AbstractLineBox line = AbstractLineBox::CreateFor(position);
if (!line.IsNull()) {
line = line.NextLine();
if (line.IsNull() || !line.CanBeCaretContainer())
line = AbstractLineBox();
}
if (line.IsNull()) {
// FIXME: We need do the same in previousLinePosition.
Node* child = FlatTreeTraversal::ChildAt(*node, p.ComputeEditingOffset());
Node* search_start_node =
child ? child : &FlatTreeTraversal::LastWithinOrSelf(*node);
PositionInFlatTree candidate =
NextRootInlineBoxCandidatePosition(search_start_node, position);
if (candidate.IsNotNull()) {
line = AbstractLineBox::CreateFor(
CreateVisiblePosition(candidate).ToPositionWithAffinity());
if (line.IsNull()) {
// TODO(editing-dev): Investigate if this is correct for null
// |CreateVisiblePosition(candidate)|.
return PositionInFlatTreeWithAffinity(candidate);
}
}
}
if (!line.IsNull()) {
// FIXME: Can be wrong for multi-column layout and with transforms.
PhysicalOffset point_in_line =
line.AbsoluteLineDirectionPointToLocalPointInBlock(
line_direction_point);
if (auto candidate =
line.PositionForPoint(point_in_line, IsEditablePosition(p))) {
// If the current position is inside an editable position, then the next
// shouldn't end up inside non-editable as that would cross the editing
// boundaries which would be an invalid selection.
if (IsEditablePosition(p) &&
!IsEditablePosition(candidate.GetPosition())) {
return AdjustForwardPositionToAvoidCrossingEditingBoundaries(candidate,
p);
}
return candidate;
}
}
// Could not find a next line. This means we must already be on the last line.
// Move to the end of the content in this block, which effectively moves us
// to the end of the line we're on.
Element* root_element = HasEditableStyle(*node)
? RootEditableElement(*node)
: node->GetDocument().documentElement();
if (!root_element)
return PositionInFlatTreeWithAffinity();
return PositionInFlatTreeWithAffinity(
PositionInFlatTree::LastPositionInNode(*root_element));
}
} // namespace blink