Bug 1889091 - Part 4: Remove extra stack pointer move. r=jandem
[gecko.git] / accessible / base / TextLeafRange.cpp
blob9b658e12af5b8a67a8166a6f9407480c1f76f246
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"
29 #include "nsINode.h"
30 #include "nsRange.h"
31 #include "nsStyleStructInlines.h"
32 #include "nsTArray.h"
33 #include "nsTextFrame.h"
34 #include "nsUnicharUtils.h"
35 #include "Pivot.h"
36 #include "TextAttrs.h"
38 using mozilla::intl::WordBreaker;
39 using FindWordOptions = mozilla::intl::WordBreaker::FindWordOptions;
41 namespace mozilla::a11y {
43 /*** Helpers ***/
45 /**
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());
59 if (!frame) {
60 MOZ_ASSERT(!aAcc->HasOwnContent() || aAcc->IsHTMLBr(),
61 "No text frame because this is a XUL label[value] text leaf or "
62 "a BR element.");
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());
83 if (!frame) {
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 {
104 public:
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;
129 private:
130 bool mIgnoreListItemMarker;
133 static HyperTextAccessible* HyperTextFor(LocalAccessible* aAcc) {
134 for (LocalAccessible* acc = aAcc; acc; acc = acc->LocalParent()) {
135 if (HyperTextAccessible* ht = acc->AsHyperText()) {
136 return ht;
139 return nullptr;
142 static Accessible* NextLeaf(Accessible* aOrigin, bool aIsEditable = false,
143 bool aIgnoreListItemMarker = false) {
144 MOZ_ASSERT(aOrigin);
145 Accessible* doc = nsAccUtils::DocumentFor(aOrigin);
146 Pivot pivot(doc);
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)
151 ? leaf
152 : nullptr;
154 return leaf;
157 static Accessible* PrevLeaf(Accessible* aOrigin, bool aIsEditable = false,
158 bool aIgnoreListItemMarker = false) {
159 MOZ_ASSERT(aOrigin);
160 Accessible* doc = nsAccUtils::DocumentFor(aOrigin);
161 Pivot pivot(doc);
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)
166 ? leaf
167 : nullptr;
169 return leaf;
172 static nsIFrame* GetFrameInBlock(const LocalAccessible* aAcc) {
173 dom::HTMLInputElement* input =
174 dom::HTMLInputElement::FromNodeOrNull(aAcc->GetContent());
175 if (!input) {
176 if (LocalAccessible* parent = aAcc->LocalParent()) {
177 input = dom::HTMLInputElement::FromNodeOrNull(parent->GetContent());
181 if (input) {
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.
195 return true;
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;
206 if (!prevLocal) {
207 // There's nothing before us, so this is the start of the first line.
208 return true;
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();
216 MOZ_ASSERT(doc);
217 for (LocalAccessible* parent = aAcc->LocalParent(); parent && parent != doc;
218 parent = parent->LocalParent()) {
219 if (parent == listItem) {
220 return false;
225 nsIFrame* thisFrame = GetFrameInBlock(aAcc);
226 if (!thisFrame) {
227 return false;
230 nsIFrame* prevFrame = GetFrameInBlock(prevLocal);
231 if (!prevFrame) {
232 return false;
235 auto [thisBlock, thisLineFrame] = thisFrame->GetContainingBlockForLine(
236 /* aLockScroll */ false);
237 if (!thisBlock) {
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.
240 return true;
243 // The previous leaf might cross lines. We want to compare against the last
244 // line.
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.
251 return true;
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.
256 bool found = false;
257 block->SetupLineCursorForQuery();
258 nsBlockInFlowLineIterator prevIt(block, prevLineFrame, &found);
259 if (!found) {
260 // Error; play it safe.
261 return true;
263 found = false;
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.
275 return true;
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
285 * space specially.
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;
293 switch (aChar) {
294 case ' ':
295 case kCharNbsp:
296 case '\t':
297 case '\f':
298 case '\n':
299 case '\r':
300 return eWbcSpace;
301 default:
302 break;
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 {
313 public:
314 PrevWordBreakClassWalker(Accessible* aAcc, const nsAString& aText,
315 int32_t aOffset)
316 : mAcc(aAcc), mText(aText), mOffset(aOffset) {
317 mClass = GetWordBreakClass(mText.CharAt(mOffset));
320 WordBreakClass CurClass() { return mClass; }
322 Maybe<WordBreakClass> PrevClass() {
323 for (;;) {
324 if (!PrevChar()) {
325 return Nothing();
327 WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset));
328 if (curClass != mClass) {
329 mClass = curClass;
330 return Some(curClass);
333 MOZ_ASSERT_UNREACHABLE();
334 return Nothing();
337 bool IsStartOfGroup() {
338 if (!PrevChar()) {
339 // There are no characters before us.
340 return true;
342 WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset));
343 // We wanted to peek at the previous character, not really move to it.
344 ++mOffset;
345 return curClass != mClass;
348 private:
349 bool PrevChar() {
350 if (mOffset > 0) {
351 --mOffset;
352 return true;
354 if (!mAcc) {
355 // PrevChar was called already and failed.
356 return false;
358 mAcc = PrevLeaf(mAcc);
359 if (!mAcc) {
360 return false;
362 mText.Truncate();
363 mAcc->AppendTextTo(mText);
364 mOffset = static_cast<int32_t>(mText.Length()) - 1;
365 return true;
368 Accessible* mAcc;
369 nsAutoString mText;
370 int32_t mOffset;
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,
380 int32_t aOffset) {
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
384 // start of a word.
385 return false;
387 WordBreakClass curClass = walker.CurClass();
388 if (curClass == eWbcSpace) {
389 // Space isn't the start of a word.
390 return false;
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).
395 return false;
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.
400 return true;
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.
406 return false;
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.
412 return false;
414 return true;
417 class BlockRule : public PivotRule {
418 public:
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()) {
441 return {};
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;
448 if (!domSel) {
449 return {};
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);
460 return domRanges;
464 * Given two DOM nodes get DOM Selection object that is common
465 * to both of them.
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.
479 return nullptr;
482 return startFrameSel ? startFrameSel->GetSelection(SelectionType::eNormal)
483 : nullptr;
486 std::pair<nsIContent*, int32_t> TextLeafPoint::ToDOMPoint(
487 bool aIncludeGenerated) const {
488 if (!(*this) || !mAcc->IsLocal()) {
489 MOZ_ASSERT_UNREACHABLE("Invalid point");
490 return {nullptr, 0};
493 nsIContent* content = mAcc->AsLocal()->GetContent();
494 nsIFrame* frame = content ? content->GetPrimaryFrame() : nullptr;
495 MOZ_ASSERT(frame);
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()
501 ? content
502 : content->GetParentElement();
503 auto parent = generatedElement ? generatedElement->GetParent() : nullptr;
504 MOZ_ASSERT(parent);
505 if (parent) {
506 if (generatedElement->IsGeneratedContentContainerForAfter()) {
507 // Use the end offset of the parent element for trailing generated
508 // content.
509 return {parent, parent->GetChildCount()};
512 if (generatedElement->IsGeneratedContentContainerForBefore() ||
513 generatedElement->IsGeneratedContentContainerForMarker()) {
514 // Use the start offset of the parent element for leading generated
515 // content.
516 return {parent, 0};
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};
539 return {content, 0};
542 return {content, RenderedToContentOffset(mAcc->AsLocal(), mOffset)};
545 /*** TextLeafPoint ***/
547 TextLeafPoint::TextLeafPoint(Accessible* aAcc, int32_t aOffset) {
548 if (!aAcc) {
549 // Construct an invalid point.
550 mAcc = nullptr;
551 mOffset = 0;
552 return;
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()) {
563 return nullptr;
565 return aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
566 ? acc->FirstChild()
567 : acc->LastChild();
570 for (Accessible* acc = GetChild(aAcc); acc; acc = GetChild(acc)) {
571 mAcc = acc;
573 mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
575 : nsAccUtils::TextLength(mAcc);
576 return;
578 mAcc = aAcc;
579 mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
580 ? aOffset
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)) &&
601 !NextLeaf(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) {
612 return true;
614 if (!mAcc->IsTextLeaf()) {
615 return false;
617 if (mOffset < static_cast<int32_t>(nsAccUtils::TextLength(mAcc))) {
618 return false;
620 nsAutoString text;
621 mAcc->AppendTextTo(text, mOffset - 1, 1);
622 return text.CharAt(0) == '\n';
625 char16_t TextLeafPoint::GetChar() const {
626 nsAutoString text;
627 mAcc->AppendTextTo(text, mOffset, 1);
628 return text.CharAt(0);
631 TextLeafPoint TextLeafPoint::FindPrevLineStartSameLocalAcc(
632 bool aIncludeOrigin) const {
633 LocalAccessible* acc = mAcc->AsLocal();
634 MOZ_ASSERT(acc);
635 if (mOffset == 0) {
636 if (aIncludeOrigin && IsLocalAccAtLineStart(acc)) {
637 return *this;
639 return TextLeafPoint();
641 nsIFrame* frame = acc->GetFrame();
642 if (!frame) {
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.
665 // Go back one more.
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
674 // start.
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();
684 MOZ_ASSERT(acc);
685 if (aIncludeOrigin && mOffset == 0 && IsLocalAccAtLineStart(acc)) {
686 return *this;
688 nsIFrame* frame = acc->GetFrame();
689 if (!frame) {
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);
707 if (
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
712 // node.
713 !(origOffset == 0 && !IsLocalAccAtLineStart(acc))) {
714 return *this;
716 continuation = continuation->GetNextContinuation();
717 if (!continuation) {
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();
728 MOZ_ASSERT(acc);
729 auto lines = acc->GetCachedTextLines();
730 if (!lines) {
731 return TextLeafPoint();
733 size_t index;
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) {
739 return *this;
741 if (aDirection == eDirNext) {
742 // We don't want to include the origin. Get the next line start.
743 ++index;
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) {
753 --index;
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()) {
764 // If:
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.
769 return *this;
772 if (mAcc->IsLocal()) {
773 boundary = aDirection == eDirNext
774 ? FindNextLineStartSameLocalAcc(aIncludeOrigin)
775 : FindPrevLineStartSameLocalAcc(aIncludeOrigin);
776 } else {
777 boundary = FindLineStartSameRemoteAcc(aDirection, aIncludeOrigin);
780 if (aIgnoreListItemMarker && aDirection == eDirPrevious && !boundary &&
781 mOffset != 0 && IsLeafAfterListItemMarker()) {
782 // If:
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);
791 return boundary;
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();
801 nsAutoString text;
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
808 // node.
809 --lineStart.mOffset;
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();
821 } else {
822 lineStart =
823 lineStart.FindLineStartSameAcc(eDirPrevious, /* aIncludeOrigin */ true);
825 // Keep walking backward until we find an acceptable word start.
826 intl::WordRange word;
827 if (mOffset == 0) {
828 word.mBegin = 0;
829 } else if (mOffset == static_cast<int32_t>(text.Length())) {
830 word = WordBreaker::FindWord(
831 text, mOffset - 1,
832 StaticPrefs::layout_word_select_stop_at_punctuation()
833 ? FindWordOptions::StopAtPunctuation
834 : FindWordOptions::None);
835 } else {
836 word = WordBreaker::FindWord(
837 text, mOffset,
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
849 // included.
850 MOZ_ASSERT(word.mBegin != 0);
851 continue;
853 if (lineStart && static_cast<int32_t>(word.mBegin) < lineStart.mOffset) {
854 // A line start always starts a new word.
855 return lineStart;
857 if (IsAcceptableWordStart(mAcc, text, static_cast<int32_t>(word.mBegin))) {
858 break;
860 if (word.mBegin == 0) {
861 // We can't go back any further.
862 if (lineStart) {
863 // A line start always starts a new word.
864 return lineStart;
866 return TextLeafPoint();
869 return TextLeafPoint(mAcc, static_cast<int32_t>(word.mBegin));
872 TextLeafPoint TextLeafPoint::FindNextWordStartSameAcc(
873 bool aIncludeOrigin) const {
874 nsAutoString text;
875 mAcc->AppendTextTo(text);
876 int32_t wordStart = mOffset;
877 if (aIncludeOrigin) {
878 if (wordStart == 0) {
879 if (IsAcceptableWordStart(mAcc, text, 0)) {
880 return *this;
882 } else {
883 // The origin might start a word, so search from just before it.
884 --wordStart;
887 TextLeafPoint lineStart = FindLineStartSameAcc(eDirNext, aIncludeOrigin);
888 if (lineStart) {
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);
905 for (;;) {
906 if (!nextBreak || *nextBreak == text.Length()) {
907 if (lineStart) {
908 // A line start always starts a new word.
909 return lineStart;
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.
926 return lineStart;
928 if (IsAcceptableWordStart(mAcc, text, wordStart)) {
929 break;
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);
951 if (!ht) {
952 return false;
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;
965 int32_t htOffset;
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);
971 if (!ht) {
972 return TextLeafPoint();
974 htOffset = ht->CaretOffset();
975 if (htOffset == -1) {
976 return TextLeafPoint();
978 } else {
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();
982 if (!ht) {
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.
992 --htOffset;
994 return ht->ToTextLeafPoint(htOffset);
997 TextLeafPoint TextLeafPoint::FindBoundary(AccessibleTextBoundary aBoundaryType,
998 nsDirection aDirection,
999 BoundaryFlags aFlags) const {
1000 if (IsCaret()) {
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) &&
1012 mAcc->Parent() &&
1013 (mAcc->Parent()->State() & states::EDITABLE);
1014 if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) {
1015 return FindLineEnd(aDirection,
1016 inEditableAndStopInIt
1017 ? aFlags
1018 : (aFlags & ~BoundaryFlags::eStopInEditable));
1020 if (aBoundaryType == nsIAccessibleText::BOUNDARY_WORD_END) {
1021 return FindWordEnd(aDirection,
1022 inEditableAndStopInIt
1023 ? aFlags
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.
1033 return *this;
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) {
1044 continue;
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;
1062 break;
1063 case nsIAccessibleText::BOUNDARY_WORD_START:
1064 if (aDirection == eDirPrevious) {
1065 boundary = searchFrom.FindPrevWordStartSameAcc(includeOrigin);
1066 } else {
1067 boundary = searchFrom.FindNextWordStartSameAcc(includeOrigin);
1069 break;
1070 case nsIAccessibleText::BOUNDARY_LINE_START:
1071 boundary = searchFrom.FindLineStartSameAcc(aDirection, includeOrigin,
1072 ignoreListItemMarker);
1073 break;
1074 case nsIAccessibleText::BOUNDARY_PARAGRAPH:
1075 boundary = searchFrom.FindParagraphSameAcc(aDirection, includeOrigin,
1076 ignoreListItemMarker);
1077 break;
1078 default:
1079 MOZ_ASSERT_UNREACHABLE();
1080 break;
1082 if (boundary) {
1083 return boundary;
1086 // The start/end of the Accessible might be a boundary. If so, we must stop
1087 // on it.
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
1106 // character.
1107 return FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
1108 aFlags & ~BoundaryFlags::eIncludeOrigin);
1110 if ((aFlags & BoundaryFlags::eIncludeOrigin) && IsLineFeedChar()) {
1111 return *this;
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()) {
1119 return prevChar;
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.
1135 return lineStart;
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()) {
1143 return prevChar;
1145 return lineStart;
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.
1170 return *this;
1173 TextLeafPoint boundary = *this;
1174 if (aDirection == eDirPrevious && !prevIsSpace) {
1175 // If there isn't space immediately before us, first find the start of the
1176 // previous word.
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.
1188 return boundary;
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
1196 // word-end for.
1197 flags &= ~BoundaryFlags::eIncludeOrigin;
1199 boundary = boundary.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START,
1200 eDirNext, flags);
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;
1206 for (;;) {
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()) {
1213 break;
1215 boundary = prev;
1217 return boundary;
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.
1225 return *this;
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.
1232 return *this;
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 ||
1239 mOffset >= 2)) {
1240 // If there is a line feed, a new paragraph begins after it.
1241 nsAutoString text;
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
1252 // mOffset - 2.
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
1272 // paragraph.
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.
1280 if (prevBlock) {
1281 if (
1282 // If there's no previous leaf, we must be the first leaf after the
1283 // block.
1284 !prevLeaf ||
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);
1296 } else {
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.
1312 nsAutoString text;
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
1325 // spelling error.
1326 return !domRanges.IsEmpty();
1329 RemoteAccessible* acc = mAcc->AsRemote();
1330 MOZ_ASSERT(acc);
1331 if (!acc->mCachedFields) {
1332 return false;
1334 auto spellingErrors = acc->mCachedFields->GetAttribute<nsTArray<int32_t>>(
1335 CacheKey::SpellingErrors);
1336 if (!spellingErrors) {
1337 return false;
1339 size_t index;
1340 const bool foundOrigin = BinarySearch(
1341 *spellingErrors, 0, spellingErrors->Length(), mOffset, &index);
1342 // In spellingErrors, even indices are start offsets, odd indices are end
1343 // offsets.
1344 const bool foundStart = index % 2 == 0;
1345 if (foundOrigin) {
1346 // mOffset is a spelling error boundary. If it's a start offset, we're in a
1347 // spelling error.
1348 return foundStart;
1350 // index points at the next spelling error boundary after mOffset.
1351 if (index == 0) {
1352 return false; // No spelling errors before mOffset.
1354 if (foundStart) {
1355 // We're not in a spelling error because it starts after mOffset.
1356 return false;
1358 // A spelling error ends after mOffset.
1359 return true;
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
1369 // aAllowAdjacent.
1370 auto domRanges =
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);
1397 } else {
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();
1421 MOZ_ASSERT(acc);
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();
1430 size_t index;
1431 if (BinarySearch(*spellingErrors, 0, spellingErrors->Length(), mOffset,
1432 &index)) {
1433 // mOffset is in spellingErrors.
1434 if (aIncludeOrigin) {
1435 return *this;
1437 if (aDirection == eDirNext) {
1438 // We don't want the origin, so move to the next spelling error boundary
1439 // after mOffset.
1440 ++index;
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]);
1450 if (index == 0) {
1451 return TextLeafPoint(); // No spelling error boundary before us.
1453 // Decrement index so it points at a spelling error boundary before mOffset.
1454 --index;
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);
1469 if (!acc) {
1470 return TextLeafPoint();
1473 return TextLeafPoint(
1474 acc, aDirection == eDirPrevious
1475 ? static_cast<int32_t>(nsAccUtils::TextLength(acc)) - 1
1476 : 0);
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());
1528 /* static */
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()))));
1542 } else {
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()))));
1553 } else {
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
1558 // a binary search.
1561 return offsets;
1564 /* static */
1565 void TextLeafPoint::UpdateCachedSpellingError(dom::Document* aDocument,
1566 const nsRange& aRange) {
1567 DocAccessible* docAcc = GetExistingDocAccessible(aDocument);
1568 if (!docAcc) {
1569 return;
1571 LocalAccessible* startAcc = docAcc->GetAccessible(aRange.GetStartContainer());
1572 LocalAccessible* endAcc = docAcc->GetAccessible(aRange.GetEndContainer());
1573 if (!startAcc || !endAcc) {
1574 return;
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.
1585 break;
1590 already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributesLocalAcc(
1591 bool aIncludeDefaults) const {
1592 LocalAccessible* acc = mAcc->AsLocal();
1593 MOZ_ASSERT(acc);
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();
1600 if (hyperAcc) {
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()) {
1611 return nullptr;
1613 RefPtr<AccAttributes> attrs;
1614 if (mAcc->IsLocal()) {
1615 attrs = GetTextAttributesLocalAcc(aIncludeDefaults);
1616 } else {
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 {
1638 if (IsCaret()) {
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()) {
1652 return *this;
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)) {
1660 return *this;
1663 TextLeafPoint lastPoint = *this;
1664 for (;;) {
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.
1669 return spelling;
1671 TextLeafPoint point;
1672 point.mAcc = aDirection == eDirNext ? lastPoint.mAcc->NextSibling()
1673 : lastPoint.mAcc->PrevSibling();
1674 if (!point.mAcc || !point.mAcc->IsText()) {
1675 break;
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) {
1686 point = lastPoint;
1687 point.mOffset = 0;
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.
1693 continue;
1695 return point;
1697 lastPoint = point;
1698 if (aDirection == eDirPrevious) {
1699 // On the next iteration, we want to search for spelling errors from the
1700 // end of this Accessible.
1701 lastPoint.mOffset =
1702 static_cast<int32_t>(nsAccUtils::TextLength(point.mAcc));
1704 lastAttrs = attrs;
1706 // We couldn't move any further. Use the start/end.
1707 return TextLeafPoint(
1708 lastPoint.mAcc,
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();
1731 if (mOffset >= 0 &&
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
1736 // length.
1737 NS_ASSERTION(
1738 static_cast<uint32_t>(mOffset) <= nsAccUtils::TextLength(local),
1739 "Wrong in offset");
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());
1757 return bounds;
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.
1786 return false;
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
1797 // container.
1798 mEnd = containerEnd;
1801 return true;
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;
1838 return result;
1841 bool TextLeafRange::SetSelection(int32_t aSelectionNum) const {
1842 if (!mStart || !mEnd || mStart.mAcc->IsLocal() != mEnd.mAcc->IsLocal()) {
1843 return false;
1846 if (mStart.mAcc->IsRemote()) {
1847 DocAccessibleParent* doc = mStart.mAcc->AsRemote()->Document();
1848 if (doc != mEnd.mAcc->AsRemote()->Document()) {
1849 return false;
1852 Unused << doc->SendSetTextSelection(mStart.mAcc->ID(), mStart.mOffset,
1853 mEnd.mAcc->ID(), mEnd.mOffset,
1854 aSelectionNum);
1855 return true;
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) {
1865 return false;
1868 RefPtr<dom::Selection> domSel = GetDOMSelection(startContent, endContent);
1869 if (!domSel) {
1870 return false;
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);
1877 } else {
1878 domRange = domSel->GetRangeAt(AssertedCast<uint32_t>(aSelectionNum));
1880 if (!domRange) {
1881 return false;
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,
1891 IgnoreErrors());
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);
1900 return true;
1903 return false;
1906 void TextLeafRange::ScrollIntoView(uint32_t aScrollType) const {
1907 if (!mStart || !mEnd || mStart.mAcc->IsLocal() != mEnd.mAcc->IsLocal()) {
1908 return;
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.
1915 return;
1918 Unused << doc->SendScrollTextLeafRangeIntoView(
1919 mStart.mAcc->ID(), mStart.mOffset, mEnd.mAcc->ID(), mEnd.mOffset,
1920 aScrollType);
1921 return;
1924 auto [startContent, startContentOffset] = mStart.ToDOMPoint();
1925 auto [endContent, endContentOffset] = mEnd.ToDOMPoint();
1927 if (!startContent || !endContent) {
1928 return;
1931 ErrorResult er;
1932 RefPtr<nsRange> domRange = nsRange::Create(startContent, startContentOffset,
1933 endContent, endContentOffset, er);
1934 if (er.Failed()) {
1935 return;
1938 nsCoreUtils::ScrollSubstringTo(mStart.mAcc->AsLocal()->GetFrame(), domRange,
1939 aScrollType);
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;
1949 } else {
1950 result.mSegmentEnd = TextLeafPoint(
1951 aRange.mStart.mAcc, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
1954 return result;
1957 TextLeafRange::Iterator TextLeafRange::Iterator::EndIterator(
1958 const TextLeafRange& aRange) {
1959 Iterator result(aRange);
1961 result.mSegmentEnd = TextLeafPoint();
1962 result.mSegmentStart = TextLeafPoint();
1964 return result;
1967 TextLeafRange::Iterator& TextLeafRange::Iterator::operator++() {
1968 if (mSegmentEnd.mAcc == mRange.mEnd.mAcc) {
1969 mSegmentEnd = TextLeafPoint();
1970 mSegmentStart = TextLeafPoint();
1971 return *this;
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);
1978 } else {
1979 mSegmentEnd =
1980 TextLeafPoint(nextLeaf, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
1982 } else {
1983 mSegmentEnd = TextLeafPoint();
1984 mSegmentStart = TextLeafPoint();
1987 return *this;
1990 } // namespace mozilla::a11y