summaryrefslogtreecommitdiff
path: root/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.cc
blob: f23149166002f0574a7739c86f5da7bcedc8661d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
// Copyright 2018 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_caret_position.h"

#include "third_party/blink/renderer/core/editing/bidi_adjustment.h"
#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
#include "third_party/blink/renderer/core/editing/text_affinity.h"
#include "third_party/blink/renderer/core/layout/layout_block_flow.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h"
#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment.h"
#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment_traversal.h"

namespace blink {

namespace {

// The calculation takes the following input:
// - An inline formatting context as a |LayoutBlockFlow|
// - An offset in the |text_content_| string of the above context
// - A TextAffinity
//
// The calculation iterates all inline fragments in the context, and tries to
// compute an NGCaretPosition using the "caret resolution process" below:
//
// The (offset, affinity) pair is compared against each inline fragment to see
// if the corresponding caret should be placed in the fragment, using the
// |TryResolveCaretPositionInXXX()| functions. These functions may return:
// - Failed, indicating that the caret must not be placed in the fragment;
// - Resolved, indicating that the care should be placed in the fragment, and
//   no further search is required. The result NGCaretPosition is returned
//   together.
// - FoundCandidate, indicating that the caret may be placed in the fragment;
//   however, further search may find a better position. The candidate
//   NGCaretPosition is also returned together.

enum class ResolutionType { kFailed, kFoundCandidate, kResolved };
struct CaretPositionResolution {
  STACK_ALLOCATED();

