From 21d75757169917b8ec6474bbd4668c45719fe3c9 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 29 Jan 2016 21:11:37 -0800 Subject: [PATCH] Add Paragraph#getPositionForOffset We'll use this function to position the caret when the user taps a text input control. Very little of the code in this patch is actually new. Most of it is restoring code that we previously removed from the engine. I've made some small changes to the restored code to handle the lack of a DOM. The only major change is to RenderObject::createPositionWithAffinity, which now just returns the values it captures instead of trying to compute a DOM position. TextAffinity and TextPosition are lifted from package:flutter. Once this patch rolls into package:flutter, I'll remove the declarations there. --- sky/engine/core/core.gni | 2 + sky/engine/core/dart/text.dart | 48 +++++- .../core/editing/PositionWithAffinity.cpp | 20 +++ .../core/editing/PositionWithAffinity.h | 44 +++++ sky/engine/core/rendering/RenderBlock.cpp | 106 ++++++++++++ sky/engine/core/rendering/RenderBlock.h | 4 + sky/engine/core/rendering/RenderBox.cpp | 67 +++++++ sky/engine/core/rendering/RenderBox.h | 2 + sky/engine/core/rendering/RenderInline.cpp | 17 ++ sky/engine/core/rendering/RenderInline.h | 2 + sky/engine/core/rendering/RenderObject.cpp | 10 ++ sky/engine/core/rendering/RenderObject.h | 5 + sky/engine/core/rendering/RenderReplaced.cpp | 20 +++ sky/engine/core/rendering/RenderReplaced.h | 2 + sky/engine/core/rendering/RenderText.cpp | 163 ++++++++++++++++++ sky/engine/core/rendering/RenderText.h | 2 + sky/engine/core/text/Paragraph.cpp | 25 +++ sky/engine/core/text/Paragraph.h | 3 + 18 files changed, 541 insertions(+), 1 deletion(-) create mode 100644 sky/engine/core/editing/PositionWithAffinity.cpp create mode 100644 sky/engine/core/editing/PositionWithAffinity.h diff --git a/sky/engine/core/core.gni b/sky/engine/core/core.gni index 5b01d018797..1a4d5063c24 100644 --- a/sky/engine/core/core.gni +++ b/sky/engine/core/core.gni @@ -14,6 +14,8 @@ sky_core_files = [ "editing/CompositionUnderline.h", "editing/CompositionUnderlineRangeFilter.cpp", "editing/CompositionUnderlineRangeFilter.h", + "editing/PositionWithAffinity.cpp", + "editing/PositionWithAffinity.h", "painting/Canvas.cpp", "painting/Canvas.h", "painting/CanvasColor.cpp", diff --git a/sky/engine/core/dart/text.dart b/sky/engine/core/dart/text.dart index 0354ada25b9..7b8667a2758 100644 --- a/sky/engine/core/dart/text.dart +++ b/sky/engine/core/dart/text.dart @@ -474,7 +474,44 @@ class TextBox { int get hashCode => hashValues(left, top, right, bottom, direction); - String toString() => "TextBox.fromLTRBD(${left.toStringAsFixed(1)}, ${top.toStringAsFixed(1)}, ${right.toStringAsFixed(1)}, ${bottom.toStringAsFixed(1)}, $direction)"; + String toString() => 'TextBox.fromLTRBD(${left.toStringAsFixed(1)}, ${top.toStringAsFixed(1)}, ${right.toStringAsFixed(1)}, ${bottom.toStringAsFixed(1)}, $direction)'; +} + +/// Whether a [TextPosition] is visually upstream or downstream of its offset. +/// +/// For example, when a text position exists at a line break, a single offset has +/// two visual positions, one prior to the line break (at the end of the first +/// line) and one after the line break (at the start of the second line). A text +/// affinity disambiguates between those cases. (Something similar happens with +/// between runs of bidirectional text.) +enum TextAffinity { + /// The position has affinity for the upstream side of the text position. + /// + /// For example, if the offset of the text position is a line break, the + /// position represents the end of the first line. + upstream, + + /// The position has affinity for the downstream side of the text position. + /// + /// For example, if the offset of the text position is a line break, the + /// position represents the start of the second line. + downstream +} + +/// A visual position in a string of text. +class TextPosition { + const TextPosition({ this.offset, this.affinity: TextAffinity.downstream }); + + /// The index of the character just prior to the position. + final int offset; + + /// If the offset has more than one visual location (e.g., occurs at a line + /// break), which of the two locations is represented by this position. + final TextAffinity affinity; + + String toString() { + return '$runtimeType(offset: $offset, affinity: $affinity)'; + } } abstract class Paragraph extends NativeFieldWrapperClass2 { @@ -496,7 +533,16 @@ abstract class Paragraph extends NativeFieldWrapperClass2 { void layout() native "Paragraph_layout"; void paint(Canvas canvas, Offset offset) native "Paragraph_paint"; + /// Returns a list of text boxes that enclose the given text range. List getBoxesForRange(int start, int end) native "Paragraph_getRectsForRange"; + + List _getPositionForOffset(Offset offset) native "Paragraph_getPositionForOffset"; + + /// Returns the text position closest to the given offset. + TextPosition getPositionForOffset(Offset offset) { + List encoded = _getPositionForOffset(offset); + return new TextPosition(offset: encoded[0], affinity: TextAffinity.values[encoded[1]]); + } } class ParagraphBuilder extends NativeFieldWrapperClass2 { diff --git a/sky/engine/core/editing/PositionWithAffinity.cpp b/sky/engine/core/editing/PositionWithAffinity.cpp new file mode 100644 index 00000000000..3df5f74ef12 --- /dev/null +++ b/sky/engine/core/editing/PositionWithAffinity.cpp @@ -0,0 +1,20 @@ +// Copyright 2014 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 "sky/engine/core/editing/PositionWithAffinity.h" + +namespace blink { + +PositionWithAffinity::PositionWithAffinity(RenderObject* renderer, int offset, EAffinity affinity) + : m_renderer(renderer) + , m_offset(offset) + , m_affinity(affinity) +{ +} + +PositionWithAffinity::~PositionWithAffinity() +{ +} + +} // namespace blink diff --git a/sky/engine/core/editing/PositionWithAffinity.h b/sky/engine/core/editing/PositionWithAffinity.h new file mode 100644 index 00000000000..2d7ca88b97e --- /dev/null +++ b/sky/engine/core/editing/PositionWithAffinity.h @@ -0,0 +1,44 @@ +// Copyright 2014 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. + +#ifndef SKY_ENGINE_CORE_EDITING_POSITIONWITHAFFINITY_H_ +#define SKY_ENGINE_CORE_EDITING_POSITIONWITHAFFINITY_H_ + +namespace blink { + +class RenderObject; + +enum EAffinity { UPSTREAM, DOWNSTREAM }; + +// VisiblePosition default affinity is downstream because +// the callers do not really care (they just want the +// deep position without regard to line position), and this +// is cheaper than UPSTREAM +#define VP_DEFAULT_AFFINITY DOWNSTREAM + +// Callers who do not know where on the line the position is, +// but would like UPSTREAM if at a line break or DOWNSTREAM +// otherwise, need a clear way to specify that. The +// constructors auto-correct UPSTREAM to DOWNSTREAM if the +// position is not at a line break. +#define VP_UPSTREAM_IF_POSSIBLE UPSTREAM + +class PositionWithAffinity { +public: + PositionWithAffinity(RenderObject* renderer, int offset, EAffinity = DOWNSTREAM); + ~PositionWithAffinity(); + + RenderObject* renderer() const { return m_renderer; } + int offset() const { return m_offset; } + EAffinity affinity() const { return m_affinity; } + +private: + RenderObject* m_renderer; + int m_offset; + EAffinity m_affinity; +}; + +} // namespace blink + +#endif // SKY_ENGINE_CORE_EDITING_POSITIONWITHAFFINITY_H_ diff --git a/sky/engine/core/rendering/RenderBlock.cpp b/sky/engine/core/rendering/RenderBlock.cpp index 4171734abcc..aa50e263513 100644 --- a/sky/engine/core/rendering/RenderBlock.cpp +++ b/sky/engine/core/rendering/RenderBlock.cpp @@ -952,6 +952,112 @@ bool RenderBlock::hitTestContents(const HitTestRequest& request, HitTestResult& return false; } +static PositionWithAffinity positionForPointInChild(RenderBox* child, const LayoutPoint& pointInParentCoordinates) +{ + LayoutPoint childLocation = child->location(); + + // FIXME: This is wrong if the child's writing-mode is different from the parent's. + LayoutPoint pointInChildCoordinates(toLayoutPoint(pointInParentCoordinates - childLocation)); + return child->positionForPoint(pointInChildCoordinates); +} + +PositionWithAffinity RenderBlock::positionForPointWithInlineChildren(const LayoutPoint& pointInLogicalContents) +{ + ASSERT(isRenderParagraph()); + + if (!firstRootBox()) + return createPositionWithAffinity(0, DOWNSTREAM); + + // look for the closest line box in the root box which is at the passed-in y coordinate + InlineBox* closestBox = 0; + RootInlineBox* firstRootBoxWithChildren = 0; + RootInlineBox* lastRootBoxWithChildren = 0; + for (RootInlineBox* root = firstRootBox(); root; root = root->nextRootBox()) { + if (!root->firstLeafChild()) + continue; + if (!firstRootBoxWithChildren) + firstRootBoxWithChildren = root; + + lastRootBoxWithChildren = root; + + // check if this root line box is located at this y coordinate + if (pointInLogicalContents.y() < root->selectionBottom()) { + closestBox = root->closestLeafChildForLogicalLeftPosition(pointInLogicalContents.x()); + if (closestBox) + break; + } + } + + if (!closestBox && lastRootBoxWithChildren) { + // y coordinate is below last root line box, pretend we hit it + closestBox = lastRootBoxWithChildren->closestLeafChildForLogicalLeftPosition(pointInLogicalContents.x()); + } + + if (closestBox) { + // pass the box a top position that is inside it + LayoutPoint point(pointInLogicalContents.x(), closestBox->root().blockDirectionPointInLine()); + if (closestBox->renderer().isReplaced()) + return positionForPointInChild(&toRenderBox(closestBox->renderer()), point); + return closestBox->renderer().positionForPoint(point); + } + + // Can't reach this. We have a root line box, but it has no kids. + // FIXME: This should ASSERT_NOT_REACHED(), but clicking on placeholder text + // seems to hit this code path. + return createPositionWithAffinity(0, DOWNSTREAM); +} + +static inline bool isChildHitTestCandidate(RenderBox* box) +{ + return box->height() && !box->isFloatingOrOutOfFlowPositioned(); +} + +PositionWithAffinity RenderBlock::positionForPoint(const LayoutPoint& point) +{ + if (isReplaced()) { + // FIXME: This seems wrong when the object's writing-mode doesn't match the line's writing-mode. + LayoutUnit pointLogicalLeft = point.x(); + LayoutUnit pointLogicalTop = point.y(); + + if (pointLogicalLeft < 0) + return createPositionWithAffinity(caretMinOffset(), DOWNSTREAM); + if (pointLogicalLeft >= logicalWidth()) + return createPositionWithAffinity(caretMaxOffset(), DOWNSTREAM); + if (pointLogicalTop < 0) + return createPositionWithAffinity(caretMinOffset(), DOWNSTREAM); + if (pointLogicalTop >= logicalHeight()) + return createPositionWithAffinity(caretMaxOffset(), DOWNSTREAM); + } + + LayoutPoint pointInContents = point; + LayoutPoint pointInLogicalContents(pointInContents); + + if (isRenderParagraph()) + return positionForPointWithInlineChildren(pointInLogicalContents); + + RenderBox* lastCandidateBox = lastChildBox(); + while (lastCandidateBox && !isChildHitTestCandidate(lastCandidateBox)) + lastCandidateBox = lastCandidateBox->previousSiblingBox(); + + if (lastCandidateBox) { + if (pointInLogicalContents.y() > logicalTopForChild(lastCandidateBox) + || (pointInLogicalContents.y() == logicalTopForChild(lastCandidateBox))) + return positionForPointInChild(lastCandidateBox, pointInContents); + + for (RenderBox* childBox = firstChildBox(); childBox; childBox = childBox->nextSiblingBox()) { + if (!isChildHitTestCandidate(childBox)) + continue; + LayoutUnit childLogicalBottom = logicalTopForChild(childBox) + logicalHeightForChild(childBox); + // We hit child if our click is above the bottom of its padding box (like IE6/7 and FF3). + if (isChildHitTestCandidate(childBox) && (pointInLogicalContents.y() < childLogicalBottom)) + return positionForPointInChild(childBox, pointInContents); + } + } + + // We only get here if there are no hit test candidate children below the click. + return RenderBox::positionForPoint(point); +} + LayoutUnit RenderBlock::availableLogicalWidth() const { return RenderBox::availableLogicalWidth(); diff --git a/sky/engine/core/rendering/RenderBlock.h b/sky/engine/core/rendering/RenderBlock.h index 4a6403f08ea..36d42007ce3 100644 --- a/sky/engine/core/rendering/RenderBlock.h +++ b/sky/engine/core/rendering/RenderBlock.h @@ -132,6 +132,8 @@ public: LayoutUnit textIndentOffset() const; + virtual PositionWithAffinity positionForPoint(const LayoutPoint&) override; + // Block flows subclass availableWidth to handle multi column layout (shrinking the width available to children when laying out.) virtual LayoutUnit availableLogicalWidth() const override final; @@ -275,6 +277,8 @@ private: virtual LayoutRect localCaretRect(InlineBox*, int caretOffset, LayoutUnit* extraWidthToEndOfLine = 0) override final; + PositionWithAffinity positionForPointWithInlineChildren(const LayoutPoint&); + void removeFromGlobalMaps(); bool widthAvailableToChildrenHasChanged(); diff --git a/sky/engine/core/rendering/RenderBox.cpp b/sky/engine/core/rendering/RenderBox.cpp index 57722a69ad7..bcb07ec042c 100644 --- a/sky/engine/core/rendering/RenderBox.cpp +++ b/sky/engine/core/rendering/RenderBox.cpp @@ -2610,6 +2610,73 @@ LayoutRect RenderBox::localCaretRect(InlineBox* box, int caretOffset, LayoutUnit return rect; } +PositionWithAffinity RenderBox::positionForPoint(const LayoutPoint& point) +{ + // no children...return this render object's element, if there is one, and offset 0 + RenderObject* firstChild = slowFirstChild(); + if (!firstChild) + return createPositionWithAffinity(caretMinOffset(), DOWNSTREAM); + + // Pass off to the closest child. + LayoutUnit minDist = LayoutUnit::max(); + RenderBox* closestRenderer = 0; + LayoutPoint adjustedPoint = point; + + for (RenderObject* renderObject = firstChild; renderObject; renderObject = renderObject->nextSibling()) { + if (!renderObject->slowFirstChild() && !renderObject->isInline() && !renderObject->isRenderParagraph()) + continue; + + if (!renderObject->isBox()) + continue; + + RenderBox* renderer = toRenderBox(renderObject); + + LayoutUnit top = renderer->borderTop() + renderer->paddingTop() + renderer->y(); + LayoutUnit bottom = top + renderer->contentHeight(); + LayoutUnit left = renderer->borderLeft() + renderer->paddingLeft() + renderer->x(); + LayoutUnit right = left + renderer->contentWidth(); + + if (point.x() <= right && point.x() >= left && point.y() <= top && point.y() >= bottom) + return renderer->positionForPoint(point - renderer->locationOffset()); + + // Find the distance from (x, y) to the box. Split the space around the box into 8 pieces + // and use a different compare depending on which piece (x, y) is in. + LayoutPoint cmp; + if (point.x() > right) { + if (point.y() < top) + cmp = LayoutPoint(right, top); + else if (point.y() > bottom) + cmp = LayoutPoint(right, bottom); + else + cmp = LayoutPoint(right, point.y()); + } else if (point.x() < left) { + if (point.y() < top) + cmp = LayoutPoint(left, top); + else if (point.y() > bottom) + cmp = LayoutPoint(left, bottom); + else + cmp = LayoutPoint(left, point.y()); + } else { + if (point.y() < top) + cmp = LayoutPoint(point.x(), top); + else + cmp = LayoutPoint(point.x(), bottom); + } + + LayoutSize difference = cmp - point; + + LayoutUnit dist = difference.width() * difference.width() + difference.height() * difference.height(); + if (dist < minDist) { + closestRenderer = renderer; + minDist = dist; + } + } + + if (closestRenderer) + return closestRenderer->positionForPoint(adjustedPoint - closestRenderer->locationOffset()); + return createPositionWithAffinity(caretMinOffset(), DOWNSTREAM); +} + void RenderBox::addVisualEffectOverflow() { if (!style()->hasVisualOverflowingEffect()) diff --git a/sky/engine/core/rendering/RenderBox.h b/sky/engine/core/rendering/RenderBox.h index 88cd2215581..47279607e12 100644 --- a/sky/engine/core/rendering/RenderBox.h +++ b/sky/engine/core/rendering/RenderBox.h @@ -435,6 +435,8 @@ public: return true; } + virtual PositionWithAffinity positionForPoint(const LayoutPoint&) override; + void removeFloatingOrPositionedChildFromBlockLists(); RenderLayer* enclosingFloatPaintingLayer() const; diff --git a/sky/engine/core/rendering/RenderInline.cpp b/sky/engine/core/rendering/RenderInline.cpp index 9fc107de214..32f5b83d614 100644 --- a/sky/engine/core/rendering/RenderInline.cpp +++ b/sky/engine/core/rendering/RenderInline.cpp @@ -370,6 +370,23 @@ bool RenderInline::hitTestCulledInline(const HitTestRequest& request, HitTestRes return false; } +PositionWithAffinity RenderInline::positionForPoint(const LayoutPoint& point) +{ + // FIXME(sky): Now that we don't have continuations, can this whole function just be the following? + // return containingBlock()->positionForPoint(point); + + // FIXME: Does not deal with relative positioned inlines (should it?) + RenderBlock* cb = containingBlock(); + if (firstLineBox()) { + // This inline actually has a line box. We must have clicked in the border/padding of one of these boxes. We + // should try to find a result by asking our containing block. + return cb->positionForPoint(point); + } + + // Translate the coords from the pre-anonymous block to the post-anonymous block. + return RenderBoxModelObject::positionForPoint(point); +} + namespace { class LinesBoundingBoxGeneratorContext { diff --git a/sky/engine/core/rendering/RenderInline.h b/sky/engine/core/rendering/RenderInline.h index 9173b0b9709..8f07ba190fe 100644 --- a/sky/engine/core/rendering/RenderInline.h +++ b/sky/engine/core/rendering/RenderInline.h @@ -116,6 +116,8 @@ private: virtual void mapLocalToContainer(const RenderBox* paintInvalidationContainer, TransformState&, MapCoordinatesFlags = ApplyContainerFlip) const override; + virtual PositionWithAffinity positionForPoint(const LayoutPoint&) override final; + virtual IntRect borderBoundingBox() const override final { IntRect boundingBox = linesBoundingBox(); diff --git a/sky/engine/core/rendering/RenderObject.cpp b/sky/engine/core/rendering/RenderObject.cpp index 4954a7029bc..b65c8fbd15e 100644 --- a/sky/engine/core/rendering/RenderObject.cpp +++ b/sky/engine/core/rendering/RenderObject.cpp @@ -1402,6 +1402,11 @@ void RenderObject::postDestroy() delete this; } +PositionWithAffinity RenderObject::positionForPoint(const LayoutPoint&) +{ + return createPositionWithAffinity(caretMinOffset(), DOWNSTREAM); +} + // FIXME(sky): Change the callers to use nodeAtPoint direclty and remove this function. // Or, rename nodeAtPoint to hitTest? bool RenderObject::hitTest(const HitTestRequest& request, HitTestResult& result, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset) @@ -1527,6 +1532,11 @@ bool RenderObject::supportsTouchAction() const return true; } +PositionWithAffinity RenderObject::createPositionWithAffinity(int offset, EAffinity affinity) +{ + return PositionWithAffinity(this, offset, affinity); +} + bool RenderObject::canUpdateSelectionOnRootLineBoxes() { if (needsLayout()) diff --git a/sky/engine/core/rendering/RenderObject.h b/sky/engine/core/rendering/RenderObject.h index 672eaaa78cf..6a8a7300b94 100644 --- a/sky/engine/core/rendering/RenderObject.h +++ b/sky/engine/core/rendering/RenderObject.h @@ -26,6 +26,7 @@ #ifndef SKY_ENGINE_CORE_RENDERING_RENDEROBJECT_H_ #define SKY_ENGINE_CORE_RENDERING_RENDEROBJECT_H_ +#include "sky/engine/core/editing/PositionWithAffinity.h" #include "sky/engine/core/rendering/HitTestRequest.h" #include "sky/engine/core/rendering/RenderObjectChildList.h" #include "sky/engine/core/rendering/SubtreeLayoutScope.h" @@ -390,6 +391,10 @@ public: virtual void updateHitTestResult(HitTestResult&, const LayoutPoint&); virtual bool nodeAtPoint(const HitTestRequest&, HitTestResult&, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset); + virtual PositionWithAffinity positionForPoint(const LayoutPoint&); + PositionWithAffinity createPositionWithAffinity(int offset, EAffinity); + PositionWithAffinity createPositionWithAffinity(const Position&); + virtual void dirtyLinesFromChangedChild(RenderObject*); // Set the style of the object and update the state of the object accordingly. diff --git a/sky/engine/core/rendering/RenderReplaced.cpp b/sky/engine/core/rendering/RenderReplaced.cpp index 73c30c2f20b..ef4d2b51a32 100644 --- a/sky/engine/core/rendering/RenderReplaced.cpp +++ b/sky/engine/core/rendering/RenderReplaced.cpp @@ -374,6 +374,26 @@ void RenderReplaced::computePreferredLogicalWidths() clearPreferredLogicalWidthsDirty(); } +PositionWithAffinity RenderReplaced::positionForPoint(const LayoutPoint& point) +{ + // FIXME: This code is buggy if the replaced element is relative positioned. + InlineBox* box = inlineBoxWrapper(); + RootInlineBox* rootBox = box ? &box->root() : 0; + + LayoutUnit top = rootBox ? rootBox->selectionTop() : logicalTop(); + LayoutUnit bottom = rootBox ? rootBox->selectionBottom() : logicalBottom(); + + LayoutUnit blockDirectionPosition = point.y() + y(); + + if (blockDirectionPosition < top) + return createPositionWithAffinity(caretMinOffset(), DOWNSTREAM); // coordinates are above + + if (blockDirectionPosition >= bottom) + return createPositionWithAffinity(caretMaxOffset(), DOWNSTREAM); // coordinates are below + + return RenderBox::positionForPoint(point); +} + LayoutRect RenderReplaced::localSelectionRect(bool checkWhetherSelected) const { if (checkWhetherSelected && !isSelected()) diff --git a/sky/engine/core/rendering/RenderReplaced.h b/sky/engine/core/rendering/RenderReplaced.h index 6e62eefda04..5f33b361dec 100644 --- a/sky/engine/core/rendering/RenderReplaced.h +++ b/sky/engine/core/rendering/RenderReplaced.h @@ -78,6 +78,8 @@ private: virtual void computePreferredLogicalWidths() override final; virtual void paintReplaced(PaintInfo&, const LayoutPoint&) { } + virtual PositionWithAffinity positionForPoint(const LayoutPoint&) override final; + virtual bool canBeSelectionLeaf() const override { return true; } void computeAspectRatioInformationForRenderBox(FloatSize& constrainedSize, double& intrinsicRatio) const; diff --git a/sky/engine/core/rendering/RenderText.cpp b/sky/engine/core/rendering/RenderText.cpp index 51951eae25c..da9c74552f5 100644 --- a/sky/engine/core/rendering/RenderText.cpp +++ b/sky/engine/core/rendering/RenderText.cpp @@ -276,6 +276,169 @@ void RenderText::absoluteQuadsForRange(Vector& quads, unsigned start, enum ShouldAffinityBeDownstream { AlwaysDownstream, AlwaysUpstream, UpstreamIfPositionIsNotAtStart }; +static bool lineDirectionPointFitsInBox(int pointLineDirection, InlineTextBox* box, ShouldAffinityBeDownstream& shouldAffinityBeDownstream) +{ + shouldAffinityBeDownstream = AlwaysDownstream; + + // the x coordinate is equal to the left edge of this box + // the affinity must be downstream so the position doesn't jump back to the previous line + // except when box is the first box in the line + if (pointLineDirection <= box->logicalLeft()) { + shouldAffinityBeDownstream = !box->prevLeafChild() ? UpstreamIfPositionIsNotAtStart : AlwaysDownstream; + return true; + } + + // and the x coordinate is to the left of the right edge of this box + // check to see if position goes in this box + if (pointLineDirection < box->logicalRight()) { + shouldAffinityBeDownstream = UpstreamIfPositionIsNotAtStart; + return true; + } + + // box is first on line + // and the x coordinate is to the left of the first text box left edge + if (!box->prevLeafChildIgnoringLineBreak() && pointLineDirection < box->logicalLeft()) + return true; + + if (!box->nextLeafChildIgnoringLineBreak()) { + // box is last on line + // and the x coordinate is to the right of the last text box right edge + // generate VisiblePosition, use UPSTREAM affinity if possible + shouldAffinityBeDownstream = UpstreamIfPositionIsNotAtStart; + return true; + } + + return false; +} + +static PositionWithAffinity createPositionWithAffinityForBox(const InlineBox* box, int offset, ShouldAffinityBeDownstream shouldAffinityBeDownstream) +{ + EAffinity affinity = VP_DEFAULT_AFFINITY; + switch (shouldAffinityBeDownstream) { + case AlwaysDownstream: + affinity = DOWNSTREAM; + break; + case AlwaysUpstream: + affinity = VP_UPSTREAM_IF_POSSIBLE; + break; + case UpstreamIfPositionIsNotAtStart: + affinity = offset > box->caretMinOffset() ? VP_UPSTREAM_IF_POSSIBLE : DOWNSTREAM; + break; + } + int textStartOffset = box->renderer().isText() ? toRenderText(box->renderer()).textStartOffset() : 0; + return box->renderer().createPositionWithAffinity(offset + textStartOffset, affinity); +} + +static PositionWithAffinity createPositionWithAffinityForBoxAfterAdjustingOffsetForBiDi(const InlineTextBox* box, int offset, ShouldAffinityBeDownstream shouldAffinityBeDownstream) +{ + ASSERT(box); + ASSERT(offset >= 0); + + if (offset && static_cast(offset) < box->len()) + return createPositionWithAffinityForBox(box, box->start() + offset, shouldAffinityBeDownstream); + + bool positionIsAtStartOfBox = !offset; + if (positionIsAtStartOfBox == box->isLeftToRightDirection()) { + // offset is on the left edge + + const InlineBox* prevBox = box->prevLeafChildIgnoringLineBreak(); + if ((prevBox && prevBox->bidiLevel() == box->bidiLevel()) + || box->renderer().containingBlock()->style()->direction() == box->direction()) // FIXME: left on 12CBA + return createPositionWithAffinityForBox(box, box->caretLeftmostOffset(), shouldAffinityBeDownstream); + + if (prevBox && prevBox->bidiLevel() > box->bidiLevel()) { + // e.g. left of B in aDC12BAb + const InlineBox* leftmostBox; + do { + leftmostBox = prevBox; + prevBox = leftmostBox->prevLeafChildIgnoringLineBreak(); + } while (prevBox && prevBox->bidiLevel() > box->bidiLevel()); + return createPositionWithAffinityForBox(leftmostBox, leftmostBox->caretRightmostOffset(), shouldAffinityBeDownstream); + } + + if (!prevBox || prevBox->bidiLevel() < box->bidiLevel()) { + // e.g. left of D in aDC12BAb + const InlineBox* rightmostBox; + const InlineBox* nextBox = box; + do { + rightmostBox = nextBox; + nextBox = rightmostBox->nextLeafChildIgnoringLineBreak(); + } while (nextBox && nextBox->bidiLevel() >= box->bidiLevel()); + return createPositionWithAffinityForBox(rightmostBox, + box->isLeftToRightDirection() ? rightmostBox->caretMaxOffset() : rightmostBox->caretMinOffset(), shouldAffinityBeDownstream); + } + + return createPositionWithAffinityForBox(box, box->caretRightmostOffset(), shouldAffinityBeDownstream); + } + + const InlineBox* nextBox = box->nextLeafChildIgnoringLineBreak(); + if ((nextBox && nextBox->bidiLevel() == box->bidiLevel()) + || box->renderer().containingBlock()->style()->direction() == box->direction()) + return createPositionWithAffinityForBox(box, box->caretRightmostOffset(), shouldAffinityBeDownstream); + + // offset is on the right edge + if (nextBox && nextBox->bidiLevel() > box->bidiLevel()) { + // e.g. right of C in aDC12BAb + const InlineBox* rightmostBox; + do { + rightmostBox = nextBox; + nextBox = rightmostBox->nextLeafChildIgnoringLineBreak(); + } while (nextBox && nextBox->bidiLevel() > box->bidiLevel()); + return createPositionWithAffinityForBox(rightmostBox, rightmostBox->caretLeftmostOffset(), shouldAffinityBeDownstream); + } + + if (!nextBox || nextBox->bidiLevel() < box->bidiLevel()) { + // e.g. right of A in aDC12BAb + const InlineBox* leftmostBox; + const InlineBox* prevBox = box; + do { + leftmostBox = prevBox; + prevBox = leftmostBox->prevLeafChildIgnoringLineBreak(); + } while (prevBox && prevBox->bidiLevel() >= box->bidiLevel()); + return createPositionWithAffinityForBox(leftmostBox, + box->isLeftToRightDirection() ? leftmostBox->caretMinOffset() : leftmostBox->caretMaxOffset(), shouldAffinityBeDownstream); + } + + return createPositionWithAffinityForBox(box, box->caretLeftmostOffset(), shouldAffinityBeDownstream); +} + +PositionWithAffinity RenderText::positionForPoint(const LayoutPoint& point) +{ + if (!firstTextBox() || textLength() == 0) + return createPositionWithAffinity(0, DOWNSTREAM); + + LayoutUnit pointLineDirection = point.x(); + LayoutUnit pointBlockDirection = point.y(); + + InlineTextBox* lastBox = 0; + for (InlineTextBox* box = firstTextBox(); box; box = box->nextTextBox()) { + if (box->isLineBreak() && !box->prevLeafChild() && box->nextLeafChild() && !box->nextLeafChild()->isLineBreak()) + box = box->nextTextBox(); + + RootInlineBox& rootBox = box->root(); + LayoutUnit top = std::min(rootBox.selectionTop(), rootBox.lineTop()); + if (pointBlockDirection > top || pointBlockDirection == top) { + LayoutUnit bottom = rootBox.selectionBottom(); + if (rootBox.nextRootBox()) + bottom = std::min(bottom, rootBox.nextRootBox()->lineTop()); + + if (pointBlockDirection < bottom) { + ShouldAffinityBeDownstream shouldAffinityBeDownstream; + if (lineDirectionPointFitsInBox(pointLineDirection, box, shouldAffinityBeDownstream)) + return createPositionWithAffinityForBoxAfterAdjustingOffsetForBiDi(box, box->offsetForPosition(pointLineDirection.toFloat()), shouldAffinityBeDownstream); + } + } + lastBox = box; + } + + if (lastBox) { + ShouldAffinityBeDownstream shouldAffinityBeDownstream; + lineDirectionPointFitsInBox(pointLineDirection, lastBox, shouldAffinityBeDownstream); + return createPositionWithAffinityForBoxAfterAdjustingOffsetForBiDi(lastBox, lastBox->offsetForPosition(pointLineDirection.toFloat()) + lastBox->start(), shouldAffinityBeDownstream); + } + return createPositionWithAffinity(0, DOWNSTREAM); +} + LayoutRect RenderText::localCaretRect(InlineBox* inlineBox, int caretOffset, LayoutUnit* extraWidthToEndOfLine) { if (!inlineBox) diff --git a/sky/engine/core/rendering/RenderText.h b/sky/engine/core/rendering/RenderText.h index 23267cb1f77..1f128d31ead 100644 --- a/sky/engine/core/rendering/RenderText.h +++ b/sky/engine/core/rendering/RenderText.h @@ -65,6 +65,8 @@ public: enum ClippingOption { NoClipping, ClipToEllipsis }; void absoluteQuads(Vector&, ClippingOption = NoClipping) const; + virtual PositionWithAffinity positionForPoint(const LayoutPoint&) override; + bool is8Bit() const { return m_text.is8Bit(); } const LChar* characters8() const { return m_text.impl()->characters8(); } const UChar* characters16() const { return m_text.impl()->characters16(); } diff --git a/sky/engine/core/text/Paragraph.cpp b/sky/engine/core/text/Paragraph.cpp index f366d58da58..9a613113520 100644 --- a/sky/engine/core/text/Paragraph.cpp +++ b/sky/engine/core/text/Paragraph.cpp @@ -36,6 +36,7 @@ IMPLEMENT_WRAPPERTYPEINFO(ui, Paragraph); V(Paragraph, ideographicBaseline) \ V(Paragraph, layout) \ V(Paragraph, getRectsForRange) \ + V(Paragraph, getPositionForOffset) \ V(Paragraph, paint) DART_BIND_ALL(Paragraph, FOR_EACH_BINDING) @@ -134,4 +135,28 @@ std::vector Paragraph::getRectsForRange(unsigned start, unsigned end) { return boxes; } +int Paragraph::absoluteOffsetForPosition(const PositionWithAffinity& position) { + DCHECK(position.renderer()); + unsigned offset = 0; + for (RenderObject* object = m_renderView.get(); object; object = object->nextInPreOrder()) { + if (object == position.renderer()) + return offset + position.offset(); + if (object->isText()) { + RenderText* text = toRenderText(object); + offset += text->textLength(); + } + } + DCHECK(false); + return 0; +} + +Dart_Handle Paragraph::getPositionForOffset(const Offset& offset) { + LayoutPoint point(offset.sk_size.width(), offset.sk_size.height()); + PositionWithAffinity position = m_renderView->positionForPoint(point); + Dart_Handle result = Dart_NewList(2); + Dart_ListSetAt(result, 0, ToDart(absoluteOffsetForPosition(position))); + Dart_ListSetAt(result, 1, ToDart(static_cast(position.affinity()))); + return result; +} + } // namespace blink diff --git a/sky/engine/core/text/Paragraph.h b/sky/engine/core/text/Paragraph.h index 536f039afc7..32005d53b88 100644 --- a/sky/engine/core/text/Paragraph.h +++ b/sky/engine/core/text/Paragraph.h @@ -48,6 +48,7 @@ public: void paint(Canvas* canvas, const Offset& offset); std::vector getRectsForRange(unsigned start, unsigned end); + Dart_Handle getPositionForOffset(const Offset& offset); RenderView* renderView() const { return m_renderView.get(); } @@ -56,6 +57,8 @@ public: private: RenderBox* firstChildBox() const { return m_renderView->firstChildBox(); } + int absoluteOffsetForPosition(const PositionWithAffinity& position); + LayoutUnit m_minWidth; LayoutUnit m_maxWidth; LayoutUnit m_minHeight;