TextLeafRange.cpp (99446B)
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/. */ 6 7 #include "TextLeafRange.h" 8 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/AbstractRange.h" 18 #include "mozilla/dom/CharacterData.h" 19 #include "mozilla/dom/HTMLInputElement.h" 20 #include "mozilla/PresShell.h" 21 #include "mozilla/intl/Segmenter.h" 22 #include "mozilla/intl/WordBreaker.h" 23 #include "mozilla/StaticPrefs_layout.h" 24 #include "mozilla/TextEditor.h" 25 #include "nsAccUtils.h" 26 #include "nsBlockFrame.h" 27 #include "nsFocusManager.h" 28 #include "nsFrameSelection.h" 29 #include "nsIAccessiblePivot.h" 30 #include "nsILineIterator.h" 31 #include "nsINode.h" 32 #include "nsStyleStructInlines.h" 33 #include "nsTArray.h" 34 #include "nsTextFrame.h" 35 #include "nsUnicharUtils.h" 36 #include "Pivot.h" 37 #include "TextAttrs.h" 38 #include "TextRange.h" 39 40 using mozilla::intl::WordBreaker; 41 using FindWordOptions = mozilla::intl::WordBreaker::FindWordOptions; 42 43 namespace mozilla::a11y { 44 45 /*** Helpers ***/ 46 47 /** 48 * These two functions convert between rendered and content text offsets. 49 * When text DOM nodes are rendered, the rendered text often does not contain 50 * all the whitespace from the source. For example, by default, the text 51 * "a b" will be rendered as "a b"; i.e. multiple spaces are compressed to 52 * one. TextLeafAccessibles contain rendered text, but when we query layout, we 53 * need to provide offsets into the original content text. Similarly, layout 54 * returns content offsets, but we need to convert them to rendered offsets to 55 * map them to TextLeafAccessibles. 56 */ 57 58 static int32_t RenderedToContentOffset(LocalAccessible* aAcc, 59 uint32_t aRenderedOffset) { 60 nsTextFrame* frame = do_QueryFrame(aAcc->GetFrame()); 61 if (!frame) { 62 MOZ_ASSERT(!aAcc->HasOwnContent() || aAcc->IsHTMLBr(), 63 "No text frame because this is a XUL label[value] text leaf or " 64 "a BR element."); 65 return static_cast<int32_t>(aRenderedOffset); 66 } 67 68 if (frame->StyleText()->WhiteSpaceIsSignificant() && 69 frame->StyleText()->NewlineIsSignificant(frame)) { 70 // Spaces and new lines aren't altered, so the content and rendered offsets 71 // are the same. This happens in pre-formatted text and text fields. 72 return static_cast<int32_t>(aRenderedOffset); 73 } 74 75 nsIFrame::RenderedText text = 76 frame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1, 77 nsIFrame::TextOffsetType::OffsetsInRenderedText, 78 nsIFrame::TrailingWhitespace::DontTrim); 79 return text.mOffsetWithinNodeText; 80 } 81 82 static uint32_t ContentToRenderedOffset(LocalAccessible* aAcc, 83 int32_t aContentOffset) { 84 nsTextFrame* frame = do_QueryFrame(aAcc->GetFrame()); 85 if (!frame) { 86 MOZ_ASSERT(!aAcc->HasOwnContent(), 87 "No text frame because this is a XUL label[value] text leaf."); 88 return aContentOffset; 89 } 90 91 if (frame->StyleText()->WhiteSpaceIsSignificant() && 92 frame->StyleText()->NewlineIsSignificant(frame)) { 93 // Spaces and new lines aren't altered, so the content and rendered offsets 94 // are the same. This happens in pre-formatted text and text fields. 95 return aContentOffset; 96 } 97 98 nsIFrame::RenderedText text = 99 frame->GetRenderedText(aContentOffset, aContentOffset + 1, 100 nsIFrame::TextOffsetType::OffsetsInContentText, 101 nsIFrame::TrailingWhitespace::DontTrim); 102 return text.mOffsetWithinNodeRenderedText; 103 } 104 105 class LeafRule : public PivotRule { 106 public: 107 explicit LeafRule(bool aIgnoreListItemMarker) 108 : mIgnoreListItemMarker(aIgnoreListItemMarker) {} 109 110 virtual uint16_t Match(Accessible* aAcc) override { 111 if (aAcc->IsOuterDoc()) { 112 // Treat an embedded doc as a single character in this document, but do 113 // not descend inside it. 114 return nsIAccessibleTraversalRule::FILTER_MATCH | 115 nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; 116 } 117 118 if (mIgnoreListItemMarker && aAcc->Role() == roles::LISTITEM_MARKER) { 119 // Ignore list item markers if configured to do so. 120 return nsIAccessibleTraversalRule::FILTER_IGNORE; 121 } 122 123 // We deliberately include Accessibles such as empty input elements and 124 // empty containers, as these can be at the start of a line. 125 if (!aAcc->HasChildren()) { 126 return nsIAccessibleTraversalRule::FILTER_MATCH; 127 } 128 return nsIAccessibleTraversalRule::FILTER_IGNORE; 129 } 130 131 private: 132 bool mIgnoreListItemMarker; 133 }; 134 135 static HyperTextAccessible* HyperTextFor(LocalAccessible* aAcc) { 136 for (LocalAccessible* acc = aAcc; acc; acc = acc->LocalParent()) { 137 if (HyperTextAccessible* ht = acc->AsHyperText()) { 138 return ht; 139 } 140 } 141 return nullptr; 142 } 143 144 static Accessible* NextLeaf(Accessible* aOrigin, bool aIsEditable = false, 145 bool aIgnoreListItemMarker = false) { 146 MOZ_ASSERT(aOrigin); 147 Accessible* doc = nsAccUtils::DocumentFor(aOrigin); 148 Pivot pivot(doc); 149 auto rule = LeafRule(aIgnoreListItemMarker); 150 Accessible* leaf = pivot.Next(aOrigin, rule); 151 if (aIsEditable && leaf) { 152 return leaf->Parent() && (leaf->Parent()->State() & states::EDITABLE) 153 ? leaf 154 : nullptr; 155 } 156 return leaf; 157 } 158 159 static Accessible* PrevLeaf(Accessible* aOrigin, bool aIsEditable = false, 160 bool aIgnoreListItemMarker = false) { 161 MOZ_ASSERT(aOrigin); 162 Accessible* doc = nsAccUtils::DocumentFor(aOrigin); 163 Pivot pivot(doc); 164 auto rule = LeafRule(aIgnoreListItemMarker); 165 Accessible* leaf = pivot.Prev(aOrigin, rule); 166 if (aIsEditable && leaf) { 167 return leaf->Parent() && (leaf->Parent()->State() & states::EDITABLE) 168 ? leaf 169 : nullptr; 170 } 171 return leaf; 172 } 173 174 static nsIFrame* GetFrameInBlock(const LocalAccessible* aAcc) { 175 dom::HTMLInputElement* input = 176 dom::HTMLInputElement::FromNodeOrNull(aAcc->GetContent()); 177 if (!input) { 178 if (LocalAccessible* parent = aAcc->LocalParent()) { 179 input = dom::HTMLInputElement::FromNodeOrNull(parent->GetContent()); 180 } 181 } 182 183 if (input) { 184 // If this is a single line input (or a leaf of an input) we want to return 185 // the top frame of the input element and not the text leaf's frame because 186 // the leaf may be inside of an embedded block frame in the input's shadow 187 // DOM that we aren't interested in. 188 return input->GetPrimaryFrame(); 189 } 190 191 return aAcc->GetFrame(); 192 } 193 194 /** 195 * Returns true if the given frames are on different lines. 196 */ 197 static bool AreFramesOnDifferentLines(nsIFrame* aFrame1, nsIFrame* aFrame2) { 198 MOZ_ASSERT(aFrame1 && aFrame2); 199 if (aFrame1 == aFrame2) { 200 // This can happen if two Accessibles share the same frame; e.g. image maps. 201 return false; 202 } 203 auto [block1, lineFrame1] = aFrame1->GetContainingBlockForLine( 204 /* aLockScroll */ false); 205 if (!block1) { 206 // Error; play it safe. 207 return true; 208 } 209 auto [block2, lineFrame2] = aFrame2->GetContainingBlockForLine( 210 /* aLockScroll */ false); 211 if (lineFrame1 == lineFrame2) { 212 return false; 213 } 214 if (block1 != block2) { 215 // These frames are in different blocks, so they're on different lines. 216 return true; 217 } 218 if (nsBlockFrame* block = do_QueryFrame(block1)) { 219 // If we have a block frame, it's faster for us to use 220 // BlockInFlowLineIterator because it uses the line cursor. 221 bool found = false; 222 block->SetupLineCursorForQuery(); 223 nsBlockInFlowLineIterator it1(block, lineFrame1, &found); 224 if (!found) { 225 // Error; play it safe. 226 return true; 227 } 228 found = false; 229 nsBlockInFlowLineIterator it2(block, lineFrame2, &found); 230 return !found || it1.GetLineList() != it2.GetLineList() || 231 it1.GetLine() != it2.GetLine(); 232 } 233 AutoAssertNoDomMutations guard; 234 nsILineIterator* it = block1->GetLineIterator(); 235 MOZ_ASSERT(it, "GetLineIterator impl in line-container blocks is infallible"); 236 int32_t line1 = it->FindLineContaining(lineFrame1); 237 if (line1 < 0) { 238 // Error; play it safe. 239 return true; 240 } 241 int32_t line2 = it->FindLineContaining(lineFrame2, line1); 242 return line1 != line2; 243 } 244 245 static bool IsLocalAccAtLineStart(LocalAccessible* aAcc) { 246 if (aAcc->NativeRole() == roles::LISTITEM_MARKER) { 247 // A bullet always starts a line. 248 return true; 249 } 250 // Splitting of content across lines is handled by layout. 251 // nsIFrame::IsLogicallyAtLineEdge queries whether a frame is the first frame 252 // on its line. However, we can't use that because the first frame on a line 253 // might not be included in the a11y tree; e.g. an empty span, or space 254 // in the DOM after a line break which is stripped when rendered. Instead, we 255 // get the line number for this Accessible's frame and the line number for the 256 // previous leaf Accessible's frame and compare them. 257 Accessible* prev = PrevLeaf(aAcc); 258 LocalAccessible* prevLocal = prev ? prev->AsLocal() : nullptr; 259 if (!prevLocal) { 260 // There's nothing before us, so this is the start of the first line. 261 return true; 262 } 263 if (prevLocal->NativeRole() == roles::LISTITEM_MARKER) { 264 // If there is a bullet immediately before us and we're inside the same 265 // list item, this is not the start of a line. 266 LocalAccessible* listItem = prevLocal->LocalParent(); 267 MOZ_ASSERT(listItem); 268 LocalAccessible* doc = listItem->Document(); 269 MOZ_ASSERT(doc); 270 for (LocalAccessible* parent = aAcc->LocalParent(); parent && parent != doc; 271 parent = parent->LocalParent()) { 272 if (parent == listItem) { 273 return false; 274 } 275 } 276 } 277 278 nsIFrame* thisFrame = GetFrameInBlock(aAcc); 279 if (!thisFrame) { 280 return false; 281 } 282 283 nsIFrame* prevFrame = GetFrameInBlock(prevLocal); 284 if (!prevFrame) { 285 return false; 286 } 287 288 // The previous leaf might cross lines. We want to compare against the last 289 // line. 290 prevFrame = prevFrame->LastContinuation(); 291 // if the lines are different, that means there's nothing before us on the 292 // same line, so we're at the start. 293 return AreFramesOnDifferentLines(thisFrame, prevFrame); 294 } 295 296 /** 297 * There are many kinds of word break, but we only need to treat punctuation and 298 * space specially. 299 */ 300 enum WordBreakClass { eWbcSpace = 0, eWbcPunct, eWbcOther }; 301 302 static WordBreakClass GetWordBreakClass(char16_t aChar) { 303 // Based on IsSelectionInlineWhitespace and IsSelectionNewline in 304 // layout/generic/nsTextFrame.cpp. 305 const char16_t kCharNbsp = 0xA0; 306 switch (aChar) { 307 case ' ': 308 case kCharNbsp: 309 case '\t': 310 case '\f': 311 case '\n': 312 case '\r': 313 return eWbcSpace; 314 default: 315 break; 316 } 317 return mozilla::IsPunctuationForWordSelect(aChar) ? eWbcPunct : eWbcOther; 318 } 319 320 /** 321 * Words can cross Accessibles. To work out whether we're at the start of a 322 * word, we might have to check the previous leaf. This class handles querying 323 * the previous WordBreakClass, crossing Accessibles if necessary. 324 */ 325 class PrevWordBreakClassWalker { 326 public: 327 PrevWordBreakClassWalker(Accessible* aAcc, const nsAString& aText, 328 int32_t aOffset) 329 : mAcc(aAcc), mText(aText), mOffset(aOffset) { 330 mClass = GetWordBreakClass(mText.CharAt(mOffset)); 331 } 332 333 WordBreakClass CurClass() { return mClass; } 334 335 Maybe<WordBreakClass> PrevClass() { 336 for (;;) { 337 if (!PrevChar()) { 338 return Nothing(); 339 } 340 WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset)); 341 if (curClass != mClass) { 342 mClass = curClass; 343 return Some(curClass); 344 } 345 } 346 MOZ_ASSERT_UNREACHABLE(); 347 return Nothing(); 348 } 349 350 bool IsStartOfGroup() { 351 if (!PrevChar()) { 352 // There are no characters before us. 353 return true; 354 } 355 WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset)); 356 // We wanted to peek at the previous character, not really move to it. 357 ++mOffset; 358 return curClass != mClass; 359 } 360 361 private: 362 bool PrevChar() { 363 if (mOffset > 0) { 364 --mOffset; 365 return true; 366 } 367 if (!mAcc) { 368 // PrevChar was called already and failed. 369 return false; 370 } 371 mAcc = PrevLeaf(mAcc); 372 if (!mAcc) { 373 return false; 374 } 375 mText.Truncate(); 376 mAcc->AppendTextTo(mText); 377 mOffset = static_cast<int32_t>(mText.Length()) - 1; 378 return true; 379 } 380 381 Accessible* mAcc; 382 nsAutoString mText; 383 int32_t mOffset; 384 WordBreakClass mClass; 385 }; 386 387 /** 388 * WordBreaker breaks at all space, punctuation, etc. We want to emulate 389 * layout, so that's not what we want. This function determines whether this 390 * is acceptable as the start of a word for our purposes. 391 */ 392 static bool IsAcceptableWordStart(Accessible* aAcc, const nsAutoString& aText, 393 int32_t aOffset) { 394 PrevWordBreakClassWalker walker(aAcc, aText, aOffset); 395 if (!walker.IsStartOfGroup()) { 396 // If we're not at the start of a WordBreaker group, this can't be the 397 // start of a word. 398 return false; 399 } 400 WordBreakClass curClass = walker.CurClass(); 401 if (curClass == eWbcSpace) { 402 // Space isn't the start of a word. 403 return false; 404 } 405 Maybe<WordBreakClass> prevClass = walker.PrevClass(); 406 if (curClass == eWbcPunct && (!prevClass || prevClass.value() != eWbcSpace)) { 407 // Punctuation isn't the start of a word (unless it is after space). 408 return false; 409 } 410 if (!prevClass || prevClass.value() != eWbcPunct) { 411 // If there's nothing before this or the group before this isn't 412 // punctuation, this is the start of a word. 413 return true; 414 } 415 // At this point, we know the group before this is punctuation. 416 if (!StaticPrefs::layout_word_select_stop_at_punctuation()) { 417 // When layout.word_select.stop_at_punctuation is false (defaults to true), 418 // if there is punctuation before this, this is not the start of a word. 419 return false; 420 } 421 Maybe<WordBreakClass> prevPrevClass = walker.PrevClass(); 422 if (!prevPrevClass || prevPrevClass.value() == eWbcSpace) { 423 // If there is punctuation before this and space (or nothing) before the 424 // punctuation, this is not the start of a word. 425 return false; 426 } 427 return true; 428 } 429 430 class BlockRule : public PivotRule { 431 public: 432 virtual uint16_t Match(Accessible* aAcc) override { 433 if (RefPtr<nsAtom>(aAcc->DisplayStyle()) == nsGkAtoms::block || 434 aAcc->IsHTMLListItem() || aAcc->IsTableRow() || aAcc->IsTableCell()) { 435 return nsIAccessibleTraversalRule::FILTER_MATCH; 436 } 437 return nsIAccessibleTraversalRule::FILTER_IGNORE; 438 } 439 }; 440 441 /** 442 * Find DOM ranges which map to text attributes overlapping the requested 443 * LocalAccessible and offsets. This includes ranges that begin or end outside 444 * of the given LocalAccessible. Note that the offset arguments are rendered 445 * offsets, but because the returned ranges are DOM ranges, those offsets are 446 * content offsets. See the documentation for 447 * dom::Selection::GetRangesForIntervalArray for information about the 448 * aAllowAdjacent argument. 449 */ 450 static nsTArray<std::pair<nsTArray<dom::AbstractRange*>, nsStaticAtom*>> 451 FindDOMTextOffsetAttributes(LocalAccessible* aAcc, int32_t aRenderedStart, 452 int32_t aRenderedEnd, bool aAllowAdjacent = false) { 453 nsTArray<std::pair<nsTArray<dom::AbstractRange*>, nsStaticAtom*>> result; 454 if (!aAcc->IsTextLeaf() || !aAcc->HasOwnContent() || 455 !aAcc->GetContent()->IsText()) { 456 return result; 457 } 458 nsIFrame* frame = aAcc->GetFrame(); 459 RefPtr<nsFrameSelection> frameSel = 460 frame ? frame->GetFrameSelection() : nullptr; 461 if (!frameSel) { 462 return result; 463 } 464 nsINode* node = aAcc->GetNode(); 465 uint32_t contentStart = RenderedToContentOffset(aAcc, aRenderedStart); 466 uint32_t contentEnd = 467 aRenderedEnd == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT 468 ? dom::CharacterData::FromNode(node)->TextLength() 469 : RenderedToContentOffset(aAcc, aRenderedEnd); 470 const std::pair<mozilla::SelectionType, nsStaticAtom*> 471 kSelectionTypesToAttributes[] = { 472 {SelectionType::eSpellCheck, nsGkAtoms::spelling}, 473 {SelectionType::eTargetText, nsGkAtoms::mark}, 474 }; 475 size_t highlightCount = frameSel->HighlightSelectionCount(); 476 result.SetCapacity(std::size(kSelectionTypesToAttributes) + highlightCount); 477 478 auto appendRanges = [&](dom::Selection* aDomSel, nsStaticAtom* aAttr) { 479 nsTArray<dom::AbstractRange*> domRanges; 480 aDomSel->GetAbstractRangesForIntervalArray( 481 node, contentStart, node, contentEnd, aAllowAdjacent, &domRanges); 482 if (!domRanges.IsEmpty()) { 483 result.AppendElement(std::make_pair(std::move(domRanges), aAttr)); 484 } 485 }; 486 487 for (auto [selType, attr] : kSelectionTypesToAttributes) { 488 dom::Selection* domSel = frameSel->GetSelection(selType); 489 if (!domSel) { 490 continue; 491 } 492 appendRanges(domSel, attr); 493 } 494 495 for (size_t h = 0; h < highlightCount; ++h) { 496 RefPtr<dom::Selection> domSel = frameSel->HighlightSelection(h); 497 MOZ_ASSERT(domSel); 498 nsStaticAtom* attr = nullptr; 499 MOZ_ASSERT(domSel->HighlightSelectionData().mHighlight); 500 switch (domSel->HighlightSelectionData().mHighlight->Type()) { 501 case dom::HighlightType::Highlight: 502 attr = nsGkAtoms::mark; 503 break; 504 case dom::HighlightType::Spelling_error: 505 attr = nsGkAtoms::spelling; 506 break; 507 case dom::HighlightType::Grammar_error: 508 attr = nsGkAtoms::grammar; 509 break; 510 } 511 MOZ_ASSERT(attr); 512 appendRanges(domSel, attr); 513 } 514 515 return result; 516 } 517 518 /** 519 * Given two DOM nodes get DOM Selection object that is common 520 * to both of them. 521 */ 522 static dom::Selection* GetDOMSelection(const nsIContent* aStartContent, 523 const nsIContent* aEndContent) { 524 nsIFrame* startFrame = aStartContent->GetPrimaryFrame(); 525 const nsFrameSelection* startFrameSel = 526 startFrame ? startFrame->GetConstFrameSelection() : nullptr; 527 nsIFrame* endFrame = aEndContent->GetPrimaryFrame(); 528 const nsFrameSelection* endFrameSel = 529 endFrame ? endFrame->GetConstFrameSelection() : nullptr; 530 531 if (startFrameSel != endFrameSel) { 532 // Start and end point don't share the same selection state. 533 // This could happen when both points aren't in the same editable. 534 return nullptr; 535 } 536 537 return startFrameSel ? &startFrameSel->NormalSelection() : nullptr; 538 } 539 540 std::pair<nsIContent*, uint32_t> TextLeafPoint::ToDOMPoint( 541 bool aIncludeGenerated) const { 542 if (!(*this) || !mAcc->IsLocal()) { 543 MOZ_ASSERT_UNREACHABLE("Invalid point"); 544 return {nullptr, 0}; 545 } 546 547 nsIContent* content = mAcc->AsLocal()->GetContent(); 548 nsIFrame* frame = content ? content->GetPrimaryFrame() : nullptr; 549 MOZ_ASSERT(frame); 550 551 if (!aIncludeGenerated && frame && frame->IsGeneratedContentFrame()) { 552 // List markers accessibles represent the generated content element, 553 // before/after text accessibles represent the child text nodes. 554 auto generatedElement = content->IsGeneratedContentContainerForMarker() 555 ? content 556 : content->GetParentElement(); 557 auto parent = generatedElement ? generatedElement->GetParent() : nullptr; 558 MOZ_ASSERT(parent); 559 if (parent) { 560 if (generatedElement->IsGeneratedContentContainerForAfter()) { 561 // Use the end offset of the parent element for trailing generated 562 // content. 563 return {parent, parent->GetChildCount()}; 564 } 565 566 if (generatedElement->IsGeneratedContentContainerForBefore() || 567 generatedElement->IsGeneratedContentContainerForMarker()) { 568 // Use the start offset of the parent element for leading generated 569 // content. 570 return {parent, 0}; 571 } 572 573 MOZ_ASSERT_UNREACHABLE("Unknown generated content type!"); 574 } 575 } 576 577 if (mAcc->IsTextLeaf()) { 578 // For text nodes, DOM uses a character offset within the node. 579 return {content, RenderedToContentOffset(mAcc->AsLocal(), mOffset)}; 580 } 581 582 if (!mAcc->IsHyperText()) { 583 // For non-text nodes (e.g. images), DOM points use the child index within 584 // the parent. mOffset could be 0 (for the start of the node) or 1 (for the 585 // end of the node). mOffset could be 1 if this is the last Accessible in a 586 // container and the point is at the end of the container. 587 MOZ_ASSERT(mOffset == 0 || mOffset == 1); 588 nsIContent* parent = content->GetParent(); 589 MOZ_ASSERT(parent); 590 // ComputeIndexOf() could return Nothing if this is an anonymous child. 591 if (auto childIndex = parent->ComputeIndexOf(content)) { 592 return {parent, mOffset == 0 ? *childIndex : *childIndex + 1}; 593 } 594 } 595 596 // This could be an empty editable container, whitespace or an empty doc. In 597 // any case, the offset inside should be 0. 598 MOZ_ASSERT(mOffset == 0); 599 600 if (RefPtr<TextControlElement> textControlElement = 601 TextControlElement::FromNodeOrNull(content)) { 602 // This is an empty input, use the shadow root's element. 603 if (RefPtr<TextEditor> textEditor = textControlElement->GetTextEditor()) { 604 if (textEditor->IsEmpty()) { 605 MOZ_ASSERT(mOffset == 0); 606 return {textEditor->GetRoot(), 0}; 607 } 608 } 609 } 610 611 return {content, 0}; 612 } 613 614 static bool IsLineBreakContinuation(nsTextFrame* aContinuation) { 615 // A fluid continuation always means a new line. 616 if (aContinuation->HasAnyStateBits(NS_FRAME_IS_FLUID_CONTINUATION)) { 617 return true; 618 } 619 // If both this continuation and the previous continuation are bidi 620 // continuations, this continuation might be both a bidi split and on a new 621 // line. 622 if (!aContinuation->HasAnyStateBits(NS_FRAME_IS_BIDI)) { 623 return true; 624 } 625 nsTextFrame* prev = aContinuation->GetPrevContinuation(); 626 if (!prev) { 627 // aContinuation is the primary frame. We can't be sure if this starts a new 628 // line, as there might be other nodes before it. That is handled by 629 // IsLocalAccAtLineStart. 630 return false; 631 } 632 if (!prev->HasAnyStateBits(NS_FRAME_IS_BIDI)) { 633 return true; 634 } 635 return AreFramesOnDifferentLines(aContinuation, prev); 636 } 637 638 static bool IsCaretValid(TextLeafPoint aPoint) { 639 Accessible* acc = aPoint.mAcc; 640 if (!acc->IsHyperText()) { 641 acc = acc->Parent(); 642 } 643 if (!(acc->State() & states::EDITABLE)) { 644 return true; 645 } 646 // The caret is within editable content. 647 Accessible* focus = FocusMgr() ? FocusMgr()->FocusedAccessible() : nullptr; 648 if (!focus) { 649 return false; 650 } 651 // If the focus isn't an editor, the caret can't be inside an editor. This 652 // can happen, for example, when a text input is the last element in a 653 // container and a user clicks in the empty area at the end of the container. 654 // In this case, the caret is actually at the end of the container outside the 655 // input. This can also happen if there is an empty area in a container before 656 // an input and a user clicks there. TextLeafPoint can't represent either of 657 // these cases and it's generally not useful. We must not normalize this to 658 // the nearest leaf because this would put the caret inside an editor which 659 // isn't focused. Instead, we pretend there is no caret. See bug 1950748 for 660 // more details. 661 return focus->State() & states::EDITABLE; 662 } 663 664 /*** TextLeafPoint ***/ 665 666 TextLeafPoint::TextLeafPoint(Accessible* aAcc, int32_t aOffset) { 667 MOZ_ASSERT(aOffset >= 0 || 668 aOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); 669 if (!aAcc) { 670 // Construct an invalid point. 671 mAcc = nullptr; 672 mOffset = 0; 673 return; 674 } 675 676 // Even though an OuterDoc contains a document, we treat it as a leaf because 677 // we don't want to move into another document. 678 if (!aAcc->IsOuterDoc() && aAcc->HasChildren()) { 679 // Find a leaf. This might not necessarily be a TextLeafAccessible; it 680 // could be an empty container. 681 auto GetChild = [&aOffset](Accessible* acc) -> Accessible* { 682 if (acc->IsOuterDoc()) { 683 return nullptr; 684 } 685 return aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT 686 ? acc->FirstChild() 687 : acc->LastChild(); 688 }; 689 690 for (Accessible* acc = GetChild(aAcc); acc; acc = GetChild(acc)) { 691 mAcc = acc; 692 } 693 mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT 694 ? 0 695 : nsAccUtils::TextLength(mAcc); 696 return; 697 } 698 mAcc = aAcc; 699 mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT 700 ? aOffset 701 : nsAccUtils::TextLength(mAcc); 702 } 703 704 bool TextLeafPoint::operator<(const TextLeafPoint& aPoint) const { 705 if (mAcc == aPoint.mAcc) { 706 return mOffset < aPoint.mOffset; 707 } 708 return mAcc->IsBefore(aPoint.mAcc); 709 } 710 711 bool TextLeafPoint::operator<=(const TextLeafPoint& aPoint) const { 712 return *this == aPoint || *this < aPoint; 713 } 714 715 bool TextLeafPoint::IsDocEdge(nsDirection aDirection) const { 716 if (aDirection == eDirPrevious) { 717 return mOffset == 0 && !PrevLeaf(mAcc); 718 } 719 720 return mOffset == static_cast<int32_t>(nsAccUtils::TextLength(mAcc)) && 721 !NextLeaf(mAcc); 722 } 723 724 bool TextLeafPoint::IsLeafAfterListItemMarker() const { 725 Accessible* prev = PrevLeaf(mAcc); 726 return prev && prev->Role() == roles::LISTITEM_MARKER && 727 prev->Parent()->IsAncestorOf(mAcc); 728 } 729 730 bool TextLeafPoint::IsEmptyLastLine() const { 731 if (mAcc->IsHTMLBr() && mOffset == 1) { 732 return true; 733 } 734 if (!mAcc->IsTextLeaf()) { 735 return false; 736 } 737 if (mOffset < static_cast<int32_t>(nsAccUtils::TextLength(mAcc))) { 738 return false; 739 } 740 nsAutoString text; 741 mAcc->AppendTextTo(text, mOffset - 1, 1); 742 return text.CharAt(0) == '\n'; 743 } 744 745 char16_t TextLeafPoint::GetChar() const { 746 nsAutoString text; 747 mAcc->AppendTextTo(text, mOffset, 1); 748 return text.CharAt(0); 749 } 750 751 TextLeafPoint TextLeafPoint::FindPrevLineStartSameLocalAcc( 752 bool aIncludeOrigin) const { 753 LocalAccessible* acc = mAcc->AsLocal(); 754 MOZ_ASSERT(acc); 755 if (mOffset == 0) { 756 if (aIncludeOrigin && IsLocalAccAtLineStart(acc)) { 757 return *this; 758 } 759 return TextLeafPoint(); 760 } 761 nsIFrame* frame = acc->GetFrame(); 762 if (!frame) { 763 // This can happen if this is an empty element with display: contents. In 764 // that case, this Accessible contains no lines. 765 return TextLeafPoint(); 766 } 767 if (!frame->IsTextFrame()) { 768 if (IsLocalAccAtLineStart(acc)) { 769 return TextLeafPoint(acc, 0); 770 } 771 return TextLeafPoint(); 772 } 773 // Each line of a text node is rendered as a continuation frame. Get the 774 // continuation containing the origin. 775 int32_t origOffset = mOffset; 776 origOffset = RenderedToContentOffset(acc, origOffset); 777 nsTextFrame* continuation = nullptr; 778 int32_t unusedOffsetInContinuation = 0; 779 frame->GetChildFrameContainingOffset( 780 origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation); 781 MOZ_ASSERT(continuation); 782 int32_t lineStart = continuation->GetContentOffset(); 783 if (lineStart > 0 && ( 784 // A line starts at the origin, but the caller 785 // doesn't want this included. 786 (!aIncludeOrigin && lineStart == origOffset) || 787 !IsLineBreakContinuation(continuation))) { 788 // Go back one more, skipping continuations that aren't line breaks or the 789 // primary frame. 790 for (nsTextFrame* prev = continuation->GetPrevContinuation(); prev; 791 prev = prev->GetPrevContinuation()) { 792 continuation = prev; 793 if (IsLineBreakContinuation(continuation)) { 794 break; 795 } 796 } 797 MOZ_ASSERT(continuation); 798 lineStart = continuation->GetContentOffset(); 799 } 800 MOZ_ASSERT(lineStart >= 0); 801 MOZ_ASSERT(lineStart == 0 || IsLineBreakContinuation(continuation)); 802 if (lineStart == 0 && !IsLocalAccAtLineStart(acc)) { 803 // This is the first line of this text node, but there is something else 804 // on the same line before this text node, so don't return this as a line 805 // start. 806 return TextLeafPoint(); 807 } 808 lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart)); 809 return TextLeafPoint(acc, lineStart); 810 } 811 812 TextLeafPoint TextLeafPoint::FindNextLineStartSameLocalAcc( 813 bool aIncludeOrigin) const { 814 LocalAccessible* acc = mAcc->AsLocal(); 815 MOZ_ASSERT(acc); 816 if (aIncludeOrigin && mOffset == 0 && IsLocalAccAtLineStart(acc)) { 817 return *this; 818 } 819 nsIFrame* frame = acc->GetFrame(); 820 if (!frame) { 821 // This can happen if this is an empty element with display: contents. In 822 // that case, this Accessible contains no lines. 823 return TextLeafPoint(); 824 } 825 if (!frame->IsTextFrame()) { 826 // There can't be multiple lines in a non-text leaf. 827 return TextLeafPoint(); 828 } 829 // Each line of a text node is rendered as a continuation frame. Get the 830 // continuation containing the origin. 831 int32_t origOffset = mOffset; 832 origOffset = RenderedToContentOffset(acc, origOffset); 833 nsTextFrame* continuation = nullptr; 834 int32_t unusedOffsetInContinuation = 0; 835 frame->GetChildFrameContainingOffset( 836 origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation); 837 MOZ_ASSERT(continuation); 838 if ( 839 // A line starts at the origin and the caller wants this included. 840 aIncludeOrigin && continuation->GetContentOffset() == origOffset && 841 IsLineBreakContinuation(continuation) && 842 // If this is the first line of this text node (offset 0), don't treat it 843 // as a line start if there's something else on the line before this text 844 // node. 845 !(origOffset == 0 && !IsLocalAccAtLineStart(acc))) { 846 return *this; 847 } 848 // Get the next continuation, skipping continuations that aren't line breaks. 849 while ((continuation = continuation->GetNextContinuation())) { 850 if (IsLineBreakContinuation(continuation)) { 851 break; 852 } 853 } 854 if (!continuation) { 855 return TextLeafPoint(); 856 } 857 int32_t lineStart = continuation->GetContentOffset(); 858 lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart)); 859 return TextLeafPoint(acc, lineStart); 860 } 861 862 TextLeafPoint TextLeafPoint::FindLineStartSameRemoteAcc( 863 nsDirection aDirection, bool aIncludeOrigin) const { 864 RemoteAccessible* acc = mAcc->AsRemote(); 865 MOZ_ASSERT(acc); 866 auto lines = acc->GetCachedTextLines(); 867 if (!lines) { 868 return TextLeafPoint(); 869 } 870 size_t index; 871 // If BinarySearch returns true, mOffset is in the array and index points at 872 // it. If BinarySearch returns false, mOffset is not in the array and index 873 // points at the next line start after mOffset. 874 if (BinarySearch(*lines, 0, lines->Length(), mOffset, &index)) { 875 if (aIncludeOrigin) { 876 return *this; 877 } 878 if (aDirection == eDirNext) { 879 // We don't want to include the origin. Get the next line start. 880 ++index; 881 } 882 } 883 MOZ_ASSERT(index <= lines->Length()); 884 if ((aDirection == eDirNext && index == lines->Length()) || 885 (aDirection == eDirPrevious && index == 0)) { 886 return TextLeafPoint(); 887 } 888 // index points at the line start after mOffset. 889 if (aDirection == eDirPrevious) { 890 --index; 891 } 892 return TextLeafPoint(mAcc, lines->ElementAt(index)); 893 } 894 895 TextLeafPoint TextLeafPoint::FindLineStartSameAcc( 896 nsDirection aDirection, bool aIncludeOrigin, 897 bool aIgnoreListItemMarker) const { 898 TextLeafPoint boundary; 899 if (aIgnoreListItemMarker && aIncludeOrigin && mOffset == 0 && 900 IsLeafAfterListItemMarker()) { 901 // If: 902 // (1) we are ignoring list markers 903 // (2) we should include origin 904 // (3) we are at the start of a leaf that follows a list item marker 905 // ...then return this point. 906 return *this; 907 } 908 909 if (mAcc->IsLocal()) { 910 boundary = aDirection == eDirNext 911 ? FindNextLineStartSameLocalAcc(aIncludeOrigin) 912 : FindPrevLineStartSameLocalAcc(aIncludeOrigin); 913 } else { 914 boundary = FindLineStartSameRemoteAcc(aDirection, aIncludeOrigin); 915 } 916 917 if (aIgnoreListItemMarker && aDirection == eDirPrevious && !boundary && 918 mOffset != 0 && IsLeafAfterListItemMarker()) { 919 // If: 920 // (1) we are ignoring list markers 921 // (2) we are searching backwards in accessible 922 // (3) we did not find a line start before this point 923 // (4) we are in a leaf that follows a list item marker 924 // ...then return the first point in this accessible. 925 boundary = TextLeafPoint(mAcc, 0); 926 } 927 928 return boundary; 929 } 930 931 TextLeafPoint TextLeafPoint::FindPrevWordStartSameAcc( 932 bool aIncludeOrigin) const { 933 if (mOffset == 0 && !aIncludeOrigin) { 934 // We can't go back any further and the caller doesn't want the origin 935 // included, so there's nothing more to do. 936 return TextLeafPoint(); 937 } 938 nsAutoString text; 939 mAcc->AppendTextTo(text); 940 TextLeafPoint lineStart = *this; 941 if (!aIncludeOrigin || (lineStart.mOffset == 1 && text.Length() == 1 && 942 text.CharAt(0) == '\n')) { 943 // We're not interested in a line that starts here, either because 944 // aIncludeOrigin is false or because we're at the end of a line break 945 // node. 946 --lineStart.mOffset; 947 } 948 // A word never starts with a line feed character. If there are multiple 949 // consecutive line feed characters and we're after the first of them, the 950 // previous line start will be a line feed character. Skip this and any prior 951 // consecutive line feed first. 952 for (; lineStart.mOffset >= 0 && text.CharAt(lineStart.mOffset) == '\n'; 953 --lineStart.mOffset) { 954 } 955 if (lineStart.mOffset < 0) { 956 // There's no line start for our purposes. 957 lineStart = TextLeafPoint(); 958 } else { 959 lineStart = 960 lineStart.FindLineStartSameAcc(eDirPrevious, /* aIncludeOrigin */ true); 961 } 962 // Keep walking backward until we find an acceptable word start. 963 intl::WordRange word; 964 if (mOffset == 0) { 965 word.mBegin = 0; 966 } else if (mOffset == static_cast<int32_t>(text.Length())) { 967 word = WordBreaker::FindWord( 968 text, mOffset - 1, 969 StaticPrefs::layout_word_select_stop_at_punctuation() 970 ? FindWordOptions::StopAtPunctuation 971 : FindWordOptions::None); 972 } else { 973 word = WordBreaker::FindWord( 974 text, mOffset, 975 StaticPrefs::layout_word_select_stop_at_punctuation() 976 ? FindWordOptions::StopAtPunctuation 977 : FindWordOptions::None); 978 } 979 for (;; word = WordBreaker::FindWord( 980 text, word.mBegin - 1, 981 StaticPrefs::layout_word_select_stop_at_punctuation() 982 ? FindWordOptions::StopAtPunctuation 983 : FindWordOptions::None)) { 984 if (!aIncludeOrigin && static_cast<int32_t>(word.mBegin) == mOffset) { 985 // A word possibly starts at the origin, but the caller doesn't want this 986 // included. 987 MOZ_ASSERT(word.mBegin != 0); 988 continue; 989 } 990 if (lineStart && static_cast<int32_t>(word.mBegin) < lineStart.mOffset) { 991 // A line start always starts a new word. 992 return lineStart; 993 } 994 if (IsAcceptableWordStart(mAcc, text, static_cast<int32_t>(word.mBegin))) { 995 break; 996 } 997 if (word.mBegin == 0) { 998 // We can't go back any further. 999 if (lineStart) { 1000 // A line start always starts a new word. 1001 return lineStart; 1002 } 1003 return TextLeafPoint(); 1004 } 1005 } 1006 return TextLeafPoint(mAcc, static_cast<int32_t>(word.mBegin)); 1007 } 1008 1009 TextLeafPoint TextLeafPoint::FindNextWordStartSameAcc( 1010 bool aIncludeOrigin) const { 1011 nsAutoString text; 1012 mAcc->AppendTextTo(text); 1013 int32_t wordStart = mOffset; 1014 if (aIncludeOrigin) { 1015 if (wordStart == 0) { 1016 if (IsAcceptableWordStart(mAcc, text, 0)) { 1017 return *this; 1018 } 1019 } else { 1020 // The origin might start a word, so search from just before it. 1021 --wordStart; 1022 } 1023 } 1024 TextLeafPoint lineStart = FindLineStartSameAcc(eDirNext, aIncludeOrigin); 1025 if (lineStart) { 1026 // A word never starts with a line feed character. If there are multiple 1027 // consecutive line feed characters, lineStart will point at the second of 1028 // them. Skip this and any subsequent consecutive line feed. 1029 for (; lineStart.mOffset < static_cast<int32_t>(text.Length()) && 1030 text.CharAt(lineStart.mOffset) == '\n'; 1031 ++lineStart.mOffset) { 1032 } 1033 if (lineStart.mOffset == static_cast<int32_t>(text.Length())) { 1034 // There's no line start for our purposes. 1035 lineStart = TextLeafPoint(); 1036 } 1037 } 1038 // Keep walking forward until we find an acceptable word start. 1039 intl::WordBreakIteratorUtf16 wordBreakIter(text); 1040 int32_t previousPos = wordStart; 1041 Maybe<uint32_t> nextBreak = wordBreakIter.Seek(wordStart); 1042 for (;;) { 1043 if (!nextBreak || *nextBreak == text.Length()) { 1044 if (lineStart) { 1045 // A line start always starts a new word. 1046 return lineStart; 1047 } 1048 if (StaticPrefs::layout_word_select_stop_at_punctuation()) { 1049 // If layout.word_select.stop_at_punctuation is true, we have to look 1050 // for punctuation class since it may not break state in UAX#29. 1051 for (int32_t i = previousPos + 1; 1052 i < static_cast<int32_t>(text.Length()); i++) { 1053 if (IsAcceptableWordStart(mAcc, text, i)) { 1054 return TextLeafPoint(mAcc, i); 1055 } 1056 } 1057 } 1058 return TextLeafPoint(); 1059 } 1060 wordStart = AssertedCast<int32_t>(*nextBreak); 1061 if (lineStart && wordStart > lineStart.mOffset) { 1062 // A line start always starts a new word. 1063 return lineStart; 1064 } 1065 if (IsAcceptableWordStart(mAcc, text, wordStart)) { 1066 break; 1067 } 1068 1069 if (StaticPrefs::layout_word_select_stop_at_punctuation()) { 1070 // If layout.word_select.stop_at_punctuation is true, we have to look 1071 // for punctuation class since it may not break state in UAX#29. 1072 for (int32_t i = previousPos + 1; i < wordStart; i++) { 1073 if (IsAcceptableWordStart(mAcc, text, i)) { 1074 return TextLeafPoint(mAcc, i); 1075 } 1076 } 1077 } 1078 previousPos = wordStart; 1079 nextBreak = wordBreakIter.Next(); 1080 } 1081 return TextLeafPoint(mAcc, wordStart); 1082 } 1083 1084 /* static */ 1085 TextLeafPoint TextLeafPoint::GetCaret(Accessible* aAcc) { 1086 if (LocalAccessible* localAcc = aAcc->AsLocal()) { 1087 // Use the HyperTextAccessible caret offset. Eventually, we'll want to move 1088 // that code into TextLeafPoint, but existing code depends on it being based 1089 // on HyperTextAccessible (including caret events). 1090 int32_t htOffset = -1; 1091 // Try the cached caret. 1092 HyperTextAccessible* ht = SelectionMgr()->AccessibleWithCaret(&htOffset); 1093 if (ht) { 1094 MOZ_ASSERT(htOffset != -1); 1095 } else { 1096 // There is no cached caret, but there might still be a caret; see bug 1097 // 1425112. 1098 ht = HyperTextFor(localAcc); 1099 if (!ht) { 1100 return TextLeafPoint(); 1101 } 1102 // An offset can only refer to a child, but the caret might be in a deeper 1103 // descendant. Walk to the deepest HyperTextAccessible using CaretOffset. 1104 bool gotCaret = false; 1105 for (;;) { 1106 htOffset = ht->CaretOffset(); 1107 if (htOffset == -1) { 1108 break; 1109 } 1110 // A descendant might return -1 in some cases, but it's okay as long as 1111 // the call on the outermost HyperTextAccessible succeeds. 1112 gotCaret = true; 1113 LocalAccessible* child = ht->GetChildAtOffset(htOffset); 1114 if (!child) { 1115 break; 1116 } 1117 if (HyperTextAccessible* childHt = child->AsHyperText()) { 1118 ht = childHt; 1119 } else { 1120 break; 1121 } 1122 } 1123 if (!gotCaret) { 1124 return TextLeafPoint(); 1125 } 1126 } 1127 // As noted above, CaretOffset on a descendant might return -1. Use 0 in 1128 // that case. 1129 TextLeafPoint point = ht->ToTextLeafPoint(htOffset == -1 ? 0 : htOffset); 1130 if (!point) { 1131 // Bug 1905021: This happens in the wild, but we don't understand why. 1132 // ToTextLeafPoint should only fail if the HyperText offset is invalid, 1133 // but CaretOffset shouldn't return an invalid offset. 1134 MOZ_ASSERT_UNREACHABLE( 1135 "Got HyperText CaretOffset but ToTextLeafPoint failed"); 1136 return point; 1137 } 1138 if (!IsCaretValid(point)) { 1139 return TextLeafPoint(); 1140 } 1141 nsIFrame* frame = ht->GetFrame(); 1142 RefPtr<nsFrameSelection> sel = frame ? frame->GetFrameSelection() : nullptr; 1143 if (sel && sel->GetHint() == CaretAssociationHint::Before) { 1144 // CaretAssociationHint::Before can mean that the caret is at the end of 1145 // a line. However, this can also occur in a few other situations: 1146 // 1. The caret is before the start of a node in the middle of a line. 1147 // This happens when moving the cursor forward to a new node. 1148 // 2. The user clicks the mouse on a character other than the first in a 1149 // node. 1150 // 3. The caret is somewhere other than the start of a line and the user 1151 // presses down or up arrow to move by line. 1152 if (point.mOffset < 1153 static_cast<int32_t>(nsAccUtils::TextLength(point.mAcc))) { 1154 // The caret is at the end of a line if the point is at the start of a 1155 // line but not at the start of a paragraph. 1156 point.mIsEndOfLineInsertionPoint = 1157 point.FindPrevLineStartSameLocalAcc(/* aIncludeOrigin */ true) == 1158 point && 1159 !point.IsParagraphStart(); 1160 } else { 1161 // This is the end of a node. CaretAssociationHint::Before is only used 1162 // at the end of a node if the caret is at the end of a line. 1163 point.mIsEndOfLineInsertionPoint = true; 1164 } 1165 } 1166 return point; 1167 } 1168 1169 // Ideally, we'd cache the caret as a leaf, but our events are based on 1170 // HyperText for now. 1171 DocAccessibleParent* remoteDoc = aAcc->AsRemote()->Document(); 1172 auto [ht, htOffset] = remoteDoc->GetCaret(); 1173 if (!ht) { 1174 return TextLeafPoint(); 1175 } 1176 TextLeafPoint point = ht->ToTextLeafPoint(htOffset); 1177 if (!point) { 1178 // The caret offset should usually be in sync with the tree. However, tree 1179 // and selection updates happen using separate IPDL calls, so it's possible 1180 // for a client caret query to arrive between them. Thus, we can end up 1181 // with an invalid caret here. 1182 return point; 1183 } 1184 if (!IsCaretValid(point)) { 1185 return TextLeafPoint(); 1186 } 1187 point.mIsEndOfLineInsertionPoint = remoteDoc->IsCaretAtEndOfLine(); 1188 return point; 1189 } 1190 1191 TextLeafPoint TextLeafPoint::AdjustEndOfLine() const { 1192 MOZ_ASSERT(mIsEndOfLineInsertionPoint); 1193 // Use the last character on the line so that we search for word and line 1194 // boundaries on the current line, not the next line. 1195 return TextLeafPoint(mAcc, mOffset) 1196 .FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); 1197 } 1198 1199 TextLeafPoint TextLeafPoint::FindBoundary(AccessibleTextBoundary aBoundaryType, 1200 nsDirection aDirection, 1201 BoundaryFlags aFlags) const { 1202 if (mIsEndOfLineInsertionPoint) { 1203 // In this block, we deliberately don't propagate mIsEndOfLineInsertionPoint 1204 // to derived points because otherwise, a call to FindBoundary on the 1205 // returned point would also return the same point. 1206 if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR || 1207 aBoundaryType == nsIAccessibleText::BOUNDARY_CLUSTER) { 1208 if (aDirection == eDirNext || (aDirection == eDirPrevious && 1209 aFlags & BoundaryFlags::eIncludeOrigin)) { 1210 // The caller wants the current or next character/cluster. Return no 1211 // character, since otherwise, this would move past the first character 1212 // on the next line. 1213 return TextLeafPoint(mAcc, mOffset); 1214 } 1215 // The caller wants the previous character/cluster. Return that as normal. 1216 return TextLeafPoint(mAcc, mOffset) 1217 .FindBoundary(aBoundaryType, aDirection, aFlags); 1218 } 1219 // For any other boundary, we need to start on this line, not the next, even 1220 // though mOffset refers to the next. 1221 return AdjustEndOfLine().FindBoundary(aBoundaryType, aDirection, aFlags); 1222 } 1223 1224 bool inEditableAndStopInIt = (aFlags & BoundaryFlags::eStopInEditable) && 1225 mAcc->Parent() && 1226 (mAcc->Parent()->State() & states::EDITABLE); 1227 if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) { 1228 return FindLineEnd(aDirection, 1229 inEditableAndStopInIt 1230 ? aFlags 1231 : (aFlags & ~BoundaryFlags::eStopInEditable)); 1232 } 1233 if (aBoundaryType == nsIAccessibleText::BOUNDARY_WORD_END) { 1234 return FindWordEnd(aDirection, 1235 inEditableAndStopInIt 1236 ? aFlags 1237 : (aFlags & ~BoundaryFlags::eStopInEditable)); 1238 } 1239 if ((aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_START || 1240 aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) && 1241 (aFlags & BoundaryFlags::eIncludeOrigin) && aDirection == eDirPrevious && 1242 IsEmptyLastLine()) { 1243 // If we're at an empty line at the end of an Accessible, we don't want to 1244 // walk into the previous line. For example, this can happen if the caret 1245 // is positioned on an empty line at the end of a textarea. 1246 return *this; 1247 } 1248 bool includeOrigin = !!(aFlags & BoundaryFlags::eIncludeOrigin); 1249 bool ignoreListItemMarker = !!(aFlags & BoundaryFlags::eIgnoreListItemMarker); 1250 Accessible* lastAcc = nullptr; 1251 for (TextLeafPoint searchFrom = *this; searchFrom; 1252 searchFrom = searchFrom.NeighborLeafPoint( 1253 aDirection, inEditableAndStopInIt, ignoreListItemMarker)) { 1254 lastAcc = searchFrom.mAcc; 1255 if (ignoreListItemMarker && searchFrom == *this && 1256 searchFrom.mAcc->Role() == roles::LISTITEM_MARKER) { 1257 continue; 1258 } 1259 TextLeafPoint boundary; 1260 // Search for the boundary within the current Accessible. 1261 switch (aBoundaryType) { 1262 case nsIAccessibleText::BOUNDARY_CHAR: 1263 if (includeOrigin) { 1264 boundary = searchFrom; 1265 } else if (aDirection == eDirPrevious && searchFrom.mOffset > 0) { 1266 boundary.mAcc = searchFrom.mAcc; 1267 boundary.mOffset = searchFrom.mOffset - 1; 1268 } else if (aDirection == eDirNext && 1269 searchFrom.mOffset + 1 < 1270 static_cast<int32_t>( 1271 nsAccUtils::TextLength(searchFrom.mAcc))) { 1272 boundary.mAcc = searchFrom.mAcc; 1273 boundary.mOffset = searchFrom.mOffset + 1; 1274 } 1275 break; 1276 case nsIAccessibleText::BOUNDARY_WORD_START: 1277 if (aDirection == eDirPrevious) { 1278 boundary = searchFrom.FindPrevWordStartSameAcc(includeOrigin); 1279 } else { 1280 boundary = searchFrom.FindNextWordStartSameAcc(includeOrigin); 1281 } 1282 break; 1283 case nsIAccessibleText::BOUNDARY_LINE_START: 1284 boundary = searchFrom.FindLineStartSameAcc(aDirection, includeOrigin, 1285 ignoreListItemMarker); 1286 break; 1287 case nsIAccessibleText::BOUNDARY_PARAGRAPH: 1288 boundary = searchFrom.FindParagraphSameAcc(aDirection, includeOrigin, 1289 ignoreListItemMarker); 1290 break; 1291 case nsIAccessibleText::BOUNDARY_CLUSTER: 1292 boundary = searchFrom.FindClusterSameAcc(aDirection, includeOrigin); 1293 break; 1294 default: 1295 MOZ_ASSERT_UNREACHABLE(); 1296 break; 1297 } 1298 if (boundary) { 1299 return boundary; 1300 } 1301 1302 // The start/end of the Accessible might be a boundary. If so, we must stop 1303 // on it. 1304 includeOrigin = true; 1305 } 1306 1307 MOZ_ASSERT(lastAcc); 1308 // No further leaf was found. Use the start/end of the first/last leaf. 1309 return TextLeafPoint( 1310 lastAcc, aDirection == eDirPrevious 1311 ? 0 1312 : static_cast<int32_t>(nsAccUtils::TextLength(lastAcc))); 1313 } 1314 1315 TextLeafPoint TextLeafPoint::FindLineEnd(nsDirection aDirection, 1316 BoundaryFlags aFlags) const { 1317 if (aDirection == eDirPrevious && IsEmptyLastLine()) { 1318 // If we're at an empty line at the end of an Accessible, we don't want to 1319 // walk into the previous line. For example, this can happen if the caret 1320 // is positioned on an empty line at the end of a textarea. 1321 // Because we want the line end, we must walk back to the line feed 1322 // character. 1323 return FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, 1324 aFlags & ~BoundaryFlags::eIncludeOrigin); 1325 } 1326 if ((aFlags & BoundaryFlags::eIncludeOrigin) && IsLineFeedChar()) { 1327 return *this; 1328 } 1329 if (aDirection == eDirPrevious && !(aFlags & BoundaryFlags::eIncludeOrigin)) { 1330 // If there is a line feed immediately before us, return that. 1331 TextLeafPoint prevChar = 1332 FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, 1333 aFlags & ~BoundaryFlags::eIncludeOrigin); 1334 if (prevChar.IsLineFeedChar()) { 1335 return prevChar; 1336 } 1337 } 1338 TextLeafPoint searchFrom = *this; 1339 if (aDirection == eDirNext && IsLineFeedChar()) { 1340 // If we search for the next line start from a line feed, we'll get the 1341 // character immediately following the line feed. We actually want the 1342 // next line start after that. Skip the line feed. 1343 searchFrom = FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext, 1344 aFlags & ~BoundaryFlags::eIncludeOrigin); 1345 } 1346 TextLeafPoint lineStart = searchFrom.FindBoundary( 1347 nsIAccessibleText::BOUNDARY_LINE_START, aDirection, aFlags); 1348 if (aDirection == eDirNext && IsEmptyLastLine()) { 1349 // There is a line feed immediately before us, but that's actually the end 1350 // of the previous line, not the end of our empty line. Don't walk back. 1351 return lineStart; 1352 } 1353 // If there is a line feed before this line start (at the end of the previous 1354 // line), we must return that. 1355 TextLeafPoint prevChar = 1356 lineStart.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, 1357 aFlags & ~BoundaryFlags::eIncludeOrigin); 1358 if (prevChar && prevChar.IsLineFeedChar()) { 1359 return prevChar; 1360 } 1361 return lineStart; 1362 } 1363 1364 bool TextLeafPoint::IsSpace() const { 1365 return GetWordBreakClass(GetChar()) == eWbcSpace; 1366 } 1367 1368 TextLeafPoint TextLeafPoint::FindWordEnd(nsDirection aDirection, 1369 BoundaryFlags aFlags) const { 1370 char16_t origChar = GetChar(); 1371 const bool origIsSpace = GetWordBreakClass(origChar) == eWbcSpace; 1372 bool prevIsSpace = false; 1373 if (aDirection == eDirPrevious || 1374 ((aFlags & BoundaryFlags::eIncludeOrigin) && origIsSpace) || !origChar) { 1375 TextLeafPoint prev = 1376 FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, 1377 aFlags & ~BoundaryFlags::eIncludeOrigin); 1378 if (aDirection == eDirPrevious && prev == *this) { 1379 return *this; // Can't go any further. 1380 } 1381 prevIsSpace = prev.IsSpace(); 1382 if ((aFlags & BoundaryFlags::eIncludeOrigin) && 1383 (origIsSpace || IsDocEdge(eDirNext)) && !prevIsSpace) { 1384 // The origin is space or end of document, but the previous 1385 // character is not. This means we're at the end of a word. 1386 return *this; 1387 } 1388 } 1389 TextLeafPoint boundary = *this; 1390 if (aDirection == eDirPrevious && !prevIsSpace) { 1391 // If there isn't space immediately before us, first find the start of the 1392 // previous word. 1393 boundary = FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, 1394 eDirPrevious, aFlags); 1395 } else if (aDirection == eDirNext && 1396 (origIsSpace || (!origChar && prevIsSpace))) { 1397 // We're within the space at the end of the word. Skip over the space. We 1398 // can do that by searching for the next word start. 1399 boundary = FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirNext, 1400 aFlags & ~BoundaryFlags::eIncludeOrigin); 1401 if (boundary.IsSpace()) { 1402 // The next word starts with a space. This can happen if there is a space 1403 // after or at the start of a block element. 1404 return boundary; 1405 } 1406 } 1407 if (aDirection == eDirNext) { 1408 BoundaryFlags flags = aFlags; 1409 if (IsDocEdge(eDirPrevious)) { 1410 // If this is the start of the doc don't be inclusive in the word-start 1411 // search because there is no preceding block where this could be a 1412 // word-end for. 1413 flags &= ~BoundaryFlags::eIncludeOrigin; 1414 } 1415 boundary = boundary.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, 1416 eDirNext, flags); 1417 } 1418 // At this point, boundary is either the start of a word or at a space. A 1419 // word ends at the beginning of consecutive space. Therefore, skip back to 1420 // the start of any space before us. 1421 TextLeafPoint prev = boundary; 1422 for (;;) { 1423 prev = prev.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, 1424 aFlags & ~BoundaryFlags::eIncludeOrigin); 1425 if (prev == boundary) { 1426 break; // Can't go any further. 1427 } 1428 if (!prev.IsSpace()) { 1429 break; 1430 } 1431 boundary = prev; 1432 } 1433 return boundary; 1434 } 1435 1436 TextLeafPoint TextLeafPoint::FindParagraphSameAcc( 1437 nsDirection aDirection, bool aIncludeOrigin, 1438 bool aIgnoreListItemMarker) const { 1439 if (aIncludeOrigin && IsDocEdge(eDirPrevious)) { 1440 // The top of the document is a paragraph boundary. 1441 return *this; 1442 } 1443 1444 if (aIgnoreListItemMarker && aIncludeOrigin && mOffset == 0 && 1445 IsLeafAfterListItemMarker()) { 1446 // If we are in a list item and the previous sibling is 1447 // a bullet, the 0 offset in this leaf is a line start. 1448 return *this; 1449 } 1450 1451 if (mAcc->IsTextLeaf() && 1452 // We don't want to copy strings unnecessarily. See below for the context 1453 // of these individual conditions. 1454 ((aIncludeOrigin && mOffset > 0) || aDirection == eDirNext || 1455 mOffset >= 2)) { 1456 // If there is a line feed, a new paragraph begins after it. 1457 nsAutoString text; 1458 mAcc->AppendTextTo(text); 1459 if (aIncludeOrigin && mOffset > 0 && text.CharAt(mOffset - 1) == '\n') { 1460 return TextLeafPoint(mAcc, mOffset); 1461 } 1462 int32_t lfOffset = -1; 1463 if (aDirection == eDirNext) { 1464 lfOffset = text.FindChar('\n', mOffset); 1465 } else if (mOffset >= 2) { 1466 // A line feed at mOffset - 1 means the origin begins a new paragraph, 1467 // but we already handled aIncludeOrigin above. Therefore, we search from 1468 // mOffset - 2. 1469 lfOffset = text.RFindChar('\n', mOffset - 2); 1470 } 1471 if (lfOffset != -1 && lfOffset + 1 < static_cast<int32_t>(text.Length())) { 1472 return TextLeafPoint(mAcc, lfOffset + 1); 1473 } 1474 } 1475 1476 if (aIgnoreListItemMarker && mOffset > 0 && aDirection == eDirPrevious && 1477 IsLeafAfterListItemMarker()) { 1478 // No line breaks were found in the preceding text to this offset. 1479 // If we are in a list item and the previous sibling is 1480 // a bullet, the 0 offset in this leaf is a line start. 1481 return TextLeafPoint(mAcc, 0); 1482 } 1483 1484 // Check whether this Accessible begins a paragraph. 1485 if ((!aIncludeOrigin && mOffset == 0) || 1486 (aDirection == eDirNext && mOffset > 0)) { 1487 // The caller isn't interested in whether this Accessible begins a 1488 // paragraph. 1489 return TextLeafPoint(); 1490 } 1491 Accessible* prevLeaf = PrevLeaf(mAcc); 1492 BlockRule blockRule; 1493 Pivot pivot(nsAccUtils::DocumentFor(mAcc)); 1494 Accessible* prevBlock = pivot.Prev(mAcc, blockRule); 1495 // Check if we're the first leaf after a block element. 1496 if (prevBlock) { 1497 if ( 1498 // If there's no previous leaf, we must be the first leaf after the 1499 // block. 1500 !prevLeaf || 1501 // A block can be a leaf; e.g. an empty div or paragraph. 1502 prevBlock == prevLeaf) { 1503 return TextLeafPoint(mAcc, 0); 1504 } 1505 if (prevBlock->IsAncestorOf(mAcc)) { 1506 // We're inside the block. 1507 if (!prevBlock->IsAncestorOf(prevLeaf)) { 1508 // The previous leaf isn't inside the block. That means we're the first 1509 // leaf in the block. 1510 return TextLeafPoint(mAcc, 0); 1511 } 1512 } else { 1513 // We aren't inside the block, so the block ends before us. 1514 if (prevBlock->IsAncestorOf(prevLeaf)) { 1515 // The previous leaf is inside the block. That means we're the first 1516 // leaf after the block. This case is necessary because a block causes a 1517 // paragraph break both before and after it. 1518 return TextLeafPoint(mAcc, 0); 1519 } 1520 } 1521 } 1522 if (!prevLeaf || prevLeaf->IsHTMLBr()) { 1523 // We're the first leaf after a line break or the start of the document. 1524 return TextLeafPoint(mAcc, 0); 1525 } 1526 if (prevLeaf->IsTextLeaf()) { 1527 // There's a text leaf before us. Check if it ends with a line feed. 1528 nsAutoString text; 1529 prevLeaf->AppendTextTo(text, nsAccUtils::TextLength(prevLeaf) - 1, 1); 1530 if (text.CharAt(0) == '\n') { 1531 return TextLeafPoint(mAcc, 0); 1532 } 1533 } 1534 return TextLeafPoint(); 1535 } 1536 1537 TextLeafPoint TextLeafPoint::FindClusterSameAcc(nsDirection aDirection, 1538 bool aIncludeOrigin) const { 1539 // We don't support clusters which cross nodes. We can live with that because 1540 // editor doesn't seem to fully support this either. 1541 if (aIncludeOrigin && mOffset == 0) { 1542 // Since we don't cross nodes, offset 0 always begins a cluster. 1543 return *this; 1544 } 1545 if (aDirection == eDirPrevious) { 1546 if (mOffset == 0) { 1547 // We can't go back any further. 1548 return TextLeafPoint(); 1549 } 1550 if (!aIncludeOrigin && mOffset == 1) { 1551 // Since we don't cross nodes, offset 0 always begins a cluster. We can't 1552 // take this fast path if aIncludeOrigin is true because offset 1 might 1553 // start a cluster, but we don't know that yet. 1554 return TextLeafPoint(mAcc, 0); 1555 } 1556 } 1557 nsAutoString text; 1558 mAcc->AppendTextTo(text); 1559 if (text.IsEmpty()) { 1560 return TextLeafPoint(); 1561 } 1562 if (aDirection == eDirNext && 1563 mOffset == static_cast<int32_t>(text.Length())) { 1564 return TextLeafPoint(); 1565 } 1566 // There is GraphemeClusterBreakReverseIteratorUtf16, but it "doesn't 1567 // handle conjoining Jamo and emoji". Therefore, we must use 1568 // GraphemeClusterBreakIteratorUtf16 even when moving backward. 1569 // GraphemeClusterBreakIteratorUtf16::Seek() always starts from the beginning 1570 // and repeatedly calls Next(), regardless of the seek offset. The best we 1571 // can do is call Next() until we find the offset we need. 1572 intl::GraphemeClusterBreakIteratorUtf16 iter(text); 1573 // Since we don't cross nodes, offset 0 always begins a cluster. 1574 int32_t prevCluster = 0; 1575 while (Maybe<uint32_t> next = iter.Next()) { 1576 int32_t cluster = static_cast<int32_t>(*next); 1577 if (aIncludeOrigin && cluster == mOffset) { 1578 return *this; 1579 } 1580 if (aDirection == eDirPrevious) { 1581 if (cluster >= mOffset) { 1582 return TextLeafPoint(mAcc, prevCluster); 1583 } 1584 prevCluster = cluster; 1585 } else if (cluster > mOffset) { 1586 MOZ_ASSERT(aDirection == eDirNext); 1587 return TextLeafPoint(mAcc, cluster); 1588 } 1589 } 1590 return TextLeafPoint(); 1591 } 1592 1593 void TextLeafPoint::AddTextOffsetAttributes(AccAttributes* aAttrs) const { 1594 auto expose = [aAttrs](nsAtom* aAttr) { 1595 if (aAttr == nsGkAtoms::spelling || aAttr == nsGkAtoms::grammar) { 1596 // XXX We don't correctly handle exposure of overlapping spelling and 1597 // grammar errors. See bug 1944217. For now, we expose the one we most 1598 // recently encountered. 1599 aAttrs->SetAttribute(nsGkAtoms::invalid, aAttr); 1600 } else if (aAttr == nsGkAtoms::mark) { 1601 aAttrs->SetAttribute(aAttr, true); 1602 } 1603 }; 1604 1605 if (LocalAccessible* acc = mAcc->AsLocal()) { 1606 auto ranges = FindDOMTextOffsetAttributes(acc, mOffset, mOffset + 1); 1607 for (auto& [domRanges, attr] : ranges) { 1608 MOZ_ASSERT(domRanges.Length() >= 1); 1609 expose(attr); 1610 } 1611 return; 1612 } 1613 1614 RemoteAccessible* acc = mAcc->AsRemote(); 1615 MOZ_ASSERT(acc); 1616 if (RequestDomainsIfInactive(CacheDomain::TextOffsetAttributes)) { 1617 return; 1618 } 1619 if (!acc->mCachedFields) { 1620 return; 1621 } 1622 auto offsetAttrs = 1623 acc->mCachedFields->GetAttribute<nsTArray<TextOffsetAttribute>>( 1624 CacheKey::TextOffsetAttributes); 1625 if (!offsetAttrs) { 1626 return; 1627 } 1628 // offsetAttrs is sorted by mStartOffset, but ranges can overlap each other. 1629 // Thus, we must check all ranges with an encompassing start offset. 1630 for (const TextOffsetAttribute& range : *offsetAttrs) { 1631 if (range.mStartOffset > mOffset) { 1632 // offsetAttrs is sorted by mStartOffset. Therefor, there aren't any 1633 // ranges of interest after this. 1634 break; 1635 } 1636 if (range.mEndOffset != TextOffsetAttribute::kOutsideLeaf && 1637 range.mEndOffset <= mOffset) { 1638 // range ends inside mAcc but before mOffset, so it doesn't encompass us. 1639 continue; 1640 } 1641 // mOffset is within range. 1642 expose(range.mAttribute); 1643 } 1644 } 1645 1646 TextLeafPoint TextLeafPoint::FindTextOffsetAttributeSameAcc( 1647 nsDirection aDirection, bool aIncludeOrigin) const { 1648 if (!aIncludeOrigin && mOffset == 0 && aDirection == eDirPrevious) { 1649 return TextLeafPoint(); 1650 } 1651 if (LocalAccessible* acc = mAcc->AsLocal()) { 1652 nsINode* node = acc->GetNode(); 1653 // There are multiple selection types. The ranges for each selection type 1654 // are sorted, but the ranges aren't sorted between selection types. 1655 // Therefore, we need to look for the closest matching offset in each 1656 // selection type. We keep track of that in the dest variable as we check 1657 // each selection type. 1658 int32_t dest = -1; 1659 if (aDirection == eDirNext) { 1660 // We want to find both start and end points, so we pass true for 1661 // aAllowAdjacent. 1662 auto ranges = FindDOMTextOffsetAttributes( 1663 acc, mOffset, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT, 1664 /* aAllowAdjacent */ true); 1665 for (auto& [domRanges, attr] : ranges) { 1666 for (dom::AbstractRange* domRange : domRanges) { 1667 if (domRange->GetStartContainer() == node) { 1668 int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset( 1669 acc, static_cast<int32_t>(domRange->StartOffset()))); 1670 if (aIncludeOrigin && matchOffset == mOffset) { 1671 return *this; 1672 } 1673 if (matchOffset > mOffset) { 1674 if (dest == -1 || matchOffset <= dest) { 1675 dest = matchOffset; 1676 } 1677 // ranges is sorted by start, so there can't be a closer range 1678 // offset after this. This is the only case where we can break 1679 // out of the loop. In the cases below, we must keep iterating 1680 // because the end offsets aren't sorted. 1681 break; 1682 } 1683 } 1684 if (domRange->GetEndContainer() == node) { 1685 int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset( 1686 acc, static_cast<int32_t>(domRange->EndOffset()))); 1687 if (aIncludeOrigin && matchOffset == mOffset) { 1688 return *this; 1689 } 1690 if (matchOffset > mOffset && (dest == -1 || matchOffset <= dest)) { 1691 dest = matchOffset; 1692 } 1693 } 1694 } 1695 } 1696 } else { 1697 auto ranges = FindDOMTextOffsetAttributes(acc, 0, mOffset, 1698 /* aAllowAdjacent */ true); 1699 for (auto& [domRanges, attr] : ranges) { 1700 for (dom::AbstractRange* domRange : Reversed(domRanges)) { 1701 if (domRange->GetEndContainer() == node) { 1702 int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset( 1703 acc, static_cast<int32_t>(domRange->EndOffset()))); 1704 if (aIncludeOrigin && matchOffset == mOffset) { 1705 return *this; 1706 } 1707 if (matchOffset < mOffset && (dest == -1 || matchOffset >= dest)) { 1708 dest = matchOffset; 1709 } 1710 } 1711 if (domRange->GetStartContainer() == node) { 1712 int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset( 1713 acc, static_cast<int32_t>(domRange->StartOffset()))); 1714 if (aIncludeOrigin && matchOffset == mOffset) { 1715 return *this; 1716 } 1717 if (matchOffset < mOffset && (dest == -1 || matchOffset >= dest)) { 1718 dest = matchOffset; 1719 } 1720 } 1721 } 1722 } 1723 } 1724 if (dest == -1) { 1725 return TextLeafPoint(); 1726 } 1727 return TextLeafPoint(mAcc, dest); 1728 } 1729 1730 RemoteAccessible* acc = mAcc->AsRemote(); 1731 MOZ_ASSERT(acc); 1732 if (RequestDomainsIfInactive(CacheDomain::TextOffsetAttributes)) { 1733 return TextLeafPoint(); 1734 } 1735 if (!acc->mCachedFields) { 1736 return TextLeafPoint(); 1737 } 1738 auto offsetAttrs = 1739 acc->mCachedFields->GetAttribute<nsTArray<TextOffsetAttribute>>( 1740 CacheKey::TextOffsetAttributes); 1741 if (!offsetAttrs) { 1742 return TextLeafPoint(); 1743 } 1744 // offsetAttrs is sorted by mStartOffset, but ranges can overlap each other. 1745 // Therefore, we must consider all ranges with an encompassing start offset. 1746 // An earlier range might end after a later range, so we keep track of the 1747 // closest offset in the dest variable and adjust that as we iterate. 1748 int32_t dest = -1; 1749 for (const TextOffsetAttribute& range : *offsetAttrs) { 1750 // Although range end offsets are exclusive, we must still treat them as a 1751 // boundary, since the end of a range still means a change in text 1752 // attributes and text offset attribute ranges do not have to be adjacent. 1753 if (aIncludeOrigin && 1754 (range.mStartOffset == mOffset || range.mEndOffset == mOffset)) { 1755 return *this; 1756 } 1757 if (aDirection == eDirNext) { 1758 if (range.mStartOffset > mOffset) { 1759 if (dest == -1 || range.mStartOffset < dest) { 1760 // range.mStartOffset is the closest offset we've seen thus far. 1761 dest = range.mStartOffset; 1762 } 1763 // offsetAttrs is sorted by mStartOffset, so there can't be a closer 1764 // range offset after this. 1765 break; 1766 } 1767 if (range.mEndOffset > mOffset && 1768 (dest == -1 || range.mEndOffset < dest)) { 1769 // range.mEndOffset is the closest offset we've seen thus far. 1770 dest = range.mEndOffset; 1771 } 1772 } else { 1773 if (range.mEndOffset != TextOffsetAttribute::kOutsideLeaf && 1774 range.mEndOffset < mOffset && range.mEndOffset > dest) { 1775 // range.mEndOffset is the closest offset we've seen thus far. 1776 dest = range.mEndOffset; 1777 } 1778 if (range.mStartOffset >= mOffset) { 1779 // offsetAttrs is sorted by mStartOffset, so any range hereafter is in 1780 // the wrong direction. 1781 break; 1782 } 1783 if (range.mStartOffset != TextOffsetAttribute::kOutsideLeaf && 1784 range.mStartOffset > dest) { 1785 // range.mStartOffset is the closest offset we've seen thus far. 1786 dest = range.mStartOffset; 1787 } 1788 } 1789 } 1790 if (dest == -1) { 1791 // There's no boundary in the requested direction. 1792 return TextLeafPoint(); 1793 } 1794 return TextLeafPoint(mAcc, dest); 1795 } 1796 1797 TextLeafPoint TextLeafPoint::NeighborLeafPoint( 1798 nsDirection aDirection, bool aIsEditable, 1799 bool aIgnoreListItemMarker) const { 1800 Accessible* acc = aDirection == eDirPrevious 1801 ? PrevLeaf(mAcc, aIsEditable, aIgnoreListItemMarker) 1802 : NextLeaf(mAcc, aIsEditable, aIgnoreListItemMarker); 1803 if (!acc) { 1804 return TextLeafPoint(); 1805 } 1806 1807 return TextLeafPoint( 1808 acc, aDirection == eDirPrevious 1809 ? static_cast<int32_t>(nsAccUtils::TextLength(acc)) - 1 1810 : 0); 1811 } 1812 1813 LayoutDeviceIntRect TextLeafPoint::ComputeBoundsFromFrame() const { 1814 LocalAccessible* local = mAcc->AsLocal(); 1815 MOZ_ASSERT(local, "Can't compute bounds in frame from non-local acc"); 1816 nsIFrame* frame = local->GetFrame(); 1817 MOZ_ASSERT(frame, "No frame found for acc!"); 1818 1819 if (!frame || !frame->IsTextFrame()) { 1820 return local->Bounds(); 1821 } 1822 1823 // Substring must be entirely within the same text node. 1824 MOZ_ASSERT(frame->IsPrimaryFrame(), 1825 "Cannot compute content offset on non-primary frame"); 1826 nsIFrame::RenderedText text = frame->GetRenderedText( 1827 mOffset, mOffset + 1, nsIFrame::TextOffsetType::OffsetsInRenderedText, 1828 nsIFrame::TrailingWhitespace::DontTrim); 1829 int32_t contentOffset = text.mOffsetWithinNodeText; 1830 int32_t contentOffsetInFrame; 1831 // Get the right frame continuation -- not really a child, but a sibling of 1832 // the primary frame passed in 1833 nsresult rv = frame->GetChildFrameContainingOffset( 1834 contentOffset, true, &contentOffsetInFrame, &frame); 1835 NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect()); 1836 1837 // Start with this frame's screen rect, which we will shrink based on 1838 // the char we care about within it. 1839 nsRect frameScreenRect = frame->GetScreenRectInAppUnits(); 1840 1841 // Add the point where the char starts to the frameScreenRect 1842 nsPoint frameTextStartPoint; 1843 rv = frame->GetPointFromOffset(contentOffset, &frameTextStartPoint); 1844 NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect()); 1845 1846 // Use the next offset to calculate the width 1847 // XXX(morgan) does this work for vertical text? 1848 nsPoint frameTextEndPoint; 1849 rv = frame->GetPointFromOffset(contentOffset + 1, &frameTextEndPoint); 1850 NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect()); 1851 1852 frameScreenRect.SetRectX( 1853 frameScreenRect.X() + 1854 std::min(frameTextStartPoint.x, frameTextEndPoint.x), 1855 mozilla::Abs(frameTextStartPoint.x - frameTextEndPoint.x)); 1856 1857 nsPresContext* presContext = local->Document()->PresContext(); 1858 return LayoutDeviceIntRect::FromAppUnitsToNearest( 1859 frameScreenRect, presContext->AppUnitsPerDevPixel()); 1860 } 1861 1862 LayoutDeviceIntRect TextLeafPoint::InsertionPointBounds() const { 1863 if (TextLeafPoint::GetCaret(mAcc) == *this) { 1864 for (Accessible* acc = mAcc; acc; acc = acc->Parent()) { 1865 if (HyperTextAccessibleBase* ht = acc->AsHyperTextBase()) { 1866 return ht->GetCaretRect().first; 1867 } 1868 } 1869 } 1870 1871 LayoutDeviceIntRect currentBounds = CharBounds(); 1872 if (currentBounds.IsEmpty()) { 1873 return LayoutDeviceIntRect(); 1874 } 1875 1876 // When 'reversed' is true we calculate the writing direction using a 1877 // neighboring character that is past the current one (eg. in LTR the 1878 // character to the right.) 1879 bool reversed = false; 1880 // We are conservative here and stay within the bounds of the editable so 1881 // we don't get confused with other text-flows outside of this block. 1882 TextLeafPoint neighborChar = 1883 FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, 1884 BoundaryFlags::eStopInEditable); 1885 if (*this == neighborChar) { 1886 // If the current char is the first, use the next char past the current 1887 // to extrapolate the writing direction. 1888 neighborChar = FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext, 1889 BoundaryFlags::eStopInEditable); 1890 reversed = true; 1891 } else { 1892 if (*this == 1893 FindBoundary( 1894 nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious, 1895 BoundaryFlags::eStopInEditable | BoundaryFlags::eIncludeOrigin)) { 1896 // If this char is a newline the previous char will be on the previous 1897 // line either equal or past the current one (eg. in LTR the previous char 1898 // will be to the right of the current instead of the left.) 1899 reversed = true; 1900 } 1901 } 1902 1903 LayoutDeviceIntRect neighborBounds = neighborChar.CharBounds(); 1904 if (neighborBounds.IsEmpty()) { 1905 // Either the point is invalid or the accessible is not visible. 1906 // We tried, didn't we.. 1907 return LayoutDeviceIntRect(); 1908 } 1909 1910 // An axis-agnostic function that determines writing direction and aligns 1911 // the caret where insertion point should be. 1912 auto alignRectToInsertion = [](int32_t neighborStart, bool reversed, 1913 int32_t& currentStart, 1914 int32_t& currentLength) { 1915 // The caret is always 1px wide (or tall, in vertical text). 1916 const int32_t caretLength = 1; 1917 int32_t delta = (neighborStart - currentStart); 1918 if (reversed) { 1919 delta *= -1; 1920 } 1921 if (delta > 0) { 1922 // Previous character is to the end (eg. right in ltr) of the current one, 1923 // or next character is to the start (eg. left in ltr) of the current one. 1924 // Align the caret to the end of the current character. 1925 currentStart += currentLength - caretLength; 1926 } 1927 currentLength = caretLength; 1928 }; 1929 1930 WritingMode wm = mAcc->GetWritingMode(); 1931 if (wm == WritingMode() && mAcc->Parent()) { 1932 // mAcc is probably a text leaf with no stored writing mode, use its parent. 1933 wm = mAcc->Parent()->GetWritingMode(); 1934 } 1935 1936 if (!wm.IsVertical()) { 1937 // Horizontal text. 1938 alignRectToInsertion(neighborBounds.x, reversed, currentBounds.x, 1939 currentBounds.width); 1940 } else { 1941 // Vertical text 1942 alignRectToInsertion(neighborBounds.y, reversed, currentBounds.y, 1943 currentBounds.height); 1944 } 1945 1946 return currentBounds; 1947 } 1948 1949 /* static */ 1950 nsTArray<TextOffsetAttribute> TextLeafPoint::GetTextOffsetAttributes( 1951 LocalAccessible* aAcc) { 1952 nsINode* node = aAcc->GetNode(); 1953 auto ranges = FindDOMTextOffsetAttributes( 1954 aAcc, 0, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); 1955 size_t capacity = 0; 1956 for (auto& [domRanges, attr] : ranges) { 1957 capacity += domRanges.Length(); 1958 } 1959 nsTArray<TextOffsetAttribute> offsets(capacity); 1960 for (auto& [domRanges, attr] : ranges) { 1961 for (dom::AbstractRange* domRange : domRanges) { 1962 TextOffsetAttribute& data = *offsets.AppendElement(); 1963 data.mAttribute = attr; 1964 if (domRange->GetStartContainer() == node) { 1965 data.mStartOffset = static_cast<int32_t>(ContentToRenderedOffset( 1966 aAcc, static_cast<int32_t>(domRange->StartOffset()))); 1967 } else { 1968 // This range overlaps aAcc, but starts before it. 1969 // This can only happen for the first range. 1970 MOZ_ASSERT(domRange == *domRanges.begin()); 1971 data.mStartOffset = TextOffsetAttribute::kOutsideLeaf; 1972 } 1973 if (domRange->GetEndContainer() == node) { 1974 data.mEndOffset = static_cast<int32_t>(ContentToRenderedOffset( 1975 aAcc, static_cast<int32_t>(domRange->EndOffset()))); 1976 } else { 1977 // This range overlaps aAcc, but ends after it. 1978 // This can only happen for the last range. 1979 MOZ_ASSERT(domRange == *domRanges.rbegin()); 1980 data.mEndOffset = TextOffsetAttribute::kOutsideLeaf; 1981 } 1982 } 1983 } 1984 offsets.Sort(); 1985 return offsets; 1986 } 1987 1988 /* static */ 1989 void TextLeafPoint::UpdateCachedTextOffsetAttributes( 1990 dom::Document* aDocument, const dom::AbstractRange& aRange) { 1991 DocAccessible* docAcc = GetExistingDocAccessible(aDocument); 1992 if (!docAcc) { 1993 return; 1994 } 1995 LocalAccessible* startAcc = docAcc->GetAccessible(aRange.GetStartContainer()); 1996 LocalAccessible* endAcc = docAcc->GetAccessible(aRange.GetEndContainer()); 1997 if (!startAcc || !endAcc) { 1998 return; 1999 } 2000 for (Accessible* acc = startAcc; acc; acc = NextLeaf(acc)) { 2001 if (acc->IsTextLeaf()) { 2002 docAcc->QueueCacheUpdate(acc->AsLocal(), 2003 CacheDomain::TextOffsetAttributes); 2004 } 2005 if (acc == endAcc) { 2006 // Subtle: We check this here rather than in the loop condition because 2007 // we want to include endAcc but stop once we reach it. Putting it in the 2008 // loop condition would mean we stop at endAcc, but we would also exclude 2009 // it; i.e. we wouldn't push the cache for it. 2010 break; 2011 } 2012 } 2013 } 2014 2015 already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributesLocalAcc( 2016 bool aIncludeDefaults) const { 2017 LocalAccessible* acc = mAcc->AsLocal(); 2018 MOZ_ASSERT(acc); 2019 MOZ_ASSERT(acc->IsText()); 2020 // TextAttrsMgr wants a HyperTextAccessible. 2021 LocalAccessible* parent = acc->LocalParent(); 2022 if (!parent) { 2023 // This should only happen if a client query occurs after a hide event is 2024 // queued for acc and after acc is detached from the document, but before 2025 // the event is fired and thus before acc is shut down. 2026 MOZ_ASSERT(!acc->IsInDocument()); 2027 return nullptr; 2028 } 2029 HyperTextAccessible* hyperAcc = parent->AsHyperText(); 2030 MOZ_ASSERT(hyperAcc); 2031 RefPtr<AccAttributes> attributes = new AccAttributes(); 2032 if (hyperAcc) { 2033 TextAttrsMgr mgr(hyperAcc, aIncludeDefaults, acc); 2034 mgr.GetAttributes(attributes); 2035 } 2036 return attributes.forget(); 2037 } 2038 2039 already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributes( 2040 bool aIncludeDefaults) const { 2041 if (!mAcc->IsText()) { 2042 return nullptr; 2043 } 2044 RefPtr<AccAttributes> attrs; 2045 if (mAcc->IsLocal()) { 2046 attrs = GetTextAttributesLocalAcc(aIncludeDefaults); 2047 } else { 2048 if (aIncludeDefaults) { 2049 attrs = mAcc->AsRemote()->DefaultTextAttributes(); 2050 } else { 2051 attrs = new AccAttributes(); 2052 } 2053 if (auto thisAttrs = mAcc->AsRemote()->GetCachedTextAttributes()) { 2054 thisAttrs->CopyTo(attrs); 2055 } 2056 } 2057 AddTextOffsetAttributes(attrs); 2058 return attrs.forget(); 2059 } 2060 2061 TextLeafPoint TextLeafPoint::FindTextAttrsStart(nsDirection aDirection, 2062 bool aIncludeOrigin) const { 2063 if (mIsEndOfLineInsertionPoint) { 2064 return AdjustEndOfLine().FindTextAttrsStart(aDirection, aIncludeOrigin); 2065 } 2066 RefPtr<const AccAttributes> lastAttrs; 2067 if (mAcc->IsText()) { 2068 lastAttrs = GetTextAttributes(); 2069 } 2070 if (aIncludeOrigin && aDirection == eDirNext && mOffset == 0) { 2071 if (!mAcc->IsText()) { 2072 // Anything other than text breaks an attrs run. 2073 return *this; 2074 } 2075 // Even when searching forward, the only way to know whether the origin is 2076 // the start of a text attrs run is to compare with the previous sibling. 2077 TextLeafPoint point; 2078 point.mAcc = mAcc->PrevSibling(); 2079 if (!point.mAcc || !point.mAcc->IsText()) { 2080 return *this; 2081 } 2082 RefPtr<const AccAttributes> attrs = point.GetTextAttributes(); 2083 if (attrs && lastAttrs && !attrs->Equal(lastAttrs)) { 2084 return *this; 2085 } 2086 } 2087 TextLeafPoint lastPoint = *this; 2088 // If we're at the start of the container and searching for a previous start, 2089 // start the search from the previous leaf. Otherwise, we'll miss the previous 2090 // start. 2091 const bool shouldTraversePrevLeaf = [&]() { 2092 const bool shouldTraverse = 2093 !aIncludeOrigin && aDirection == eDirPrevious && mOffset == 0; 2094 Accessible* prevSibling = mAcc->PrevSibling(); 2095 if (prevSibling) { 2096 return shouldTraverse && !prevSibling->IsText(); 2097 } 2098 return shouldTraverse; 2099 }(); 2100 if (shouldTraversePrevLeaf) { 2101 // Go to the previous leaf and start the search from there, if it exists. 2102 Accessible* prevLeaf = PrevLeaf(mAcc); 2103 if (!prevLeaf) { 2104 return *this; 2105 } 2106 lastPoint = TextLeafPoint( 2107 prevLeaf, static_cast<int32_t>(nsAccUtils::TextLength(prevLeaf))); 2108 } 2109 // This loop searches within a container (that is, it only looks at siblings). 2110 // We might cross containers before or after this loop, but not within it. 2111 for (;;) { 2112 if (TextLeafPoint offsetAttr = lastPoint.FindTextOffsetAttributeSameAcc( 2113 aDirection, aIncludeOrigin && lastPoint.mAcc == mAcc)) { 2114 // An offset attribute starts or ends somewhere in the Accessible we're 2115 // considering. This causes an attribute change, so return that point. 2116 return offsetAttr; 2117 } 2118 TextLeafPoint point; 2119 point.mAcc = aDirection == eDirNext ? lastPoint.mAcc->NextSibling() 2120 : lastPoint.mAcc->PrevSibling(); 2121 if (!point.mAcc || !point.mAcc->IsText()) { 2122 break; 2123 } 2124 RefPtr<const AccAttributes> attrs = point.GetTextAttributes(); 2125 if (((!lastAttrs || !attrs) && attrs != lastAttrs) || 2126 (attrs && !attrs->Equal(lastAttrs))) { 2127 // The attributes change here. If we're moving forward, we want to return 2128 // this point. 2129 if (aDirection == eDirNext) { 2130 return point; 2131 } 2132 2133 // Otherwise, we're moving backward and we've now moved before the start 2134 // point of the current text attributes run. 2135 const auto attrsStart = TextLeafPoint(lastPoint.mAcc, 0); 2136 2137 // Return the current text attributes run start point if: 2138 // 1. The caller wants this function to include the origin in the 2139 // search (aIncludeOrigin implies that we must return the first text 2140 // attributes run start point that we find, even if that point is the 2141 // origin) 2142 // 2. Our search did not begin on the text attributes run start point 2143 if (aIncludeOrigin || attrsStart != *this) { 2144 return attrsStart; 2145 } 2146 2147 // Otherwise, the origin was the attributes run start point and the caller 2148 // wants this function to ignore it in its search. Keep searching. 2149 } 2150 lastPoint = point; 2151 if (aDirection == eDirPrevious) { 2152 // On the next iteration, we want to search for offset attributes from the 2153 // end of this Accessible. 2154 lastPoint.mOffset = 2155 static_cast<int32_t>(nsAccUtils::TextLength(point.mAcc)); 2156 } 2157 lastAttrs = attrs; 2158 } 2159 2160 // We couldn't move any further in this container. 2161 if (aDirection == eDirPrevious) { 2162 // Treat the start of a container as a format boundary. 2163 return TextLeafPoint(lastPoint.mAcc, 0); 2164 } 2165 // If we're at the end of the container then we have to use the start of the 2166 // next leaf. 2167 Accessible* nextLeaf = NextLeaf(lastPoint.mAcc); 2168 if (nextLeaf) { 2169 return TextLeafPoint(nextLeaf, 0); 2170 } 2171 // If there's no next leaf, then fall back to the end of the last point. 2172 return TextLeafPoint( 2173 lastPoint.mAcc, 2174 static_cast<int32_t>(nsAccUtils::TextLength(lastPoint.mAcc))); 2175 } 2176 2177 LayoutDeviceIntRect TextLeafPoint::CharBounds() const { 2178 if (!mAcc) { 2179 return LayoutDeviceIntRect(); 2180 } 2181 2182 if (mAcc->IsHTMLBr()) { 2183 // HTML <br> elements don't provide character bounds, but do provide text (a 2184 // line feed). They also have 0 width and/or height, depending on the 2185 // doctype and writing mode. Expose minimum 1 x 1 so clients treat it as an 2186 // actual rectangle; e.g. when the caret is positioned on a <br>. 2187 LayoutDeviceIntRect bounds = mAcc->Bounds(); 2188 if (bounds.width == 0) { 2189 bounds.width = 1; 2190 } 2191 if (bounds.height == 0) { 2192 bounds.height = 1; 2193 } 2194 return bounds; 2195 } 2196 2197 if (!mAcc->IsTextLeaf()) { 2198 // This could be an empty container. Alternatively, it could be a list 2199 // bullet,which does provide text but doesn't support character bounds. In 2200 // either case, return the Accessible's bounds. 2201 return mAcc->Bounds(); 2202 } 2203 2204 auto maybeAdjustLineFeedBounds = [this](LayoutDeviceIntRect& aBounds) { 2205 if (!IsLineFeedChar()) { 2206 return; 2207 } 2208 // Line feeds have a 0 width or height, depending on the writing mode. 2209 // Use 1 instead so that clients treat it as an actual rectangle; e.g. when 2210 // displaying the caret when it is positioned on a line feed. 2211 MOZ_ASSERT(aBounds.IsZeroArea()); 2212 if (aBounds.width == 0) { 2213 aBounds.width = 1; 2214 } 2215 if (aBounds.height == 0) { 2216 aBounds.height = 1; 2217 } 2218 }; 2219 2220 if (LocalAccessible* local = mAcc->AsLocal()) { 2221 if (mOffset >= 0 && 2222 static_cast<uint32_t>(mOffset) >= nsAccUtils::TextLength(local)) { 2223 // It's valid for a caller to query the length because the caret might be 2224 // at the end of editable text. In that case, we should just silently 2225 // return. However, we assert that the offset isn't greater than the 2226 // length. 2227 NS_ASSERTION( 2228 static_cast<uint32_t>(mOffset) <= nsAccUtils::TextLength(local), 2229 "Wrong in offset"); 2230 return LayoutDeviceIntRect(); 2231 } 2232 2233 LayoutDeviceIntRect bounds = ComputeBoundsFromFrame(); 2234 2235 // This document may have a resolution set, we will need to multiply 2236 // the document-relative coordinates by that value and re-apply the doc's 2237 // screen coordinates. 2238 nsPresContext* presContext = local->Document()->PresContext(); 2239 nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame(); 2240 LayoutDeviceIntRect orgRectPixels = 2241 LayoutDeviceIntRect::FromAppUnitsToNearest( 2242 rootFrame->GetScreenRectInAppUnits(), 2243 presContext->AppUnitsPerDevPixel()); 2244 bounds.MoveBy(-orgRectPixels.X(), -orgRectPixels.Y()); 2245 bounds.ScaleRoundOut(presContext->PresShell()->GetResolution()); 2246 bounds.MoveBy(orgRectPixels.X(), orgRectPixels.Y()); 2247 maybeAdjustLineFeedBounds(bounds); 2248 return bounds; 2249 } 2250 2251 if (RequestDomainsIfInactive(CacheDomain::TextBounds)) { 2252 return LayoutDeviceIntRect(); 2253 } 2254 RemoteAccessible* remote = mAcc->AsRemote(); 2255 if (!remote->mCachedFields) { 2256 return LayoutDeviceIntRect(); 2257 } 2258 2259 nsRect charBounds = remote->GetCachedCharRect(mOffset); 2260 // A character can have 0 width, but we still want its other coordinates. 2261 // Thus, we explicitly test for an all-0 rect here to determine whether this 2262 // is a valid char rect, rather than using IsZeroArea or IsEmpty. 2263 if (!charBounds.IsEqualRect(0, 0, 0, 0)) { 2264 LayoutDeviceIntRect bounds = remote->BoundsWithOffset(Some(charBounds)); 2265 maybeAdjustLineFeedBounds(bounds); 2266 return bounds; 2267 } 2268 2269 return LayoutDeviceIntRect(); 2270 } 2271 2272 bool TextLeafPoint::ContainsPoint(int32_t aX, int32_t aY) { 2273 if (mAcc && !mAcc->IsText()) { 2274 // If we're dealing with an empty embedded object, use the 2275 // accessible's non-text bounds. 2276 return mAcc->Bounds().Contains(aX, aY); 2277 } 2278 2279 return CharBounds().Contains(aX, aY); 2280 } 2281 2282 /*** TextLeafRange ***/ 2283 2284 bool TextLeafRange::Crop(Accessible* aContainer) { 2285 TextLeafPoint containerStart(aContainer, 0); 2286 TextLeafPoint containerEnd(aContainer, 2287 nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); 2288 2289 if (mEnd < containerStart || containerEnd < mStart) { 2290 // The range ends before the container, or starts after it. 2291 return false; 2292 } 2293 2294 if (mStart < containerStart) { 2295 // If range start is before container start, adjust range start to 2296 // start of container. 2297 mStart = containerStart; 2298 } 2299 2300 if (containerEnd < mEnd) { 2301 // If range end is after container end, adjust range end to end of 2302 // container. 2303 mEnd = containerEnd; 2304 } 2305 2306 return true; 2307 } 2308 2309 LayoutDeviceIntRect TextLeafRange::Bounds() const { 2310 // We can't simply query the first and last character, and union their bounds. 2311 // They might reside on different lines, so a simple union may yield an 2312 // incorrect width. We should use the length of the longest spanned line for 2313 // our width. To achieve this, walk all the lines and union them into the 2314 // result rectangle. 2315 LayoutDeviceIntRect result; 2316 const bool succeeded = WalkLineRects( 2317 [&result](TextLeafRange aLine, LayoutDeviceIntRect aLineRect) { 2318 result.UnionRect(result, aLineRect); 2319 }); 2320 2321 if (!succeeded) { 2322 return {}; 2323 } 2324 return result; 2325 } 2326 2327 nsTArray<LayoutDeviceIntRect> TextLeafRange::LineRects() const { 2328 // Get the bounds of the content so we can restrict our lines to just the 2329 // text visible within the bounds of the document. 2330 Maybe<LayoutDeviceIntRect> contentBounds; 2331 if (Accessible* doc = nsAccUtils::DocumentFor(mStart.mAcc)) { 2332 contentBounds.emplace(doc->Bounds()); 2333 } 2334 2335 nsTArray<LayoutDeviceIntRect> lineRects; 2336 WalkLineRects([&lineRects, &contentBounds](TextLeafRange aLine, 2337 LayoutDeviceIntRect aLineRect) { 2338 // Clip the bounds to the bounds of the content area. 2339 bool boundsVisible = true; 2340 if (contentBounds.isSome()) { 2341 boundsVisible = aLineRect.IntersectRect(aLineRect, *contentBounds); 2342 } 2343 if (boundsVisible) { 2344 lineRects.AppendElement(aLineRect); 2345 } 2346 }); 2347 2348 return lineRects; 2349 } 2350 2351 TextLeafPoint TextLeafRange::TextLeafPointAtScreenPoint(int32_t aX, 2352 int32_t aY) const { 2353 // Step backwards one character to make the endPoint inclusive. This means we 2354 // can use operator!= when comparing against endPoint below (which is very 2355 // fast), rather than operator< (which might be significantly slower). 2356 const TextLeafPoint endPoint = 2357 mEnd.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); 2358 2359 // If there are no characters in this container, we might have moved endPoint 2360 // before mStart. In that case, we shouldn't try to move farther forward, as 2361 // that might result in an infinite loop. 2362 TextLeafPoint point = mStart; 2363 if (mStart <= endPoint) { 2364 for (; !point.ContainsPoint(aX, aY) && point != endPoint; 2365 point = 2366 point.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext)) { 2367 } 2368 } 2369 2370 return point; 2371 } 2372 2373 bool TextLeafRange::SetSelection(int32_t aSelectionNum, bool aSetFocus) const { 2374 if (!mStart || !mEnd || mStart.mAcc->IsLocal() != mEnd.mAcc->IsLocal()) { 2375 return false; 2376 } 2377 2378 if (mStart.mAcc->IsRemote()) { 2379 DocAccessibleParent* doc = mStart.mAcc->AsRemote()->Document(); 2380 if (doc != mEnd.mAcc->AsRemote()->Document()) { 2381 return false; 2382 } 2383 2384 (void)doc->SendSetTextSelection(mStart.mAcc->ID(), mStart.mOffset, 2385 mEnd.mAcc->ID(), mEnd.mOffset, 2386 aSelectionNum, aSetFocus); 2387 return true; 2388 } 2389 2390 bool reversed = mEnd < mStart; 2391 auto [startContent, startContentOffset] = 2392 !reversed ? mStart.ToDOMPoint(false) : mEnd.ToDOMPoint(false); 2393 auto [endContent, endContentOffset] = 2394 !reversed ? mEnd.ToDOMPoint(false) : mStart.ToDOMPoint(false); 2395 if (!startContent || !endContent) { 2396 return false; 2397 } 2398 2399 RefPtr<dom::Selection> domSel = GetDOMSelection(startContent, endContent); 2400 if (!domSel) { 2401 return false; 2402 } 2403 2404 HyperTextAccessible* hyp = nullptr; 2405 if (mStart.mAcc->IsHyperText()) { 2406 hyp = mStart.mAcc->AsLocal()->AsHyperText(); 2407 } else { 2408 Accessible* parent = mStart.mAcc->Parent(); 2409 if (parent) { 2410 hyp = parent->AsLocal()->AsHyperText(); 2411 // Note that hyp will still be null here if the parent is not a HyperText. 2412 // That's okay. 2413 } 2414 } 2415 2416 // Before setting the selection range, we need to ensure that the editor 2417 // is initialized. (See bug 804927.) 2418 // Otherwise, it's possible that lazy editor initialization will override 2419 // the selection we set here and leave the caret at the end of the text. 2420 // By calling GetEditor here, we ensure that editor initialization is 2421 // completed before we set the selection. 2422 RefPtr<EditorBase> editor; 2423 if (hyp) { 2424 editor = hyp->GetEditor(); 2425 } 2426 2427 // XXX isFocusable will be false if mStart is not a direct child of the 2428 // contentEditable. However, contentEditables generally don't mess with 2429 // selection when they are focused. This has also been our behavior for a very 2430 // long time. 2431 const bool isFocusable = hyp && hyp->InteractiveState() & states::FOCUSABLE; 2432 // If the Accessible is focusable, focus it before setting the selection to 2433 // override the control's own selection changes on focus if any; e.g. inputs 2434 // that do select all on focus. This also ensures that the user can interact 2435 // with wherever they've moved the caret. See bug 524115. 2436 if (aSetFocus && isFocusable) { 2437 hyp->TakeFocus(); 2438 } 2439 2440 uint32_t rangeCount = 0; 2441 if (aSelectionNum == kRemoveAllExistingSelectedRanges) { 2442 domSel->RemoveAllRanges(IgnoreErrors()); 2443 } else { 2444 rangeCount = domSel->RangeCount(); 2445 } 2446 RefPtr<nsRange> domRange = nullptr; 2447 const bool newRange = 2448 aSelectionNum == static_cast<int32_t>(rangeCount) || aSelectionNum < 0; 2449 if (newRange) { 2450 domRange = nsRange::Create(startContent); 2451 } else { 2452 domRange = domSel->GetRangeAt(AssertedCast<uint32_t>(aSelectionNum)); 2453 } 2454 if (!domRange) { 2455 return false; 2456 } 2457 2458 domRange->SetStart(startContent, startContentOffset); 2459 domRange->SetEnd(endContent, endContentOffset); 2460 2461 // If this is not a new range, notify selection listeners that the existing 2462 // selection range has changed. Otherwise, just add the new range. 2463 if (!newRange) { 2464 domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*domRange, 2465 IgnoreErrors()); 2466 } 2467 2468 IgnoredErrorResult err; 2469 domSel->AddRangeAndSelectFramesAndNotifyListeners(*domRange, err); 2470 if (err.Failed()) { 2471 return false; 2472 } 2473 2474 // Changing the direction of the selection assures that the caret 2475 // will be at the logical end of the selection. 2476 domSel->SetDirection(reversed ? eDirPrevious : eDirNext); 2477 2478 // Make sure the selection is visible. See bug 1170242. 2479 domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION, 2480 ScrollAxis(), ScrollAxis(), 2481 ScrollFlags::ScrollOverflowHidden); 2482 2483 if (aSetFocus && mStart == mEnd && !isFocusable) { 2484 // We're moving the caret. Notify nsFocusManager so that the focus position 2485 // is correct. See bug 546068. 2486 if (nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager()) { 2487 MOZ_ASSERT(mStart.mAcc->AsLocal()->Document()); 2488 dom::Document* domDoc = 2489 mStart.mAcc->AsLocal()->Document()->DocumentNode(); 2490 MOZ_ASSERT(domDoc); 2491 nsCOMPtr<nsPIDOMWindowOuter> window = domDoc->GetWindow(); 2492 RefPtr<dom::Element> result; 2493 DOMFocusManager->MoveFocus( 2494 window, nullptr, nsIFocusManager::MOVEFOCUS_CARET, 2495 nsIFocusManager::FLAG_BYMOVEFOCUS, getter_AddRefs(result)); 2496 } 2497 } 2498 return true; 2499 } 2500 2501 /* static */ 2502 void TextLeafRange::GetSelection(Accessible* aAcc, 2503 nsTArray<TextLeafRange>& aRanges) { 2504 // Use HyperTextAccessibleBase::SelectionRanges. Eventually, we'll want to 2505 // move that code into TextLeafPoint, but events and caching are based on 2506 // HyperText offsets for now. 2507 HyperTextAccessibleBase* hyp = aAcc->AsHyperTextBase(); 2508 if (!hyp) { 2509 return; 2510 } 2511 AutoTArray<TextRange, 1> hypRanges; 2512 hyp->CroppedSelectionRanges(hypRanges); 2513 aRanges.SetCapacity(hypRanges.Length()); 2514 for (TextRange& hypRange : hypRanges) { 2515 TextLeafPoint start = 2516 hypRange.StartContainer()->AsHyperTextBase()->ToTextLeafPoint( 2517 hypRange.StartOffset()); 2518 TextLeafPoint end = 2519 hypRange.EndContainer()->AsHyperTextBase()->ToTextLeafPoint( 2520 hypRange.EndOffset()); 2521 aRanges.EmplaceBack(start, end); 2522 } 2523 } 2524 2525 void TextLeafRange::ScrollIntoView(uint32_t aScrollType) const { 2526 if (!mStart || !mEnd || mStart.mAcc->IsLocal() != mEnd.mAcc->IsLocal()) { 2527 return; 2528 } 2529 2530 if (mStart.mAcc->IsRemote()) { 2531 DocAccessibleParent* doc = mStart.mAcc->AsRemote()->Document(); 2532 if (doc != mEnd.mAcc->AsRemote()->Document()) { 2533 // Can't scroll range that spans docs. 2534 return; 2535 } 2536 2537 (void)doc->SendScrollTextLeafRangeIntoView(mStart.mAcc->ID(), 2538 mStart.mOffset, mEnd.mAcc->ID(), 2539 mEnd.mOffset, aScrollType); 2540 return; 2541 } 2542 2543 auto [startContent, startContentOffset] = mStart.ToDOMPoint(); 2544 auto [endContent, endContentOffset] = mEnd.ToDOMPoint(); 2545 2546 if (!startContent || !endContent) { 2547 return; 2548 } 2549 2550 ErrorResult er; 2551 RefPtr<nsRange> domRange = nsRange::Create(startContent, startContentOffset, 2552 endContent, endContentOffset, er); 2553 if (er.Failed()) { 2554 return; 2555 } 2556 2557 nsCoreUtils::ScrollSubstringTo(mStart.mAcc->AsLocal()->GetFrame(), domRange, 2558 aScrollType); 2559 } 2560 2561 nsTArray<TextLeafRange> TextLeafRange::VisibleLines( 2562 Accessible* aContainer) const { 2563 MOZ_ASSERT(aContainer); 2564 nsTArray<TextLeafRange> lines; 2565 if (mStart == mEnd) { 2566 return lines; 2567 } 2568 // We want to restrict our lines to those visible within aContainer. 2569 LayoutDeviceIntRect containerBounds = aContainer->Bounds(); 2570 WalkLineRects([&lines, &containerBounds](TextLeafRange aLine, 2571 LayoutDeviceIntRect aLineRect) { 2572 // XXX This doesn't correctly handle lines that are scrolled out where the 2573 // scroll container is a descendant of aContainer. Such lines might 2574 // intersect with containerBounds, but the scroll container could be a 2575 // descendant of aContainer and should thus exclude this line. See bug 2576 // 1945010 for more details. 2577 if (aLineRect.Intersects(containerBounds)) { 2578 lines.AppendElement(aLine); 2579 } 2580 }); 2581 return lines; 2582 } 2583 2584 bool TextLeafRange::WalkLineRects(LineRectCallback aCallback) const { 2585 if (mEnd < mStart) { 2586 return false; 2587 } 2588 if (mStart == mEnd) { 2589 // Return the insertion point bounds for the offset if range is collapsed. 2590 aCallback(*this, mStart.InsertionPointBounds()); 2591 return true; 2592 } 2593 2594 bool locatedFinalLine = false; 2595 TextLeafPoint currPoint = mStart; 2596 2597 // Union the first and last chars of each line to create a line rect. 2598 while (!locatedFinalLine) { 2599 TextLeafPoint nextLineStartPoint = currPoint.FindBoundary( 2600 nsIAccessibleText::BOUNDARY_LINE_START, eDirNext); 2601 // If currPoint is at the end of the document, nextLineStartPoint will be 2602 // equal to currPoint. However, this can only happen if mEnd is also the end 2603 // of the document. 2604 MOZ_ASSERT(nextLineStartPoint != currPoint || nextLineStartPoint == mEnd); 2605 if (mEnd <= nextLineStartPoint) { 2606 // nextLineStart is past the end of the range. Constrain this last line to 2607 // the end of the range. 2608 nextLineStartPoint = mEnd; 2609 locatedFinalLine = true; 2610 } 2611 // Fetch the last point in the current line by going back one char from the 2612 // start of the next line. 2613 TextLeafPoint lastPointInLine = nextLineStartPoint.FindBoundary( 2614 nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); 2615 MOZ_ASSERT(currPoint <= lastPointInLine); 2616 2617 LayoutDeviceIntRect currLineRect = currPoint.CharBounds(); 2618 currLineRect.UnionRect(currLineRect, lastPointInLine.CharBounds()); 2619 // The range we pass must include the last character and range ends are 2620 // exclusive, hence the use of nextLineStartPoint. 2621 TextLeafRange currLine = TextLeafRange(currPoint, nextLineStartPoint); 2622 aCallback(currLine, currLineRect); 2623 2624 currPoint = nextLineStartPoint; 2625 } 2626 return true; 2627 } 2628 2629 TextLeafRange::Iterator TextLeafRange::Iterator::BeginIterator( 2630 const TextLeafRange& aRange) { 2631 Iterator result(aRange); 2632 2633 result.mSegmentStart = aRange.mStart; 2634 if (aRange.mStart.mAcc == aRange.mEnd.mAcc) { 2635 result.mSegmentEnd = aRange.mEnd; 2636 } else { 2637 result.mSegmentEnd = TextLeafPoint( 2638 aRange.mStart.mAcc, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); 2639 } 2640 2641 return result; 2642 } 2643 2644 TextLeafRange::Iterator TextLeafRange::Iterator::EndIterator( 2645 const TextLeafRange& aRange) { 2646 Iterator result(aRange); 2647 2648 result.mSegmentEnd = TextLeafPoint(); 2649 result.mSegmentStart = TextLeafPoint(); 2650 2651 return result; 2652 } 2653 2654 TextLeafRange::Iterator& TextLeafRange::Iterator::operator++() { 2655 if (mSegmentEnd.mAcc == mRange.mEnd.mAcc) { 2656 mSegmentEnd = TextLeafPoint(); 2657 mSegmentStart = TextLeafPoint(); 2658 return *this; 2659 } 2660 2661 if (Accessible* nextLeaf = NextLeaf(mSegmentEnd.mAcc)) { 2662 mSegmentStart = TextLeafPoint(nextLeaf, 0); 2663 if (nextLeaf == mRange.mEnd.mAcc) { 2664 mSegmentEnd = TextLeafPoint(nextLeaf, mRange.mEnd.mOffset); 2665 } else { 2666 mSegmentEnd = 2667 TextLeafPoint(nextLeaf, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); 2668 } 2669 } else { 2670 mSegmentEnd = TextLeafPoint(); 2671 mSegmentStart = TextLeafPoint(); 2672 } 2673 2674 return *this; 2675 } 2676 2677 } // namespace mozilla::a11y