1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "TextLeafRange.h"
9 #include "HyperTextAccessible-inl.h"
10 #include "mozilla/a11y/Accessible.h"
11 #include "mozilla/a11y/CacheConstants.h"
12 #include "mozilla/a11y/DocAccessible.h"
13 #include "mozilla/a11y/DocAccessibleParent.h"
14 #include "mozilla/a11y/LocalAccessible.h"
15 #include "mozilla/BinarySearch.h"
16 #include "mozilla/Casting.h"
17 #include "mozilla/dom/CharacterData.h"
18 #include "mozilla/dom/HTMLInputElement.h"
19 #include "mozilla/PresShell.h"
20 #include "mozilla/intl/Segmenter.h"
21 #include "mozilla/intl/WordBreaker.h"
22 #include "mozilla/StaticPrefs_layout.h"
23 #include "mozilla/TextEditor.h"
24 #include "nsAccUtils.h"
25 #include "nsBlockFrame.h"
26 #include "nsFrameSelection.h"
27 #include "nsIAccessiblePivot.h"
28 #include "nsILineIterator.h"
31 #include "nsStyleStructInlines.h"
33 #include "nsTextFrame.h"
34 #include "nsUnicharUtils.h"
36 #include "TextAttrs.h"
38 using mozilla::intl::WordBreaker
;
39 using FindWordOptions
= mozilla::intl::WordBreaker::FindWordOptions
;
41 namespace mozilla::a11y
{
46 * These two functions convert between rendered and content text offsets.
47 * When text DOM nodes are rendered, the rendered text often does not contain
48 * all the whitespace from the source. For example, by default, the text
49 * "a b" will be rendered as "a b"; i.e. multiple spaces are compressed to
50 * one. TextLeafAccessibles contain rendered text, but when we query layout, we
51 * need to provide offsets into the original content text. Similarly, layout
52 * returns content offsets, but we need to convert them to rendered offsets to
53 * map them to TextLeafAccessibles.
56 static int32_t RenderedToContentOffset(LocalAccessible
* aAcc
,
57 uint32_t aRenderedOffset
) {
58 nsTextFrame
* frame
= do_QueryFrame(aAcc
->GetFrame());
60 MOZ_ASSERT(!aAcc
->HasOwnContent() || aAcc
->IsHTMLBr(),
61 "No text frame because this is a XUL label[value] text leaf or "
63 return static_cast<int32_t>(aRenderedOffset
);
66 if (frame
->StyleText()->WhiteSpaceIsSignificant() &&
67 frame
->StyleText()->NewlineIsSignificant(frame
)) {
68 // Spaces and new lines aren't altered, so the content and rendered offsets
69 // are the same. This happens in pre-formatted text and text fields.
70 return static_cast<int32_t>(aRenderedOffset
);
73 nsIFrame::RenderedText text
=
74 frame
->GetRenderedText(aRenderedOffset
, aRenderedOffset
+ 1,
75 nsIFrame::TextOffsetType::OffsetsInRenderedText
,
76 nsIFrame::TrailingWhitespace::DontTrim
);
77 return text
.mOffsetWithinNodeText
;
80 static uint32_t ContentToRenderedOffset(LocalAccessible
* aAcc
,
81 int32_t aContentOffset
) {
82 nsTextFrame
* frame
= do_QueryFrame(aAcc
->GetFrame());
84 MOZ_ASSERT(!aAcc
->HasOwnContent(),
85 "No text frame because this is a XUL label[value] text leaf.");
86 return aContentOffset
;
89 if (frame
->StyleText()->WhiteSpaceIsSignificant() &&
90 frame
->StyleText()->NewlineIsSignificant(frame
)) {
91 // Spaces and new lines aren't altered, so the content and rendered offsets
92 // are the same. This happens in pre-formatted text and text fields.
93 return aContentOffset
;
96 nsIFrame::RenderedText text
=
97 frame
->GetRenderedText(aContentOffset
, aContentOffset
+ 1,
98 nsIFrame::TextOffsetType::OffsetsInContentText
,
99 nsIFrame::TrailingWhitespace::DontTrim
);
100 return text
.mOffsetWithinNodeRenderedText
;
103 class LeafRule
: public PivotRule
{
105 explicit LeafRule(bool aIgnoreListItemMarker
)
106 : mIgnoreListItemMarker(aIgnoreListItemMarker
) {}
108 virtual uint16_t Match(Accessible
* aAcc
) override
{
109 if (aAcc
->IsOuterDoc()) {
110 // Treat an embedded doc as a single character in this document, but do
111 // not descend inside it.
112 return nsIAccessibleTraversalRule::FILTER_MATCH
|
113 nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
;
116 if (mIgnoreListItemMarker
&& aAcc
->Role() == roles::LISTITEM_MARKER
) {
117 // Ignore list item markers if configured to do so.
118 return nsIAccessibleTraversalRule::FILTER_IGNORE
;
121 // We deliberately include Accessibles such as empty input elements and
122 // empty containers, as these can be at the start of a line.
123 if (!aAcc
->HasChildren()) {
124 return nsIAccessibleTraversalRule::FILTER_MATCH
;
126 return nsIAccessibleTraversalRule::FILTER_IGNORE
;
130 bool mIgnoreListItemMarker
;
133 static HyperTextAccessible
* HyperTextFor(LocalAccessible
* aAcc
) {
134 for (LocalAccessible
* acc
= aAcc
; acc
; acc
= acc
->LocalParent()) {
135 if (HyperTextAccessible
* ht
= acc
->AsHyperText()) {
142 static Accessible
* NextLeaf(Accessible
* aOrigin
, bool aIsEditable
= false,
143 bool aIgnoreListItemMarker
= false) {
145 Accessible
* doc
= nsAccUtils::DocumentFor(aOrigin
);
147 auto rule
= LeafRule(aIgnoreListItemMarker
);
148 Accessible
* leaf
= pivot
.Next(aOrigin
, rule
);
149 if (aIsEditable
&& leaf
) {
150 return leaf
->Parent() && (leaf
->Parent()->State() & states::EDITABLE
)
157 static Accessible
* PrevLeaf(Accessible
* aOrigin
, bool aIsEditable
= false,
158 bool aIgnoreListItemMarker
= false) {
160 Accessible
* doc
= nsAccUtils::DocumentFor(aOrigin
);
162 auto rule
= LeafRule(aIgnoreListItemMarker
);
163 Accessible
* leaf
= pivot
.Prev(aOrigin
, rule
);
164 if (aIsEditable
&& leaf
) {
165 return leaf
->Parent() && (leaf
->Parent()->State() & states::EDITABLE
)
172 static nsIFrame
* GetFrameInBlock(const LocalAccessible
* aAcc
) {
173 dom::HTMLInputElement
* input
=
174 dom::HTMLInputElement::FromNodeOrNull(aAcc
->GetContent());
176 if (LocalAccessible
* parent
= aAcc
->LocalParent()) {
177 input
= dom::HTMLInputElement::FromNodeOrNull(parent
->GetContent());
182 // If this is a single line input (or a leaf of an input) we want to return
183 // the top frame of the input element and not the text leaf's frame because
184 // the leaf may be inside of an embedded block frame in the input's shadow
185 // DOM that we aren't interested in.
186 return input
->GetPrimaryFrame();
189 return aAcc
->GetFrame();
192 static bool IsLocalAccAtLineStart(LocalAccessible
* aAcc
) {
193 if (aAcc
->NativeRole() == roles::LISTITEM_MARKER
) {
194 // A bullet always starts a line.
197 // Splitting of content across lines is handled by layout.
198 // nsIFrame::IsLogicallyAtLineEdge queries whether a frame is the first frame
199 // on its line. However, we can't use that because the first frame on a line
200 // might not be included in the a11y tree; e.g. an empty span, or space
201 // in the DOM after a line break which is stripped when rendered. Instead, we
202 // get the line number for this Accessible's frame and the line number for the
203 // previous leaf Accessible's frame and compare them.
204 Accessible
* prev
= PrevLeaf(aAcc
);
205 LocalAccessible
* prevLocal
= prev
? prev
->AsLocal() : nullptr;
207 // There's nothing before us, so this is the start of the first line.
210 if (prevLocal
->NativeRole() == roles::LISTITEM_MARKER
) {
211 // If there is a bullet immediately before us and we're inside the same
212 // list item, this is not the start of a line.
213 LocalAccessible
* listItem
= prevLocal
->LocalParent();
214 MOZ_ASSERT(listItem
);
215 LocalAccessible
* doc
= listItem
->Document();
217 for (LocalAccessible
* parent
= aAcc
->LocalParent(); parent
&& parent
!= doc
;
218 parent
= parent
->LocalParent()) {
219 if (parent
== listItem
) {
225 nsIFrame
* thisFrame
= GetFrameInBlock(aAcc
);
230 nsIFrame
* prevFrame
= GetFrameInBlock(prevLocal
);
235 auto [thisBlock
, thisLineFrame
] = thisFrame
->GetContainingBlockForLine(
236 /* aLockScroll */ false);
238 // We couldn't get the containing block for this frame. In that case, we
239 // play it safe and assume this is the beginning of a new line.
243 // The previous leaf might cross lines. We want to compare against the last
245 prevFrame
= prevFrame
->LastContinuation();
246 auto [prevBlock
, prevLineFrame
] = prevFrame
->GetContainingBlockForLine(
247 /* aLockScroll */ false);
248 if (thisBlock
!= prevBlock
) {
249 // If the blocks are different, that means there's nothing before us on the
250 // same line, so we're at the start.
253 if (nsBlockFrame
* block
= do_QueryFrame(thisBlock
)) {
254 // If we have a block frame, it's faster for us to use
255 // BlockInFlowLineIterator because it uses the line cursor.
257 block
->SetupLineCursorForQuery();
258 nsBlockInFlowLineIterator
prevIt(block
, prevLineFrame
, &found
);
260 // Error; play it safe.
264 nsBlockInFlowLineIterator
thisIt(block
, thisLineFrame
, &found
);
265 // if the lines are different, that means there's nothing before us on the
266 // same line, so we're at the start.
267 return !found
|| prevIt
.GetLine() != thisIt
.GetLine();
269 AutoAssertNoDomMutations guard
;
270 nsILineIterator
* it
= prevBlock
->GetLineIterator();
271 MOZ_ASSERT(it
, "GetLineIterator impl in line-container blocks is infallible");
272 int32_t prevLineNum
= it
->FindLineContaining(prevLineFrame
);
273 if (prevLineNum
< 0) {
274 // Error; play it safe.
277 int32_t thisLineNum
= it
->FindLineContaining(thisLineFrame
, prevLineNum
);
278 // if the blocks and line numbers are different, that means there's nothing
279 // before us on the same line, so we're at the start.
280 return thisLineNum
!= prevLineNum
;
284 * There are many kinds of word break, but we only need to treat punctuation and
287 enum WordBreakClass
{ eWbcSpace
= 0, eWbcPunct
, eWbcOther
};
289 static WordBreakClass
GetWordBreakClass(char16_t aChar
) {
290 // Based on IsSelectionInlineWhitespace and IsSelectionNewline in
291 // layout/generic/nsTextFrame.cpp.
292 const char16_t kCharNbsp
= 0xA0;
304 return mozilla::IsPunctuationForWordSelect(aChar
) ? eWbcPunct
: eWbcOther
;
308 * Words can cross Accessibles. To work out whether we're at the start of a
309 * word, we might have to check the previous leaf. This class handles querying
310 * the previous WordBreakClass, crossing Accessibles if necessary.
312 class PrevWordBreakClassWalker
{
314 PrevWordBreakClassWalker(Accessible
* aAcc
, const nsAString
& aText
,
316 : mAcc(aAcc
), mText(aText
), mOffset(aOffset
) {
317 mClass
= GetWordBreakClass(mText
.CharAt(mOffset
));
320 WordBreakClass
CurClass() { return mClass
; }
322 Maybe
<WordBreakClass
> PrevClass() {
327 WordBreakClass curClass
= GetWordBreakClass(mText
.CharAt(mOffset
));
328 if (curClass
!= mClass
) {
330 return Some(curClass
);
333 MOZ_ASSERT_UNREACHABLE();
337 bool IsStartOfGroup() {
339 // There are no characters before us.
342 WordBreakClass curClass
= GetWordBreakClass(mText
.CharAt(mOffset
));
343 // We wanted to peek at the previous character, not really move to it.
345 return curClass
!= mClass
;
355 // PrevChar was called already and failed.
358 mAcc
= PrevLeaf(mAcc
);
363 mAcc
->AppendTextTo(mText
);
364 mOffset
= static_cast<int32_t>(mText
.Length()) - 1;
371 WordBreakClass mClass
;
375 * WordBreaker breaks at all space, punctuation, etc. We want to emulate
376 * layout, so that's not what we want. This function determines whether this
377 * is acceptable as the start of a word for our purposes.
379 static bool IsAcceptableWordStart(Accessible
* aAcc
, const nsAutoString
& aText
,
381 PrevWordBreakClassWalker
walker(aAcc
, aText
, aOffset
);
382 if (!walker
.IsStartOfGroup()) {
383 // If we're not at the start of a WordBreaker group, this can't be the
387 WordBreakClass curClass
= walker
.CurClass();
388 if (curClass
== eWbcSpace
) {
389 // Space isn't the start of a word.
392 Maybe
<WordBreakClass
> prevClass
= walker
.PrevClass();
393 if (curClass
== eWbcPunct
&& (!prevClass
|| prevClass
.value() != eWbcSpace
)) {
394 // Punctuation isn't the start of a word (unless it is after space).
397 if (!prevClass
|| prevClass
.value() != eWbcPunct
) {
398 // If there's nothing before this or the group before this isn't
399 // punctuation, this is the start of a word.
402 // At this point, we know the group before this is punctuation.
403 if (!StaticPrefs::layout_word_select_stop_at_punctuation()) {
404 // When layout.word_select.stop_at_punctuation is false (defaults to true),
405 // if there is punctuation before this, this is not the start of a word.
408 Maybe
<WordBreakClass
> prevPrevClass
= walker
.PrevClass();
409 if (!prevPrevClass
|| prevPrevClass
.value() == eWbcSpace
) {
410 // If there is punctuation before this and space (or nothing) before the
411 // punctuation, this is not the start of a word.
417 class BlockRule
: public PivotRule
{
419 virtual uint16_t Match(Accessible
* aAcc
) override
{
420 if (RefPtr
<nsAtom
>(aAcc
->DisplayStyle()) == nsGkAtoms::block
||
421 aAcc
->IsHTMLListItem() || aAcc
->IsTableRow() || aAcc
->IsTableCell()) {
422 return nsIAccessibleTraversalRule::FILTER_MATCH
;
424 return nsIAccessibleTraversalRule::FILTER_IGNORE
;
429 * Find spelling error DOM ranges overlapping the requested LocalAccessible and
430 * offsets. This includes ranges that begin or end outside of the given
431 * LocalAccessible. Note that the offset arguments are rendered offsets, but
432 * because the returned ranges are DOM ranges, those offsets are content
433 * offsets. See the documentation for dom::Selection::GetRangesForIntervalArray
434 * for information about the aAllowAdjacent argument.
436 static nsTArray
<nsRange
*> FindDOMSpellingErrors(LocalAccessible
* aAcc
,
437 int32_t aRenderedStart
,
438 int32_t aRenderedEnd
,
439 bool aAllowAdjacent
= false) {
440 if (!aAcc
->IsTextLeaf() || !aAcc
->HasOwnContent()) {
443 nsIFrame
* frame
= aAcc
->GetFrame();
444 RefPtr
<nsFrameSelection
> frameSel
=
445 frame
? frame
->GetFrameSelection() : nullptr;
446 dom::Selection
* domSel
=
447 frameSel
? frameSel
->GetSelection(SelectionType::eSpellCheck
) : nullptr;
451 nsINode
* node
= aAcc
->GetNode();
452 uint32_t contentStart
= RenderedToContentOffset(aAcc
, aRenderedStart
);
453 uint32_t contentEnd
=
454 aRenderedEnd
== nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
455 ? dom::CharacterData::FromNode(node
)->TextLength()
456 : RenderedToContentOffset(aAcc
, aRenderedEnd
);
457 nsTArray
<nsRange
*> domRanges
;
458 domSel
->GetDynamicRangesForIntervalArray(node
, contentStart
, node
, contentEnd
,
459 aAllowAdjacent
, &domRanges
);
464 * Given two DOM nodes get DOM Selection object that is common
467 static dom::Selection
* GetDOMSelection(const nsIContent
* aStartContent
,
468 const nsIContent
* aEndContent
) {
469 nsIFrame
* startFrame
= aStartContent
->GetPrimaryFrame();
470 const nsFrameSelection
* startFrameSel
=
471 startFrame
? startFrame
->GetConstFrameSelection() : nullptr;
472 nsIFrame
* endFrame
= aEndContent
->GetPrimaryFrame();
473 const nsFrameSelection
* endFrameSel
=
474 endFrame
? endFrame
->GetConstFrameSelection() : nullptr;
476 if (startFrameSel
!= endFrameSel
) {
477 // Start and end point don't share the same selection state.
478 // This could happen when both points aren't in the same editable.
482 return startFrameSel
? startFrameSel
->GetSelection(SelectionType::eNormal
)
486 std::pair
<nsIContent
*, int32_t> TextLeafPoint::ToDOMPoint(
487 bool aIncludeGenerated
) const {
488 if (!(*this) || !mAcc
->IsLocal()) {
489 MOZ_ASSERT_UNREACHABLE("Invalid point");
493 nsIContent
* content
= mAcc
->AsLocal()->GetContent();
494 nsIFrame
* frame
= content
? content
->GetPrimaryFrame() : nullptr;
497 if (!aIncludeGenerated
&& frame
&& frame
->IsGeneratedContentFrame()) {
498 // List markers accessibles represent the generated content element,
499 // before/after text accessibles represent the child text nodes.
500 auto generatedElement
= content
->IsGeneratedContentContainerForMarker()
502 : content
->GetParentElement();
503 auto parent
= generatedElement
? generatedElement
->GetParent() : nullptr;
506 if (generatedElement
->IsGeneratedContentContainerForAfter()) {
507 // Use the end offset of the parent element for trailing generated
509 return {parent
, parent
->GetChildCount()};
512 if (generatedElement
->IsGeneratedContentContainerForBefore() ||
513 generatedElement
->IsGeneratedContentContainerForMarker()) {
514 // Use the start offset of the parent element for leading generated
519 MOZ_ASSERT_UNREACHABLE("Unknown generated content type!");
523 if (!mAcc
->IsTextLeaf() && !mAcc
->IsHTMLBr() && !mAcc
->HasChildren()) {
524 // If this is not a text leaf it can be an empty editable container,
525 // whitespace, or an empty doc. In any case, the offset inside should be 0.
526 MOZ_ASSERT(mOffset
== 0);
528 if (RefPtr
<TextControlElement
> textControlElement
=
529 TextControlElement::FromNodeOrNull(content
)) {
530 // This is an empty input, use the shadow root's element.
531 if (RefPtr
<TextEditor
> textEditor
= textControlElement
->GetTextEditor()) {
532 if (textEditor
->IsEmpty()) {
533 MOZ_ASSERT(mOffset
== 0);
534 return {textEditor
->GetRoot(), 0};
542 return {content
, RenderedToContentOffset(mAcc
->AsLocal(), mOffset
)};
545 /*** TextLeafPoint ***/
547 TextLeafPoint::TextLeafPoint(Accessible
* aAcc
, int32_t aOffset
) {
549 // Construct an invalid point.
555 // Even though an OuterDoc contains a document, we treat it as a leaf because
556 // we don't want to move into another document.
557 if (aOffset
!= nsIAccessibleText::TEXT_OFFSET_CARET
&& !aAcc
->IsOuterDoc() &&
558 aAcc
->HasChildren()) {
559 // Find a leaf. This might not necessarily be a TextLeafAccessible; it
560 // could be an empty container.
561 auto GetChild
= [&aOffset
](Accessible
* acc
) -> Accessible
* {
562 if (acc
->IsOuterDoc()) {
565 return aOffset
!= nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
570 for (Accessible
* acc
= GetChild(aAcc
); acc
; acc
= GetChild(acc
)) {
573 mOffset
= aOffset
!= nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
575 : nsAccUtils::TextLength(mAcc
);
579 mOffset
= aOffset
!= nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
581 : nsAccUtils::TextLength(mAcc
);
584 bool TextLeafPoint::operator<(const TextLeafPoint
& aPoint
) const {
585 if (mAcc
== aPoint
.mAcc
) {
586 return mOffset
< aPoint
.mOffset
;
588 return mAcc
->IsBefore(aPoint
.mAcc
);
591 bool TextLeafPoint::operator<=(const TextLeafPoint
& aPoint
) const {
592 return *this == aPoint
|| *this < aPoint
;
595 bool TextLeafPoint::IsDocEdge(nsDirection aDirection
) const {
596 if (aDirection
== eDirPrevious
) {
597 return mOffset
== 0 && !PrevLeaf(mAcc
);
600 return mOffset
== static_cast<int32_t>(nsAccUtils::TextLength(mAcc
)) &&
604 bool TextLeafPoint::IsLeafAfterListItemMarker() const {
605 Accessible
* prev
= PrevLeaf(mAcc
);
606 return prev
&& prev
->Role() == roles::LISTITEM_MARKER
&&
607 prev
->Parent()->IsAncestorOf(mAcc
);
610 bool TextLeafPoint::IsEmptyLastLine() const {
611 if (mAcc
->IsHTMLBr() && mOffset
== 1) {
614 if (!mAcc
->IsTextLeaf()) {
617 if (mOffset
< static_cast<int32_t>(nsAccUtils::TextLength(mAcc
))) {
621 mAcc
->AppendTextTo(text
, mOffset
- 1, 1);
622 return text
.CharAt(0) == '\n';
625 char16_t
TextLeafPoint::GetChar() const {
627 mAcc
->AppendTextTo(text
, mOffset
, 1);
628 return text
.CharAt(0);
631 TextLeafPoint
TextLeafPoint::FindPrevLineStartSameLocalAcc(
632 bool aIncludeOrigin
) const {
633 LocalAccessible
* acc
= mAcc
->AsLocal();
636 if (aIncludeOrigin
&& IsLocalAccAtLineStart(acc
)) {
639 return TextLeafPoint();
641 nsIFrame
* frame
= acc
->GetFrame();
643 // This can happen if this is an empty element with display: contents. In
644 // that case, this Accessible contains no lines.
645 return TextLeafPoint();
647 if (!frame
->IsTextFrame()) {
648 if (IsLocalAccAtLineStart(acc
)) {
649 return TextLeafPoint(acc
, 0);
651 return TextLeafPoint();
653 // Each line of a text node is rendered as a continuation frame. Get the
654 // continuation containing the origin.
655 int32_t origOffset
= mOffset
;
656 origOffset
= RenderedToContentOffset(acc
, origOffset
);
657 nsTextFrame
* continuation
= nullptr;
658 int32_t unusedOffsetInContinuation
= 0;
659 frame
->GetChildFrameContainingOffset(
660 origOffset
, true, &unusedOffsetInContinuation
, (nsIFrame
**)&continuation
);
661 MOZ_ASSERT(continuation
);
662 int32_t lineStart
= continuation
->GetContentOffset();
663 if (!aIncludeOrigin
&& lineStart
> 0 && lineStart
== origOffset
) {
664 // A line starts at the origin, but the caller doesn't want this included.
666 continuation
= continuation
->GetPrevContinuation();
667 MOZ_ASSERT(continuation
);
668 lineStart
= continuation
->GetContentOffset();
670 MOZ_ASSERT(lineStart
>= 0);
671 if (lineStart
== 0 && !IsLocalAccAtLineStart(acc
)) {
672 // This is the first line of this text node, but there is something else
673 // on the same line before this text node, so don't return this as a line
675 return TextLeafPoint();
677 lineStart
= static_cast<int32_t>(ContentToRenderedOffset(acc
, lineStart
));
678 return TextLeafPoint(acc
, lineStart
);
681 TextLeafPoint
TextLeafPoint::FindNextLineStartSameLocalAcc(
682 bool aIncludeOrigin
) const {
683 LocalAccessible
* acc
= mAcc
->AsLocal();
685 if (aIncludeOrigin
&& mOffset
== 0 && IsLocalAccAtLineStart(acc
)) {
688 nsIFrame
* frame
= acc
->GetFrame();
690 // This can happen if this is an empty element with display: contents. In
691 // that case, this Accessible contains no lines.
692 return TextLeafPoint();
694 if (!frame
->IsTextFrame()) {
695 // There can't be multiple lines in a non-text leaf.
696 return TextLeafPoint();
698 // Each line of a text node is rendered as a continuation frame. Get the
699 // continuation containing the origin.
700 int32_t origOffset
= mOffset
;
701 origOffset
= RenderedToContentOffset(acc
, origOffset
);
702 nsTextFrame
* continuation
= nullptr;
703 int32_t unusedOffsetInContinuation
= 0;
704 frame
->GetChildFrameContainingOffset(
705 origOffset
, true, &unusedOffsetInContinuation
, (nsIFrame
**)&continuation
);
706 MOZ_ASSERT(continuation
);
708 // A line starts at the origin and the caller wants this included.
709 aIncludeOrigin
&& continuation
->GetContentOffset() == origOffset
&&
710 // If this is the first line of this text node (offset 0), don't treat it
711 // as a line start if there's something else on the line before this text
713 !(origOffset
== 0 && !IsLocalAccAtLineStart(acc
))) {
716 continuation
= continuation
->GetNextContinuation();
718 return TextLeafPoint();
720 int32_t lineStart
= continuation
->GetContentOffset();
721 lineStart
= static_cast<int32_t>(ContentToRenderedOffset(acc
, lineStart
));
722 return TextLeafPoint(acc
, lineStart
);
725 TextLeafPoint
TextLeafPoint::FindLineStartSameRemoteAcc(
726 nsDirection aDirection
, bool aIncludeOrigin
) const {
727 RemoteAccessible
* acc
= mAcc
->AsRemote();
729 auto lines
= acc
->GetCachedTextLines();
731 return TextLeafPoint();
734 // If BinarySearch returns true, mOffset is in the array and index points at
735 // it. If BinarySearch returns false, mOffset is not in the array and index
736 // points at the next line start after mOffset.
737 if (BinarySearch(*lines
, 0, lines
->Length(), mOffset
, &index
)) {
738 if (aIncludeOrigin
) {
741 if (aDirection
== eDirNext
) {
742 // We don't want to include the origin. Get the next line start.
746 MOZ_ASSERT(index
<= lines
->Length());
747 if ((aDirection
== eDirNext
&& index
== lines
->Length()) ||
748 (aDirection
== eDirPrevious
&& index
== 0)) {
749 return TextLeafPoint();
751 // index points at the line start after mOffset.
752 if (aDirection
== eDirPrevious
) {
755 return TextLeafPoint(mAcc
, lines
->ElementAt(index
));
758 TextLeafPoint
TextLeafPoint::FindLineStartSameAcc(
759 nsDirection aDirection
, bool aIncludeOrigin
,
760 bool aIgnoreListItemMarker
) const {
761 TextLeafPoint boundary
;
762 if (aIgnoreListItemMarker
&& aIncludeOrigin
&& mOffset
== 0 &&
763 IsLeafAfterListItemMarker()) {
765 // (1) we are ignoring list markers
766 // (2) we should include origin
767 // (3) we are at the start of a leaf that follows a list item marker
768 // ...then return this point.
772 if (mAcc
->IsLocal()) {
773 boundary
= aDirection
== eDirNext
774 ? FindNextLineStartSameLocalAcc(aIncludeOrigin
)
775 : FindPrevLineStartSameLocalAcc(aIncludeOrigin
);
777 boundary
= FindLineStartSameRemoteAcc(aDirection
, aIncludeOrigin
);
780 if (aIgnoreListItemMarker
&& aDirection
== eDirPrevious
&& !boundary
&&
781 mOffset
!= 0 && IsLeafAfterListItemMarker()) {
783 // (1) we are ignoring list markers
784 // (2) we are searching backwards in accessible
785 // (3) we did not find a line start before this point
786 // (4) we are in a leaf that follows a list item marker
787 // ...then return the first point in this accessible.
788 boundary
= TextLeafPoint(mAcc
, 0);
794 TextLeafPoint
TextLeafPoint::FindPrevWordStartSameAcc(
795 bool aIncludeOrigin
) const {
796 if (mOffset
== 0 && !aIncludeOrigin
) {
797 // We can't go back any further and the caller doesn't want the origin
798 // included, so there's nothing more to do.
799 return TextLeafPoint();
802 mAcc
->AppendTextTo(text
);
803 TextLeafPoint lineStart
= *this;
804 if (!aIncludeOrigin
|| (lineStart
.mOffset
== 1 && text
.Length() == 1 &&
805 text
.CharAt(0) == '\n')) {
806 // We're not interested in a line that starts here, either because
807 // aIncludeOrigin is false or because we're at the end of a line break
811 // A word never starts with a line feed character. If there are multiple
812 // consecutive line feed characters and we're after the first of them, the
813 // previous line start will be a line feed character. Skip this and any prior
814 // consecutive line feed first.
815 for (; lineStart
.mOffset
>= 0 && text
.CharAt(lineStart
.mOffset
) == '\n';
816 --lineStart
.mOffset
) {
818 if (lineStart
.mOffset
< 0) {
819 // There's no line start for our purposes.
820 lineStart
= TextLeafPoint();
823 lineStart
.FindLineStartSameAcc(eDirPrevious
, /* aIncludeOrigin */ true);
825 // Keep walking backward until we find an acceptable word start.
826 intl::WordRange word
;
829 } else if (mOffset
== static_cast<int32_t>(text
.Length())) {
830 word
= WordBreaker::FindWord(
832 StaticPrefs::layout_word_select_stop_at_punctuation()
833 ? FindWordOptions::StopAtPunctuation
834 : FindWordOptions::None
);
836 word
= WordBreaker::FindWord(
838 StaticPrefs::layout_word_select_stop_at_punctuation()
839 ? FindWordOptions::StopAtPunctuation
840 : FindWordOptions::None
);
842 for (;; word
= WordBreaker::FindWord(
843 text
, word
.mBegin
- 1,
844 StaticPrefs::layout_word_select_stop_at_punctuation()
845 ? FindWordOptions::StopAtPunctuation
846 : FindWordOptions::None
)) {
847 if (!aIncludeOrigin
&& static_cast<int32_t>(word
.mBegin
) == mOffset
) {
848 // A word possibly starts at the origin, but the caller doesn't want this
850 MOZ_ASSERT(word
.mBegin
!= 0);
853 if (lineStart
&& static_cast<int32_t>(word
.mBegin
) < lineStart
.mOffset
) {
854 // A line start always starts a new word.
857 if (IsAcceptableWordStart(mAcc
, text
, static_cast<int32_t>(word
.mBegin
))) {
860 if (word
.mBegin
== 0) {
861 // We can't go back any further.
863 // A line start always starts a new word.
866 return TextLeafPoint();
869 return TextLeafPoint(mAcc
, static_cast<int32_t>(word
.mBegin
));
872 TextLeafPoint
TextLeafPoint::FindNextWordStartSameAcc(
873 bool aIncludeOrigin
) const {
875 mAcc
->AppendTextTo(text
);
876 int32_t wordStart
= mOffset
;
877 if (aIncludeOrigin
) {
878 if (wordStart
== 0) {
879 if (IsAcceptableWordStart(mAcc
, text
, 0)) {
883 // The origin might start a word, so search from just before it.
887 TextLeafPoint lineStart
= FindLineStartSameAcc(eDirNext
, aIncludeOrigin
);
889 // A word never starts with a line feed character. If there are multiple
890 // consecutive line feed characters, lineStart will point at the second of
891 // them. Skip this and any subsequent consecutive line feed.
892 for (; lineStart
.mOffset
< static_cast<int32_t>(text
.Length()) &&
893 text
.CharAt(lineStart
.mOffset
) == '\n';
894 ++lineStart
.mOffset
) {
896 if (lineStart
.mOffset
== static_cast<int32_t>(text
.Length())) {
897 // There's no line start for our purposes.
898 lineStart
= TextLeafPoint();
901 // Keep walking forward until we find an acceptable word start.
902 intl::WordBreakIteratorUtf16
wordBreakIter(text
);
903 int32_t previousPos
= wordStart
;
904 Maybe
<uint32_t> nextBreak
= wordBreakIter
.Seek(wordStart
);
906 if (!nextBreak
|| *nextBreak
== text
.Length()) {
908 // A line start always starts a new word.
911 if (StaticPrefs::layout_word_select_stop_at_punctuation()) {
912 // If layout.word_select.stop_at_punctuation is true, we have to look
913 // for punctuation class since it may not break state in UAX#29.
914 for (int32_t i
= previousPos
+ 1;
915 i
< static_cast<int32_t>(text
.Length()); i
++) {
916 if (IsAcceptableWordStart(mAcc
, text
, i
)) {
917 return TextLeafPoint(mAcc
, i
);
921 return TextLeafPoint();
923 wordStart
= AssertedCast
<int32_t>(*nextBreak
);
924 if (lineStart
&& wordStart
> lineStart
.mOffset
) {
925 // A line start always starts a new word.
928 if (IsAcceptableWordStart(mAcc
, text
, wordStart
)) {
932 if (StaticPrefs::layout_word_select_stop_at_punctuation()) {
933 // If layout.word_select.stop_at_punctuation is true, we have to look
934 // for punctuation class since it may not break state in UAX#29.
935 for (int32_t i
= previousPos
+ 1; i
< wordStart
; i
++) {
936 if (IsAcceptableWordStart(mAcc
, text
, i
)) {
937 return TextLeafPoint(mAcc
, i
);
941 previousPos
= wordStart
;
942 nextBreak
= wordBreakIter
.Next();
944 return TextLeafPoint(mAcc
, wordStart
);
947 bool TextLeafPoint::IsCaretAtEndOfLine() const {
948 MOZ_ASSERT(IsCaret());
949 if (LocalAccessible
* acc
= mAcc
->AsLocal()) {
950 HyperTextAccessible
* ht
= HyperTextFor(acc
);
954 // Use HyperTextAccessible::IsCaretAtEndOfLine. Eventually, we'll want to
955 // move that code into TextLeafPoint, but existing code depends on it living
956 // in HyperTextAccessible (including caret events).
957 return ht
->IsCaretAtEndOfLine();
959 return mAcc
->AsRemote()->Document()->IsCaretAtEndOfLine();
962 TextLeafPoint
TextLeafPoint::ActualizeCaret(bool aAdjustAtEndOfLine
) const {
963 MOZ_ASSERT(IsCaret());
964 HyperTextAccessibleBase
* ht
;
966 if (LocalAccessible
* acc
= mAcc
->AsLocal()) {
967 // Use HyperTextAccessible::CaretOffset. Eventually, we'll want to move
968 // that code into TextLeafPoint, but existing code depends on it living in
969 // HyperTextAccessible (including caret events).
970 ht
= HyperTextFor(acc
);
972 return TextLeafPoint();
974 htOffset
= ht
->CaretOffset();
975 if (htOffset
== -1) {
976 return TextLeafPoint();
979 // Ideally, we'd cache the caret as a leaf, but our events are based on
980 // HyperText for now.
981 std::tie(ht
, htOffset
) = mAcc
->AsRemote()->Document()->GetCaret();
983 return TextLeafPoint();
986 if (aAdjustAtEndOfLine
&& htOffset
> 0 && IsCaretAtEndOfLine()) {
987 // It is the same character offset when the caret is visually at the very
988 // end of a line or the start of a new line (soft line break). Getting text
989 // at the line should provide the line with the visual caret. Otherwise,
990 // screen readers will announce the wrong line as the user presses up or
991 // down arrow and land at the end of a line.
994 return ht
->ToTextLeafPoint(htOffset
);
997 TextLeafPoint
TextLeafPoint::FindBoundary(AccessibleTextBoundary aBoundaryType
,
998 nsDirection aDirection
,
999 BoundaryFlags aFlags
) const {
1001 if (aBoundaryType
== nsIAccessibleText::BOUNDARY_CHAR
) {
1002 if (IsCaretAtEndOfLine()) {
1003 // The caret is at the end of the line. Return no character.
1004 return ActualizeCaret(/* aAdjustAtEndOfLine */ false);
1007 return ActualizeCaret().FindBoundary(
1008 aBoundaryType
, aDirection
, aFlags
& BoundaryFlags::eIncludeOrigin
);
1011 bool inEditableAndStopInIt
= (aFlags
& BoundaryFlags::eStopInEditable
) &&
1013 (mAcc
->Parent()->State() & states::EDITABLE
);
1014 if (aBoundaryType
== nsIAccessibleText::BOUNDARY_LINE_END
) {
1015 return FindLineEnd(aDirection
,
1016 inEditableAndStopInIt
1018 : (aFlags
& ~BoundaryFlags::eStopInEditable
));
1020 if (aBoundaryType
== nsIAccessibleText::BOUNDARY_WORD_END
) {
1021 return FindWordEnd(aDirection
,
1022 inEditableAndStopInIt
1024 : (aFlags
& ~BoundaryFlags::eStopInEditable
));
1026 if ((aBoundaryType
== nsIAccessibleText::BOUNDARY_LINE_START
||
1027 aBoundaryType
== nsIAccessibleText::BOUNDARY_PARAGRAPH
) &&
1028 (aFlags
& BoundaryFlags::eIncludeOrigin
) && aDirection
== eDirPrevious
&&
1029 IsEmptyLastLine()) {
1030 // If we're at an empty line at the end of an Accessible, we don't want to
1031 // walk into the previous line. For example, this can happen if the caret
1032 // is positioned on an empty line at the end of a textarea.
1035 bool includeOrigin
= !!(aFlags
& BoundaryFlags::eIncludeOrigin
);
1036 bool ignoreListItemMarker
= !!(aFlags
& BoundaryFlags::eIgnoreListItemMarker
);
1037 Accessible
* lastAcc
= nullptr;
1038 for (TextLeafPoint searchFrom
= *this; searchFrom
;
1039 searchFrom
= searchFrom
.NeighborLeafPoint(
1040 aDirection
, inEditableAndStopInIt
, ignoreListItemMarker
)) {
1041 lastAcc
= searchFrom
.mAcc
;
1042 if (ignoreListItemMarker
&& searchFrom
== *this &&
1043 searchFrom
.mAcc
->Role() == roles::LISTITEM_MARKER
) {
1046 TextLeafPoint boundary
;
1047 // Search for the boundary within the current Accessible.
1048 switch (aBoundaryType
) {
1049 case nsIAccessibleText::BOUNDARY_CHAR
:
1050 if (includeOrigin
) {
1051 boundary
= searchFrom
;
1052 } else if (aDirection
== eDirPrevious
&& searchFrom
.mOffset
> 0) {
1053 boundary
.mAcc
= searchFrom
.mAcc
;
1054 boundary
.mOffset
= searchFrom
.mOffset
- 1;
1055 } else if (aDirection
== eDirNext
&&
1056 searchFrom
.mOffset
+ 1 <
1057 static_cast<int32_t>(
1058 nsAccUtils::TextLength(searchFrom
.mAcc
))) {
1059 boundary
.mAcc
= searchFrom
.mAcc
;
1060 boundary
.mOffset
= searchFrom
.mOffset
+ 1;
1063 case nsIAccessibleText::BOUNDARY_WORD_START
:
1064 if (aDirection
== eDirPrevious
) {
1065 boundary
= searchFrom
.FindPrevWordStartSameAcc(includeOrigin
);
1067 boundary
= searchFrom
.FindNextWordStartSameAcc(includeOrigin
);
1070 case nsIAccessibleText::BOUNDARY_LINE_START
:
1071 boundary
= searchFrom
.FindLineStartSameAcc(aDirection
, includeOrigin
,
1072 ignoreListItemMarker
);
1074 case nsIAccessibleText::BOUNDARY_PARAGRAPH
:
1075 boundary
= searchFrom
.FindParagraphSameAcc(aDirection
, includeOrigin
,
1076 ignoreListItemMarker
);
1079 MOZ_ASSERT_UNREACHABLE();
1086 // The start/end of the Accessible might be a boundary. If so, we must stop
1088 includeOrigin
= true;
1091 MOZ_ASSERT(lastAcc
);
1092 // No further leaf was found. Use the start/end of the first/last leaf.
1093 return TextLeafPoint(
1094 lastAcc
, aDirection
== eDirPrevious
1096 : static_cast<int32_t>(nsAccUtils::TextLength(lastAcc
)));
1099 TextLeafPoint
TextLeafPoint::FindLineEnd(nsDirection aDirection
,
1100 BoundaryFlags aFlags
) const {
1101 if (aDirection
== eDirPrevious
&& IsEmptyLastLine()) {
1102 // If we're at an empty line at the end of an Accessible, we don't want to
1103 // walk into the previous line. For example, this can happen if the caret
1104 // is positioned on an empty line at the end of a textarea.
1105 // Because we want the line end, we must walk back to the line feed
1107 return FindBoundary(nsIAccessibleText::BOUNDARY_CHAR
, eDirPrevious
,
1108 aFlags
& ~BoundaryFlags::eIncludeOrigin
);
1110 if ((aFlags
& BoundaryFlags::eIncludeOrigin
) && IsLineFeedChar()) {
1113 if (aDirection
== eDirPrevious
&& !(aFlags
& BoundaryFlags::eIncludeOrigin
)) {
1114 // If there is a line feed immediately before us, return that.
1115 TextLeafPoint prevChar
=
1116 FindBoundary(nsIAccessibleText::BOUNDARY_CHAR
, eDirPrevious
,
1117 aFlags
& ~BoundaryFlags::eIncludeOrigin
);
1118 if (prevChar
.IsLineFeedChar()) {
1122 TextLeafPoint searchFrom
= *this;
1123 if (aDirection
== eDirNext
&& IsLineFeedChar()) {
1124 // If we search for the next line start from a line feed, we'll get the
1125 // character immediately following the line feed. We actually want the
1126 // next line start after that. Skip the line feed.
1127 searchFrom
= FindBoundary(nsIAccessibleText::BOUNDARY_CHAR
, eDirNext
,
1128 aFlags
& ~BoundaryFlags::eIncludeOrigin
);
1130 TextLeafPoint lineStart
= searchFrom
.FindBoundary(
1131 nsIAccessibleText::BOUNDARY_LINE_START
, aDirection
, aFlags
);
1132 if (aDirection
== eDirNext
&& IsEmptyLastLine()) {
1133 // There is a line feed immediately before us, but that's actually the end
1134 // of the previous line, not the end of our empty line. Don't walk back.
1137 // If there is a line feed before this line start (at the end of the previous
1138 // line), we must return that.
1139 TextLeafPoint prevChar
=
1140 lineStart
.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR
, eDirPrevious
,
1141 aFlags
& ~BoundaryFlags::eIncludeOrigin
);
1142 if (prevChar
&& prevChar
.IsLineFeedChar()) {
1148 bool TextLeafPoint::IsSpace() const {
1149 return GetWordBreakClass(GetChar()) == eWbcSpace
;
1152 TextLeafPoint
TextLeafPoint::FindWordEnd(nsDirection aDirection
,
1153 BoundaryFlags aFlags
) const {
1154 char16_t origChar
= GetChar();
1155 const bool origIsSpace
= GetWordBreakClass(origChar
) == eWbcSpace
;
1156 bool prevIsSpace
= false;
1157 if (aDirection
== eDirPrevious
||
1158 ((aFlags
& BoundaryFlags::eIncludeOrigin
) && origIsSpace
) || !origChar
) {
1159 TextLeafPoint prev
=
1160 FindBoundary(nsIAccessibleText::BOUNDARY_CHAR
, eDirPrevious
,
1161 aFlags
& ~BoundaryFlags::eIncludeOrigin
);
1162 if (aDirection
== eDirPrevious
&& prev
== *this) {
1163 return *this; // Can't go any further.
1165 prevIsSpace
= prev
.IsSpace();
1166 if ((aFlags
& BoundaryFlags::eIncludeOrigin
) &&
1167 (origIsSpace
|| IsDocEdge(eDirNext
)) && !prevIsSpace
) {
1168 // The origin is space or end of document, but the previous
1169 // character is not. This means we're at the end of a word.
1173 TextLeafPoint boundary
= *this;
1174 if (aDirection
== eDirPrevious
&& !prevIsSpace
) {
1175 // If there isn't space immediately before us, first find the start of the
1177 boundary
= FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START
,
1178 eDirPrevious
, aFlags
);
1179 } else if (aDirection
== eDirNext
&&
1180 (origIsSpace
|| (!origChar
&& prevIsSpace
))) {
1181 // We're within the space at the end of the word. Skip over the space. We
1182 // can do that by searching for the next word start.
1183 boundary
= FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START
, eDirNext
,
1184 aFlags
& ~BoundaryFlags::eIncludeOrigin
);
1185 if (boundary
.IsSpace()) {
1186 // The next word starts with a space. This can happen if there is a space
1187 // after or at the start of a block element.
1191 if (aDirection
== eDirNext
) {
1192 BoundaryFlags flags
= aFlags
;
1193 if (IsDocEdge(eDirPrevious
)) {
1194 // If this is the start of the doc don't be inclusive in the word-start
1195 // search because there is no preceding block where this could be a
1197 flags
&= ~BoundaryFlags::eIncludeOrigin
;
1199 boundary
= boundary
.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START
,
1202 // At this point, boundary is either the start of a word or at a space. A
1203 // word ends at the beginning of consecutive space. Therefore, skip back to
1204 // the start of any space before us.
1205 TextLeafPoint prev
= boundary
;
1207 prev
= prev
.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR
, eDirPrevious
,
1208 aFlags
& ~BoundaryFlags::eIncludeOrigin
);
1209 if (prev
== boundary
) {
1210 break; // Can't go any further.
1212 if (!prev
.IsSpace()) {
1220 TextLeafPoint
TextLeafPoint::FindParagraphSameAcc(
1221 nsDirection aDirection
, bool aIncludeOrigin
,
1222 bool aIgnoreListItemMarker
) const {
1223 if (aIncludeOrigin
&& IsDocEdge(eDirPrevious
)) {
1224 // The top of the document is a paragraph boundary.
1228 if (aIgnoreListItemMarker
&& aIncludeOrigin
&& mOffset
== 0 &&
1229 IsLeafAfterListItemMarker()) {
1230 // If we are in a list item and the previous sibling is
1231 // a bullet, the 0 offset in this leaf is a line start.
1235 if (mAcc
->IsTextLeaf() &&
1236 // We don't want to copy strings unnecessarily. See below for the context
1237 // of these individual conditions.
1238 ((aIncludeOrigin
&& mOffset
> 0) || aDirection
== eDirNext
||
1240 // If there is a line feed, a new paragraph begins after it.
1242 mAcc
->AppendTextTo(text
);
1243 if (aIncludeOrigin
&& mOffset
> 0 && text
.CharAt(mOffset
- 1) == '\n') {
1244 return TextLeafPoint(mAcc
, mOffset
);
1246 int32_t lfOffset
= -1;
1247 if (aDirection
== eDirNext
) {
1248 lfOffset
= text
.FindChar('\n', mOffset
);
1249 } else if (mOffset
>= 2) {
1250 // A line feed at mOffset - 1 means the origin begins a new paragraph,
1251 // but we already handled aIncludeOrigin above. Therefore, we search from
1253 lfOffset
= text
.RFindChar('\n', mOffset
- 2);
1255 if (lfOffset
!= -1 && lfOffset
+ 1 < static_cast<int32_t>(text
.Length())) {
1256 return TextLeafPoint(mAcc
, lfOffset
+ 1);
1260 if (aIgnoreListItemMarker
&& mOffset
> 0 && aDirection
== eDirPrevious
&&
1261 IsLeafAfterListItemMarker()) {
1262 // No line breaks were found in the preceding text to this offset.
1263 // If we are in a list item and the previous sibling is
1264 // a bullet, the 0 offset in this leaf is a line start.
1265 return TextLeafPoint(mAcc
, 0);
1268 // Check whether this Accessible begins a paragraph.
1269 if ((!aIncludeOrigin
&& mOffset
== 0) ||
1270 (aDirection
== eDirNext
&& mOffset
> 0)) {
1271 // The caller isn't interested in whether this Accessible begins a
1273 return TextLeafPoint();
1275 Accessible
* prevLeaf
= PrevLeaf(mAcc
);
1276 BlockRule blockRule
;
1277 Pivot
pivot(nsAccUtils::DocumentFor(mAcc
));
1278 Accessible
* prevBlock
= pivot
.Prev(mAcc
, blockRule
);
1279 // Check if we're the first leaf after a block element.
1282 // If there's no previous leaf, we must be the first leaf after the
1285 // A block can be a leaf; e.g. an empty div or paragraph.
1286 prevBlock
== prevLeaf
) {
1287 return TextLeafPoint(mAcc
, 0);
1289 if (prevBlock
->IsAncestorOf(mAcc
)) {
1290 // We're inside the block.
1291 if (!prevBlock
->IsAncestorOf(prevLeaf
)) {
1292 // The previous leaf isn't inside the block. That means we're the first
1293 // leaf in the block.
1294 return TextLeafPoint(mAcc
, 0);
1297 // We aren't inside the block, so the block ends before us.
1298 if (prevBlock
->IsAncestorOf(prevLeaf
)) {
1299 // The previous leaf is inside the block. That means we're the first
1300 // leaf after the block. This case is necessary because a block causes a
1301 // paragraph break both before and after it.
1302 return TextLeafPoint(mAcc
, 0);
1306 if (!prevLeaf
|| prevLeaf
->IsHTMLBr()) {
1307 // We're the first leaf after a line break or the start of the document.
1308 return TextLeafPoint(mAcc
, 0);
1310 if (prevLeaf
->IsTextLeaf()) {
1311 // There's a text leaf before us. Check if it ends with a line feed.
1313 prevLeaf
->AppendTextTo(text
, nsAccUtils::TextLength(prevLeaf
) - 1, 1);
1314 if (text
.CharAt(0) == '\n') {
1315 return TextLeafPoint(mAcc
, 0);
1318 return TextLeafPoint();
1321 bool TextLeafPoint::IsInSpellingError() const {
1322 if (LocalAccessible
* acc
= mAcc
->AsLocal()) {
1323 auto domRanges
= FindDOMSpellingErrors(acc
, mOffset
, mOffset
+ 1);
1324 // If there is a spelling error overlapping this character, we're in a
1326 return !domRanges
.IsEmpty();
1329 RemoteAccessible
* acc
= mAcc
->AsRemote();
1331 if (!acc
->mCachedFields
) {
1334 auto spellingErrors
= acc
->mCachedFields
->GetAttribute
<nsTArray
<int32_t>>(
1335 CacheKey::SpellingErrors
);
1336 if (!spellingErrors
) {
1340 const bool foundOrigin
= BinarySearch(
1341 *spellingErrors
, 0, spellingErrors
->Length(), mOffset
, &index
);
1342 // In spellingErrors, even indices are start offsets, odd indices are end
1344 const bool foundStart
= index
% 2 == 0;
1346 // mOffset is a spelling error boundary. If it's a start offset, we're in a
1350 // index points at the next spelling error boundary after mOffset.
1352 return false; // No spelling errors before mOffset.
1355 // We're not in a spelling error because it starts after mOffset.
1358 // A spelling error ends after mOffset.
1362 TextLeafPoint
TextLeafPoint::FindSpellingErrorSameAcc(
1363 nsDirection aDirection
, bool aIncludeOrigin
) const {
1364 if (!aIncludeOrigin
&& mOffset
== 0 && aDirection
== eDirPrevious
) {
1365 return TextLeafPoint();
1367 if (LocalAccessible
* acc
= mAcc
->AsLocal()) {
1368 // We want to find both start and end points, so we pass true for
1371 aDirection
== eDirNext
1372 ? FindDOMSpellingErrors(acc
, mOffset
,
1373 nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
,
1374 /* aAllowAdjacent */ true)
1375 : FindDOMSpellingErrors(acc
, 0, mOffset
,
1376 /* aAllowAdjacent */ true);
1377 nsINode
* node
= acc
->GetNode();
1378 if (aDirection
== eDirNext
) {
1379 for (nsRange
* domRange
: domRanges
) {
1380 if (domRange
->GetStartContainer() == node
) {
1381 int32_t matchOffset
= static_cast<int32_t>(ContentToRenderedOffset(
1382 acc
, static_cast<int32_t>(domRange
->StartOffset())));
1383 if ((aIncludeOrigin
&& matchOffset
== mOffset
) ||
1384 matchOffset
> mOffset
) {
1385 return TextLeafPoint(mAcc
, matchOffset
);
1388 if (domRange
->GetEndContainer() == node
) {
1389 int32_t matchOffset
= static_cast<int32_t>(ContentToRenderedOffset(
1390 acc
, static_cast<int32_t>(domRange
->EndOffset())));
1391 if ((aIncludeOrigin
&& matchOffset
== mOffset
) ||
1392 matchOffset
> mOffset
) {
1393 return TextLeafPoint(mAcc
, matchOffset
);
1398 for (nsRange
* domRange
: Reversed(domRanges
)) {
1399 if (domRange
->GetEndContainer() == node
) {
1400 int32_t matchOffset
= static_cast<int32_t>(ContentToRenderedOffset(
1401 acc
, static_cast<int32_t>(domRange
->EndOffset())));
1402 if ((aIncludeOrigin
&& matchOffset
== mOffset
) ||
1403 matchOffset
< mOffset
) {
1404 return TextLeafPoint(mAcc
, matchOffset
);
1407 if (domRange
->GetStartContainer() == node
) {
1408 int32_t matchOffset
= static_cast<int32_t>(ContentToRenderedOffset(
1409 acc
, static_cast<int32_t>(domRange
->StartOffset())));
1410 if ((aIncludeOrigin
&& matchOffset
== mOffset
) ||
1411 matchOffset
< mOffset
) {
1412 return TextLeafPoint(mAcc
, matchOffset
);
1417 return TextLeafPoint();
1420 RemoteAccessible
* acc
= mAcc
->AsRemote();
1422 if (!acc
->mCachedFields
) {
1423 return TextLeafPoint();
1425 auto spellingErrors
= acc
->mCachedFields
->GetAttribute
<nsTArray
<int32_t>>(
1426 CacheKey::SpellingErrors
);
1427 if (!spellingErrors
) {
1428 return TextLeafPoint();
1431 if (BinarySearch(*spellingErrors
, 0, spellingErrors
->Length(), mOffset
,
1433 // mOffset is in spellingErrors.
1434 if (aIncludeOrigin
) {
1437 if (aDirection
== eDirNext
) {
1438 // We don't want the origin, so move to the next spelling error boundary
1443 // index points at the next spelling error boundary after mOffset.
1444 if (aDirection
== eDirNext
) {
1445 if (spellingErrors
->Length() == index
) {
1446 return TextLeafPoint(); // No spelling error boundary after us.
1448 return TextLeafPoint(mAcc
, (*spellingErrors
)[index
]);
1451 return TextLeafPoint(); // No spelling error boundary before us.
1453 // Decrement index so it points at a spelling error boundary before mOffset.
1455 if ((*spellingErrors
)[index
] == -1) {
1456 MOZ_ASSERT(index
== 0);
1457 // A spelling error starts before mAcc.
1458 return TextLeafPoint();
1460 return TextLeafPoint(mAcc
, (*spellingErrors
)[index
]);
1463 TextLeafPoint
TextLeafPoint::NeighborLeafPoint(
1464 nsDirection aDirection
, bool aIsEditable
,
1465 bool aIgnoreListItemMarker
) const {
1466 Accessible
* acc
= aDirection
== eDirPrevious
1467 ? PrevLeaf(mAcc
, aIsEditable
, aIgnoreListItemMarker
)
1468 : NextLeaf(mAcc
, aIsEditable
, aIgnoreListItemMarker
);
1470 return TextLeafPoint();
1473 return TextLeafPoint(
1474 acc
, aDirection
== eDirPrevious
1475 ? static_cast<int32_t>(nsAccUtils::TextLength(acc
)) - 1
1479 LayoutDeviceIntRect
TextLeafPoint::ComputeBoundsFromFrame() const {
1480 LocalAccessible
* local
= mAcc
->AsLocal();
1481 MOZ_ASSERT(local
, "Can't compute bounds in frame from non-local acc");
1482 nsIFrame
* frame
= local
->GetFrame();
1483 MOZ_ASSERT(frame
, "No frame found for acc!");
1485 if (!frame
|| !frame
->IsTextFrame()) {
1486 return local
->Bounds();
1489 // Substring must be entirely within the same text node.
1490 MOZ_ASSERT(frame
->IsPrimaryFrame(),
1491 "Cannot compute content offset on non-primary frame");
1492 nsIFrame::RenderedText text
= frame
->GetRenderedText(
1493 mOffset
, mOffset
+ 1, nsIFrame::TextOffsetType::OffsetsInRenderedText
,
1494 nsIFrame::TrailingWhitespace::DontTrim
);
1495 int32_t contentOffset
= text
.mOffsetWithinNodeText
;
1496 int32_t contentOffsetInFrame
;
1497 // Get the right frame continuation -- not really a child, but a sibling of
1498 // the primary frame passed in
1499 nsresult rv
= frame
->GetChildFrameContainingOffset(
1500 contentOffset
, true, &contentOffsetInFrame
, &frame
);
1501 NS_ENSURE_SUCCESS(rv
, LayoutDeviceIntRect());
1503 // Start with this frame's screen rect, which we will shrink based on
1504 // the char we care about within it.
1505 nsRect frameScreenRect
= frame
->GetScreenRectInAppUnits();
1507 // Add the point where the char starts to the frameScreenRect
1508 nsPoint frameTextStartPoint
;
1509 rv
= frame
->GetPointFromOffset(contentOffset
, &frameTextStartPoint
);
1510 NS_ENSURE_SUCCESS(rv
, LayoutDeviceIntRect());
1512 // Use the next offset to calculate the width
1513 // XXX(morgan) does this work for vertical text?
1514 nsPoint frameTextEndPoint
;
1515 rv
= frame
->GetPointFromOffset(contentOffset
+ 1, &frameTextEndPoint
);
1516 NS_ENSURE_SUCCESS(rv
, LayoutDeviceIntRect());
1518 frameScreenRect
.SetRectX(
1519 frameScreenRect
.X() +
1520 std::min(frameTextStartPoint
.x
, frameTextEndPoint
.x
),
1521 mozilla::Abs(frameTextStartPoint
.x
- frameTextEndPoint
.x
));
1523 nsPresContext
* presContext
= local
->Document()->PresContext();
1524 return LayoutDeviceIntRect::FromAppUnitsToNearest(
1525 frameScreenRect
, presContext
->AppUnitsPerDevPixel());
1529 nsTArray
<int32_t> TextLeafPoint::GetSpellingErrorOffsets(
1530 LocalAccessible
* aAcc
) {
1531 nsINode
* node
= aAcc
->GetNode();
1532 auto domRanges
= FindDOMSpellingErrors(
1533 aAcc
, 0, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
);
1534 // Our offsets array will contain two offsets for each range: one for the
1535 // start, one for the end. That is, the array is of the form:
1536 // [r1start, r1end, r2start, r2end, ...]
1537 nsTArray
<int32_t> offsets(domRanges
.Length() * 2);
1538 for (nsRange
* domRange
: domRanges
) {
1539 if (domRange
->GetStartContainer() == node
) {
1540 offsets
.AppendElement(static_cast<int32_t>(ContentToRenderedOffset(
1541 aAcc
, static_cast<int32_t>(domRange
->StartOffset()))));
1543 // This range overlaps aAcc, but starts before it.
1544 // This can only happen for the first range.
1545 MOZ_ASSERT(domRange
== *domRanges
.begin() && offsets
.IsEmpty());
1546 // Using -1 here means this won't be treated as the start of a spelling
1547 // error range, while still indicating that we're within a spelling error.
1548 offsets
.AppendElement(-1);
1550 if (domRange
->GetEndContainer() == node
) {
1551 offsets
.AppendElement(static_cast<int32_t>(ContentToRenderedOffset(
1552 aAcc
, static_cast<int32_t>(domRange
->EndOffset()))));
1554 // This range overlaps aAcc, but ends after it.
1555 // This can only happen for the last range.
1556 MOZ_ASSERT(domRange
== *domRanges
.rbegin());
1557 // We don't append -1 here because this would just make things harder for
1565 void TextLeafPoint::UpdateCachedSpellingError(dom::Document
* aDocument
,
1566 const nsRange
& aRange
) {
1567 DocAccessible
* docAcc
= GetExistingDocAccessible(aDocument
);
1571 LocalAccessible
* startAcc
= docAcc
->GetAccessible(aRange
.GetStartContainer());
1572 LocalAccessible
* endAcc
= docAcc
->GetAccessible(aRange
.GetEndContainer());
1573 if (!startAcc
|| !endAcc
) {
1576 for (Accessible
* acc
= startAcc
; acc
; acc
= NextLeaf(acc
)) {
1577 if (acc
->IsTextLeaf()) {
1578 docAcc
->QueueCacheUpdate(acc
->AsLocal(), CacheDomain::Spelling
);
1580 if (acc
== endAcc
) {
1581 // Subtle: We check this here rather than in the loop condition because
1582 // we want to include endAcc but stop once we reach it. Putting it in the
1583 // loop condition would mean we stop at endAcc, but we would also exclude
1584 // it; i.e. we wouldn't push the cache for it.
1590 already_AddRefed
<AccAttributes
> TextLeafPoint::GetTextAttributesLocalAcc(
1591 bool aIncludeDefaults
) const {
1592 LocalAccessible
* acc
= mAcc
->AsLocal();
1594 MOZ_ASSERT(acc
->IsText());
1595 // TextAttrsMgr wants a HyperTextAccessible.
1596 LocalAccessible
* parent
= acc
->LocalParent();
1597 HyperTextAccessible
* hyperAcc
= parent
->AsHyperText();
1598 MOZ_ASSERT(hyperAcc
);
1599 RefPtr
<AccAttributes
> attributes
= new AccAttributes();
1601 TextAttrsMgr
mgr(hyperAcc
, aIncludeDefaults
, acc
,
1602 acc
? acc
->IndexInParent() : -1);
1603 mgr
.GetAttributes(attributes
, nullptr, nullptr);
1605 return attributes
.forget();
1608 already_AddRefed
<AccAttributes
> TextLeafPoint::GetTextAttributes(
1609 bool aIncludeDefaults
) const {
1610 if (!mAcc
->IsText()) {
1613 RefPtr
<AccAttributes
> attrs
;
1614 if (mAcc
->IsLocal()) {
1615 attrs
= GetTextAttributesLocalAcc(aIncludeDefaults
);
1617 attrs
= new AccAttributes();
1618 if (aIncludeDefaults
) {
1619 Accessible
* parent
= mAcc
->Parent();
1620 if (parent
&& parent
->IsRemote() && parent
->IsHyperText()) {
1621 if (auto defAttrs
= parent
->AsRemote()->GetCachedTextAttributes()) {
1622 defAttrs
->CopyTo(attrs
);
1626 if (auto thisAttrs
= mAcc
->AsRemote()->GetCachedTextAttributes()) {
1627 thisAttrs
->CopyTo(attrs
);
1630 if (IsInSpellingError()) {
1631 attrs
->SetAttribute(nsGkAtoms::invalid
, nsGkAtoms::spelling
);
1633 return attrs
.forget();
1636 TextLeafPoint
TextLeafPoint::FindTextAttrsStart(nsDirection aDirection
,
1637 bool aIncludeOrigin
) const {
1639 return ActualizeCaret().FindTextAttrsStart(aDirection
, aIncludeOrigin
);
1641 const bool isRemote
= mAcc
->IsRemote();
1642 RefPtr
<const AccAttributes
> lastAttrs
=
1643 isRemote
? mAcc
->AsRemote()->GetCachedTextAttributes()
1644 : GetTextAttributesLocalAcc();
1645 if (aIncludeOrigin
&& aDirection
== eDirNext
&& mOffset
== 0) {
1646 // Even when searching forward, the only way to know whether the origin is
1647 // the start of a text attrs run is to compare with the previous sibling.
1648 // Anything other than text breaks an attrs run.
1649 TextLeafPoint point
;
1650 point
.mAcc
= mAcc
->PrevSibling();
1651 if (!point
.mAcc
|| !point
.mAcc
->IsText()) {
1654 // For RemoteAccessible, we can get attributes from the cache without any
1655 // calculation or copying.
1656 RefPtr
<const AccAttributes
> attrs
=
1657 isRemote
? point
.mAcc
->AsRemote()->GetCachedTextAttributes()
1658 : point
.GetTextAttributesLocalAcc();
1659 if (attrs
&& lastAttrs
&& !attrs
->Equal(lastAttrs
)) {
1663 TextLeafPoint lastPoint
= *this;
1665 if (TextLeafPoint spelling
= lastPoint
.FindSpellingErrorSameAcc(
1666 aDirection
, aIncludeOrigin
&& lastPoint
.mAcc
== mAcc
)) {
1667 // A spelling error starts or ends somewhere in the Accessible we're
1668 // considering. This causes an attribute change, so return that point.
1671 TextLeafPoint point
;
1672 point
.mAcc
= aDirection
== eDirNext
? lastPoint
.mAcc
->NextSibling()
1673 : lastPoint
.mAcc
->PrevSibling();
1674 if (!point
.mAcc
|| !point
.mAcc
->IsText()) {
1677 RefPtr
<const AccAttributes
> attrs
=
1678 isRemote
? point
.mAcc
->AsRemote()->GetCachedTextAttributes()
1679 : point
.GetTextAttributesLocalAcc();
1680 if (attrs
&& lastAttrs
&& !attrs
->Equal(lastAttrs
)) {
1681 // The attributes change here. If we're moving forward, we want to
1682 // return this point. If we're moving backward, we've now moved before
1683 // the start of the attrs run containing the origin, so return that start
1684 // point; i.e. the start of the last Accessible we hit.
1685 if (aDirection
== eDirPrevious
) {
1689 if (!aIncludeOrigin
&& point
== *this) {
1690 MOZ_ASSERT(aDirection
== eDirPrevious
);
1691 // The origin is the start of an attrs run, but the caller doesn't want
1692 // the origin included.
1698 if (aDirection
== eDirPrevious
) {
1699 // On the next iteration, we want to search for spelling errors from the
1700 // end of this Accessible.
1702 static_cast<int32_t>(nsAccUtils::TextLength(point
.mAcc
));
1706 // We couldn't move any further. Use the start/end.
1707 return TextLeafPoint(
1709 aDirection
== eDirPrevious
1711 : static_cast<int32_t>(nsAccUtils::TextLength(lastPoint
.mAcc
)));
1714 LayoutDeviceIntRect
TextLeafPoint::CharBounds() {
1715 if (mAcc
&& !mAcc
->IsText()) {
1716 // If we're dealing with an empty container, return the
1717 // accessible's non-text bounds.
1718 return mAcc
->Bounds();
1721 if (!mAcc
|| (mAcc
->IsRemote() && !mAcc
->AsRemote()->mCachedFields
)) {
1722 return LayoutDeviceIntRect();
1725 if (LocalAccessible
* local
= mAcc
->AsLocal()) {
1726 if (!local
->IsTextLeaf() || nsAccUtils::TextLength(local
) == 0) {
1727 // Empty content, use our own bounds to at least get x,y coordinates
1728 return local
->Bounds();
1732 static_cast<uint32_t>(mOffset
) >= nsAccUtils::TextLength(local
)) {
1733 // It's valid for a caller to query the length because the caret might be
1734 // at the end of editable text. In that case, we should just silently
1735 // return. However, we assert that the offset isn't greater than the
1738 static_cast<uint32_t>(mOffset
) <= nsAccUtils::TextLength(local
),
1740 return LayoutDeviceIntRect();
1743 LayoutDeviceIntRect bounds
= ComputeBoundsFromFrame();
1745 // This document may have a resolution set, we will need to multiply
1746 // the document-relative coordinates by that value and re-apply the doc's
1747 // screen coordinates.
1748 nsPresContext
* presContext
= local
->Document()->PresContext();
1749 nsIFrame
* rootFrame
= presContext
->PresShell()->GetRootFrame();
1750 LayoutDeviceIntRect orgRectPixels
=
1751 LayoutDeviceIntRect::FromAppUnitsToNearest(
1752 rootFrame
->GetScreenRectInAppUnits(),
1753 presContext
->AppUnitsPerDevPixel());
1754 bounds
.MoveBy(-orgRectPixels
.X(), -orgRectPixels
.Y());
1755 bounds
.ScaleRoundOut(presContext
->PresShell()->GetResolution());
1756 bounds
.MoveBy(orgRectPixels
.X(), orgRectPixels
.Y());
1760 RemoteAccessible
* remote
= mAcc
->AsRemote();
1761 nsRect charBounds
= remote
->GetCachedCharRect(mOffset
);
1762 if (!charBounds
.IsEmpty()) {
1763 return remote
->BoundsWithOffset(Some(charBounds
));
1766 return LayoutDeviceIntRect();
1769 bool TextLeafPoint::ContainsPoint(int32_t aX
, int32_t aY
) {
1770 if (mAcc
&& !mAcc
->IsText()) {
1771 // If we're dealing with an empty embedded object, use the
1772 // accessible's non-text bounds.
1773 return mAcc
->Bounds().Contains(aX
, aY
);
1776 return CharBounds().Contains(aX
, aY
);
1779 bool TextLeafRange::Crop(Accessible
* aContainer
) {
1780 TextLeafPoint
containerStart(aContainer
, 0);
1781 TextLeafPoint
containerEnd(aContainer
,
1782 nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
);
1784 if (mEnd
< containerStart
|| containerEnd
< mStart
) {
1785 // The range ends before the container, or starts after it.
1789 if (mStart
< containerStart
) {
1790 // If range start is before container start, adjust range start to
1791 // start of container.
1792 mStart
= containerStart
;
1795 if (containerEnd
< mEnd
) {
1796 // If range end is after container end, adjust range end to end of
1798 mEnd
= containerEnd
;
1804 LayoutDeviceIntRect
TextLeafRange::Bounds() const {
1805 if (mEnd
== mStart
|| mEnd
< mStart
) {
1806 return LayoutDeviceIntRect();
1809 bool locatedFinalLine
= false;
1810 TextLeafPoint currPoint
= mStart
;
1811 LayoutDeviceIntRect result
= currPoint
.CharBounds();
1813 // Union the first and last chars of each line to create a line rect. Then,
1814 // union the lines together.
1815 while (!locatedFinalLine
) {
1816 // Fetch the last point in the current line by getting the
1817 // start of the next line and going back one char. We don't
1818 // use BOUNDARY_LINE_END here because it is equivalent to LINE_START when
1819 // the line doesn't end with a line feed character.
1820 TextLeafPoint lineStartPoint
= currPoint
.FindBoundary(
1821 nsIAccessibleText::BOUNDARY_LINE_START
, eDirNext
);
1822 TextLeafPoint lastPointInLine
= lineStartPoint
.FindBoundary(
1823 nsIAccessibleText::BOUNDARY_CHAR
, eDirPrevious
);
1824 // If currPoint is the end of the document, lineStartPoint will be equal
1825 // to currPoint and we would be in an endless loop.
1826 if (lineStartPoint
== currPoint
|| mEnd
<= lastPointInLine
) {
1827 lastPointInLine
= mEnd
;
1828 locatedFinalLine
= true;
1831 LayoutDeviceIntRect currLine
= currPoint
.CharBounds();
1832 currLine
.UnionRect(currLine
, lastPointInLine
.CharBounds());
1833 result
.UnionRect(result
, currLine
);
1835 currPoint
= lineStartPoint
;
1841 bool TextLeafRange::SetSelection(int32_t aSelectionNum
) const {
1842 if (!mStart
|| !mEnd
|| mStart
.mAcc
->IsLocal() != mEnd
.mAcc
->IsLocal()) {
1846 if (mStart
.mAcc
->IsRemote()) {
1847 DocAccessibleParent
* doc
= mStart
.mAcc
->AsRemote()->Document();
1848 if (doc
!= mEnd
.mAcc
->AsRemote()->Document()) {
1852 Unused
<< doc
->SendSetTextSelection(mStart
.mAcc
->ID(), mStart
.mOffset
,
1853 mEnd
.mAcc
->ID(), mEnd
.mOffset
,
1858 bool reversed
= mEnd
< mStart
;
1859 auto [startContent
, startContentOffset
] =
1860 !reversed
? mStart
.ToDOMPoint(false) : mEnd
.ToDOMPoint(false);
1861 auto [endContent
, endContentOffset
] =
1862 !reversed
? mEnd
.ToDOMPoint(false) : mStart
.ToDOMPoint(false);
1864 if (!startContent
|| !endContent
) {
1868 RefPtr
<dom::Selection
> domSel
= GetDOMSelection(startContent
, endContent
);
1873 uint32_t rangeCount
= domSel
->RangeCount();
1874 RefPtr
<nsRange
> domRange
= nullptr;
1875 if (aSelectionNum
== static_cast<int32_t>(rangeCount
) || aSelectionNum
< 0) {
1876 domRange
= nsRange::Create(startContent
);
1878 domRange
= domSel
->GetRangeAt(AssertedCast
<uint32_t>(aSelectionNum
));
1884 domRange
->SetStart(startContent
, startContentOffset
);
1885 domRange
->SetEnd(endContent
, endContentOffset
);
1887 // If this is not a new range, notify selection listeners that the existing
1888 // selection range has changed. Otherwise, just add the new range.
1889 if (aSelectionNum
!= static_cast<int32_t>(rangeCount
)) {
1890 domSel
->RemoveRangeAndUnselectFramesAndNotifyListeners(*domRange
,
1894 IgnoredErrorResult err
;
1895 domSel
->AddRangeAndSelectFramesAndNotifyListeners(*domRange
, err
);
1896 if (!err
.Failed()) {
1897 // Changing the direction of the selection assures that the caret
1898 // will be at the logical end of the selection.
1899 domSel
->SetDirection(reversed
? eDirPrevious
: eDirNext
);
1906 void TextLeafRange::ScrollIntoView(uint32_t aScrollType
) const {
1907 if (!mStart
|| !mEnd
|| mStart
.mAcc
->IsLocal() != mEnd
.mAcc
->IsLocal()) {
1911 if (mStart
.mAcc
->IsRemote()) {
1912 DocAccessibleParent
* doc
= mStart
.mAcc
->AsRemote()->Document();
1913 if (doc
!= mEnd
.mAcc
->AsRemote()->Document()) {
1914 // Can't scroll range that spans docs.
1918 Unused
<< doc
->SendScrollTextLeafRangeIntoView(
1919 mStart
.mAcc
->ID(), mStart
.mOffset
, mEnd
.mAcc
->ID(), mEnd
.mOffset
,
1924 auto [startContent
, startContentOffset
] = mStart
.ToDOMPoint();
1925 auto [endContent
, endContentOffset
] = mEnd
.ToDOMPoint();
1927 if (!startContent
|| !endContent
) {
1932 RefPtr
<nsRange
> domRange
= nsRange::Create(startContent
, startContentOffset
,
1933 endContent
, endContentOffset
, er
);
1938 nsCoreUtils::ScrollSubstringTo(mStart
.mAcc
->AsLocal()->GetFrame(), domRange
,
1942 TextLeafRange::Iterator
TextLeafRange::Iterator::BeginIterator(
1943 const TextLeafRange
& aRange
) {
1944 Iterator
result(aRange
);
1946 result
.mSegmentStart
= aRange
.mStart
;
1947 if (aRange
.mStart
.mAcc
== aRange
.mEnd
.mAcc
) {
1948 result
.mSegmentEnd
= aRange
.mEnd
;
1950 result
.mSegmentEnd
= TextLeafPoint(
1951 aRange
.mStart
.mAcc
, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
);
1957 TextLeafRange::Iterator
TextLeafRange::Iterator::EndIterator(
1958 const TextLeafRange
& aRange
) {
1959 Iterator
result(aRange
);
1961 result
.mSegmentEnd
= TextLeafPoint();
1962 result
.mSegmentStart
= TextLeafPoint();
1967 TextLeafRange::Iterator
& TextLeafRange::Iterator::operator++() {
1968 if (mSegmentEnd
.mAcc
== mRange
.mEnd
.mAcc
) {
1969 mSegmentEnd
= TextLeafPoint();
1970 mSegmentStart
= TextLeafPoint();
1974 if (Accessible
* nextLeaf
= NextLeaf(mSegmentEnd
.mAcc
)) {
1975 mSegmentStart
= TextLeafPoint(nextLeaf
, 0);
1976 if (nextLeaf
== mRange
.mEnd
.mAcc
) {
1977 mSegmentEnd
= TextLeafPoint(nextLeaf
, mRange
.mEnd
.mOffset
);
1980 TextLeafPoint(nextLeaf
, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
);
1983 mSegmentEnd
= TextLeafPoint();
1984 mSegmentStart
= TextLeafPoint();
1990 } // namespace mozilla::a11y