 public:
  ResolutionType type = ResolutionType::kFailed;
  NGCaretPosition caret_position;
};

bool CanResolveCaretPositionBeforeFragment(const NGInlineCursor& cursor,
                                           TextAffinity affinity) {
  if (affinity == TextAffinity::kDownstream)
    return true;
  if (RuntimeEnabledFeatures::BidiCaretAffinityEnabled())
    return false;
  NGInlineCursor current_line(cursor);
  current_line.MoveToContainingLine();
  // A fragment after line wrap must be the first logical leaf in its line.
  NGInlineCursor first_logical_leaf(current_line);
  first_logical_leaf.MoveToFirstLogicalLeaf();
  if (cursor != first_logical_leaf)
    return true;
  NGInlineCursor last_line(current_line);
  last_line.MoveToPreviousLine();
  return !last_line || !last_line.Current().HasSoftWrapToNextLine();
}

bool CanResolveCaretPositionAfterFragment(const NGInlineCursor& cursor,
                                          TextAffinity affinity) {
  if (affinity == TextAffinity::kUpstream)
    return true;
  if (RuntimeEnabledFeatures::BidiCaretAffinityEnabled())
    return false;
  NGInlineCursor current_line(cursor);
  current_line.MoveToContainingLine();
  // A fragment before line wrap must be the last logical leaf in its line.
  NGInlineCursor last_logical_leaf(current_line);
  last_logical_leaf.MoveToLastLogicalLeaf();
  if (cursor != last_logical_leaf)
    return true;
  return !current_line.Current().HasSoftWrapToNextLine();
}

// Returns a |kFailed| resolution if |offset| doesn't belong to the text
// fragment. Otherwise, return either |kFoundCandidate| or |kResolved| depending
// on |affinity|.
CaretPositionResolution TryResolveCaretPositionInTextFragment(
    const NGInlineCursor& cursor,
    unsigned offset,
    TextAffinity affinity) {
  if (cursor.Current().IsGeneratedText())
    return CaretPositionResolution();

  const NGOffsetMapping& mapping =
      *NGOffsetMapping::GetFor(cursor.Current().GetLayoutObject());

  // A text fragment natually allows caret placement in offset range
  // [StartOffset(), EndOffset()], i.e., from before the first character to
  // after the last character.
  // Besides, leading/trailing bidi control characters are ignored since their
  // two sides are considered the same caret position. Hence, if there are n and
  // m leading and trailing bidi control characters, then the allowed offset
  // range is [StartOffset() - n, EndOffset() + m].
  // Note that we don't ignore other characters that are not in fragments. For
  // example, a trailing space of a line is not in any fragment, but its two
  // sides are still different caret positions, so we don't ignore it.
  const NGTextOffset current_offset = cursor.Current().TextOffset();
  const unsigned start_offset = current_offset.start;
  const unsigned end_offset = current_offset.end;
  if (offset < start_offset &&
      !mapping.HasBidiControlCharactersOnly(offset, start_offset))
    return CaretPositionResolution();
  if (offset > current_offset.end &&
      !mapping.HasBidiControlCharactersOnly(end_offset, offset))
    return CaretPositionResolution();

  offset = std::max(offset, start_offset);
  offset = std::min(offset, end_offset);
  NGCaretPosition candidate = {cursor, NGCaretPositionType::kAtTextOffset,
                               offset};

  // Offsets in the interior of a fragment can be resolved directly.
  if (offset > start_offset && offset < end_offset)
    return {ResolutionType::kResolved, candidate};

  if (offset == start_offset &&
      CanResolveCaretPositionBeforeFragment(cursor, affinity)) {
    return {ResolutionType::kResolved, candidate};
  }

  if (offset == end_offset && !cursor.Current().IsLineBreak() &&
      CanResolveCaretPositionAfterFragment(cursor, affinity)) {
    return {ResolutionType::kResolved, candidate};
  }

  // We may have a better candidate
  return {ResolutionType::kFoundCandidate, candidate};
}

unsigned GetTextOffsetBefore(const Node& node) {
  // TODO(xiaochengh): Design more straightforward way to get text offset of
  // atomic inline box.
  DCHECK(node.GetLayoutObject()->IsAtomicInlineLevel());
  const Position before_node = Position::BeforeNode(node);
  base::Optional<unsigned> maybe_offset_before =
      NGOffsetMapping::GetFor(before_node)->GetTextContentOffset(before_node);
  // We should have offset mapping for atomic inline boxes.
  DCHECK(maybe_offset_before.has_value());
  return *maybe_offset_before;
}

// Returns a |kFailed| resolution if |offset| doesn't belong to the atomic
// inline box fragment. Otherwise, return either |kFoundCandidate| or
// |kResolved| depending on |affinity|.
CaretPositionResolution TryResolveCaretPositionByBoxFragmentSide(
    const NGInlineCursor& cursor,
    unsigned offset,
    TextAffinity affinity) {
  const Node* const node = cursor.Current().GetNode();
  // There is no caret position at a pseudo or generated box side.
  if (!node || node->IsPseudoElement()) {
    // TODO(xiaochengh): This leads to false negatives for, e.g., RUBY, where an
    // anonymous wrapping inline block is created.
    return CaretPositionResolution();
  }

  const unsigned offset_before = GetTextOffsetBefore(*node);
  const unsigned offset_after = offset_before + 1;
  // TODO(xiaochengh): Ignore bidi control characters before & after the box.
  if (offset != offset_before && offset != offset_after)
    return CaretPositionResolution();
  const NGCaretPositionType position_type =
      offset == offset_before ? NGCaretPositionType::kBeforeBox
                              : NGCaretPositionType::kAfterBox;
  NGCaretPosition candidate{cursor, position_type, base::nullopt};

  if (offset == offset_before &&
      CanResolveCaretPositionBeforeFragment(cursor, affinity)) {
    return {ResolutionType::kResolved, candidate};
  }

  if (offset == offset_after &&
      CanResolveCaretPositionAfterFragment(cursor, affinity)) {
    return {ResolutionType::kResolved, candidate};
  }

  return {ResolutionType::kFoundCandidate, candidate};
}

CaretPositionResolution TryResolveCaretPositionWithFragment(
    const NGInlineCursor& cursor,
    unsigned offset,
    TextAffinity affinity) {
  if (cursor.Current().IsText())
    return TryResolveCaretPositionInTextFragment(cursor, offset, affinity);
  if (cursor.Current().IsAtomicInline())
    return TryResolveCaretPositionByBoxFragmentSide(cursor, offset, affinity);
  return CaretPositionResolution();
}

bool NeedsBidiAdjustment(const NGCaretPosition& caret_position) {
  if (RuntimeEnabledFeatures::BidiCaretAffinityEnabled())
    return false;
  if (caret_position.IsNull())
    return false;
  if (caret_position.position_type != NGCaretPositionType::kAtTextOffset)
    return true;
  DCHECK(caret_position.text_offset.has_value());
  const NGTextOffset offset = caret_position.cursor.Current().TextOffset();
  const unsigned start_offset = offset.start;
  const unsigned end_offset = offset.end;
  DCHECK_GE(*caret_position.text_offset, start_offset);
  DCHECK_LE(*caret_position.text_offset, end_offset);
  // Bidi adjustment is needed only for caret positions at bidi boundaries.
  // Caret positions in the middle of a text fragment can't be at bidi
  // boundaries, and hence, don't need any adjustment.
  return *caret_position.text_offset == start_offset ||
         *caret_position.text_offset == end_offset;
}

NGCaretPosition AdjustCaretPositionForBidiText(
    const NGCaretPosition& caret_position) {
  if (!NeedsBidiAdjustment(caret_position))
    return caret_position;
  return BidiAdjustment::AdjustForCaretPositionResolution(caret_position);
}

bool IsUpstreamAfterLineBreak(const NGCaretPosition& caret_position) {
  if (caret_position.position_type != NGCaretPositionType::kAtTextOffset)
    return false;

  DCHECK(caret_position.cursor.IsNotNull());
  DCHECK(caret_position.text_offset.has_value());

  if (!caret_position.cursor.Current().IsLineBreak())
    return false;
  return *caret_position.text_offset ==
         caret_position.cursor.Current().TextEndOffset();
}

NGCaretPosition BetterCandidateBetween(const NGCaretPosition& current,
                                       const NGCaretPosition& other,
                                       unsigned offset,
                                       TextAffinity affinity) {
  DCHECK(!other.IsNull());
  if (current.IsNull())
    return other;

  // There shouldn't be too many cases where we have multiple candidates.
  // Make sure all of them are captured and handled here.

  // Only known case: either |current| or |other| is upstream after line break.
  DCHECK_EQ(affinity, TextAffinity::kUpstream);
  if (IsUpstreamAfterLineBreak(current)) {
    DCHECK(!IsUpstreamAfterLineBreak(other));
    return other;
  }
  return current;
}

}  // namespace

// The main function for compute an NGCaretPosition. See the comments at the top
// of this file for details.
NGCaretPosition ComputeNGCaretPosition(const LayoutBlockFlow& context,
                                       unsigned offset,
                                       TextAffinity affinity,
                                       const LayoutText* layout_text) {
  NGInlineCursor cursor(context);

  NGCaretPosition candidate;
  if (layout_text && layout_text->HasInlineFragments())
    cursor.MoveTo(*layout_text);
  for (; cursor; cursor.MoveToNext()) {
    const CaretPositionResolution resolution =
        TryResolveCaretPositionWithFragment(cursor, offset, affinity);

    if (resolution.type == ResolutionType::kFailed)
      continue;

    // TODO(xiaochengh): Handle caret poisition in empty container (e.g. empty
    // line box).

    if (resolution.type == ResolutionType::kResolved) {
      candidate = resolution.caret_position;
      if (!layout_text ||
          candidate.cursor.Current().GetLayoutObject() == layout_text)
        return AdjustCaretPositionForBidiText(resolution.caret_position);
      continue;
    }

    DCHECK_EQ(ResolutionType::kFoundCandidate, resolution.type);
    candidate = BetterCandidateBetween(candidate, resolution.caret_position,
                                       offset, affinity);
  }

  return AdjustCaretPositionForBidiText(candidate);
}

NGCaretPosition ComputeNGCaretPosition(
    const PositionWithAffinity& position_with_affinity) {
  const Position& position = position_with_affinity.GetPosition();
  LayoutBlockFlow* context = NGInlineFormattingContextOf(position);
  if (!context)
    return NGCaretPosition();

  const NGOffsetMapping* mapping = NGInlineNode::GetOffsetMapping(context);
  if (!mapping) {
    // TODO(yosin): We should find when we reach here[1].
    // [1] http://crbug.com/1100481
    NOTREACHED() << context;
    return NGCaretPosition();
  }
  const base::Optional<unsigned> maybe_offset =
      mapping->GetTextContentOffset(position);
  if (!maybe_offset.has_value()) {
    // TODO(xiaochengh): Investigate if we reach here.
    NOTREACHED();
    return NGCaretPosition();
  }

  const LayoutText* const layout_text =
      position.IsOffsetInAnchor() && IsA<Text>(position.AnchorNode())
          ? To<LayoutText>(AssociatedLayoutObjectOf(
                *position.AnchorNode(), position.OffsetInContainerNode()))
          : nullptr;

  const unsigned offset = *maybe_offset;
  const TextAffinity affinity = position_with_affinity.Affinity();
  return ComputeNGCaretPosition(*context, offset, affinity, layout_text);
}

Position NGCaretPosition::ToPositionInDOMTree() const {
  return ToPositionInDOMTreeWithAffinity().GetPosition();
}

PositionWithAffinity NGCaretPosition::ToPositionInDOMTreeWithAffinity() const {
  if (IsNull())
    return PositionWithAffinity();
  switch (position_type) {
    case NGCaretPositionType::kBeforeBox:
      if (const Node* node = cursor.Current().GetNode()) {
        return PositionWithAffinity(Position::BeforeNode(*node),
                                    TextAffinity::kDownstream);
      }
      return PositionWithAffinity();
    case NGCaretPositionType::kAfterBox:
      if (const Node* node = cursor.Current().GetNode()) {
        return PositionWithAffinity(Position::AfterNode(*node),
                                    TextAffinity::kUpstreamIfPossible);
      }
      return PositionWithAffinity();
    case NGCaretPositionType::kAtTextOffset:
      // In case of ::first-letter, |cursor.Current().GetNode()| is null.
      DCHECK(text_offset.has_value());
      const NGOffsetMapping* mapping =
          NGOffsetMapping::GetFor(cursor.Current().GetLayoutObject());
      if (!mapping) {
        // TODO(yosin): We're not sure why |mapping| is |nullptr|. It seems
        // we are attempt to use destroyed/moved |NGFragmentItem|.
        // See http://crbug.com/1145514
        NOTREACHED() << cursor << " " << cursor.Current().GetLayoutObject();
        return PositionWithAffinity();
      }
      const TextAffinity affinity =
          *text_offset == cursor.Current().TextEndOffset()
              ? TextAffinity::kUpstreamIfPossible
              : TextAffinity::kDownstream;
      const Position position = affinity == TextAffinity::kDownstream
                                    ? mapping->GetLastPosition(*text_offset)
                                    : mapping->GetFirstPosition(*text_offset);
      if (position.IsNull())
        return PositionWithAffinity();
      return PositionWithAffinity(position, affinity);
  }
  NOTREACHED();
  return PositionWithAffinity();
}

}  // namespace blink