HyperTextAccessibleBase.cpp (31712B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #include "HyperTextAccessibleBase.h" 7 8 #include "mozilla/a11y/Accessible.h" 9 #include "nsAccUtils.h" 10 #include "TextLeafRange.h" 11 #include "TextRange.h" 12 13 namespace mozilla::a11y { 14 15 int32_t HyperTextAccessibleBase::GetChildIndexAtOffset(uint32_t aOffset) const { 16 auto& offsets = 17 const_cast<HyperTextAccessibleBase*>(this)->GetCachedHyperTextOffsets(); 18 int32_t lastOffset = 0; 19 const uint32_t offsetCount = offsets.Length(); 20 21 if (offsetCount > 0) { 22 lastOffset = offsets[offsetCount - 1]; 23 if (static_cast<int32_t>(aOffset) < lastOffset) { 24 // We've cached up to aOffset. 25 size_t index; 26 if (BinarySearch(offsets, 0, offsetCount, static_cast<int32_t>(aOffset), 27 &index)) { 28 // aOffset is the exclusive end of a child, so return the child before 29 // it. 30 return static_cast<int32_t>((index < offsetCount - 1) ? index + 1 31 : index); 32 } 33 if (index == offsetCount) { 34 // aOffset is past the end of the text. 35 return -1; 36 } 37 // index points at the exclusive end after aOffset. 38 return static_cast<int32_t>(index); 39 } 40 } 41 42 // We haven't yet cached up to aOffset. Find it, caching as we go. 43 const Accessible* thisAcc = Acc(); 44 uint32_t childCount = thisAcc->ChildCount(); 45 // Even though we're only caching up to aOffset, it's likely that we'll 46 // eventually cache offsets for all children. Pre-allocate thus to minimize 47 // re-allocations. 48 offsets.SetCapacity(childCount); 49 while (offsets.Length() < childCount) { 50 Accessible* child = thisAcc->ChildAt(offsets.Length()); 51 lastOffset += static_cast<int32_t>(nsAccUtils::TextLength(child)); 52 offsets.AppendElement(lastOffset); 53 if (static_cast<int32_t>(aOffset) < lastOffset) { 54 return static_cast<int32_t>(offsets.Length() - 1); 55 } 56 } 57 58 if (static_cast<int32_t>(aOffset) == lastOffset) { 59 return static_cast<int32_t>(offsets.Length() - 1); 60 } 61 62 return -1; 63 } 64 65 Accessible* HyperTextAccessibleBase::GetChildAtOffset(uint32_t aOffset) const { 66 const Accessible* thisAcc = Acc(); 67 return thisAcc->ChildAt(GetChildIndexAtOffset(aOffset)); 68 } 69 70 int32_t HyperTextAccessibleBase::GetChildOffset(const Accessible* aChild, 71 bool aInvalidateAfter) const { 72 const Accessible* thisAcc = Acc(); 73 if (aChild->Parent() != thisAcc) { 74 return -1; 75 } 76 int32_t index = aChild->IndexInParent(); 77 if (index == -1) { 78 return -1; 79 } 80 return GetChildOffset(index, aInvalidateAfter); 81 } 82 83 int32_t HyperTextAccessibleBase::GetChildOffset(uint32_t aChildIndex, 84 bool aInvalidateAfter) const { 85 auto& offsets = 86 const_cast<HyperTextAccessibleBase*>(this)->GetCachedHyperTextOffsets(); 87 if (aChildIndex == 0) { 88 if (aInvalidateAfter) { 89 offsets.Clear(); 90 } 91 return 0; 92 } 93 94 int32_t countCachedAfterChild = static_cast<int32_t>(offsets.Length()) - 95 static_cast<int32_t>(aChildIndex); 96 if (countCachedAfterChild > 0) { 97 // We've cached up to aChildIndex. 98 if (aInvalidateAfter) { 99 offsets.RemoveElementsAt(aChildIndex, countCachedAfterChild); 100 } 101 return offsets[aChildIndex - 1]; 102 } 103 104 // We haven't yet cached up to aChildIndex. Find it, caching as we go. 105 const Accessible* thisAcc = Acc(); 106 // Even though we're only caching up to aChildIndex, it's likely that we'll 107 // eventually cache offsets for all children. Pre-allocate thus to minimize 108 // re-allocations. 109 offsets.SetCapacity(thisAcc->ChildCount()); 110 uint32_t lastOffset = offsets.IsEmpty() ? 0 : offsets[offsets.Length() - 1]; 111 while (offsets.Length() < aChildIndex) { 112 Accessible* child = thisAcc->ChildAt(offsets.Length()); 113 lastOffset += nsAccUtils::TextLength(child); 114 offsets.AppendElement(lastOffset); 115 } 116 117 return offsets[aChildIndex - 1]; 118 } 119 120 uint32_t HyperTextAccessibleBase::CharacterCount() const { 121 return GetChildOffset(Acc()->ChildCount()); 122 } 123 124 index_t HyperTextAccessibleBase::ConvertMagicOffset(int32_t aOffset) const { 125 if (aOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT) { 126 return CharacterCount(); 127 } 128 129 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { 130 return CaretOffset(); 131 } 132 133 return aOffset; 134 } 135 136 void HyperTextAccessibleBase::TextSubstring(int32_t aStartOffset, 137 int32_t aEndOffset, 138 nsAString& aText) const { 139 aText.Truncate(); 140 141 index_t startOffset = ConvertMagicOffset(aStartOffset); 142 index_t endOffset = ConvertMagicOffset(aEndOffset); 143 if (!startOffset.IsValid() || !endOffset.IsValid() || 144 startOffset > endOffset || endOffset > CharacterCount()) { 145 NS_ERROR("Wrong in offset"); 146 return; 147 } 148 149 int32_t startChildIdx = GetChildIndexAtOffset(startOffset); 150 if (startChildIdx == -1) { 151 return; 152 } 153 154 int32_t endChildIdx = GetChildIndexAtOffset(endOffset); 155 if (endChildIdx == -1) { 156 return; 157 } 158 159 const Accessible* thisAcc = Acc(); 160 if (startChildIdx == endChildIdx) { 161 int32_t childOffset = GetChildOffset(startChildIdx); 162 if (childOffset == -1) { 163 return; 164 } 165 166 Accessible* child = thisAcc->ChildAt(startChildIdx); 167 child->AppendTextTo(aText, startOffset - childOffset, 168 endOffset - startOffset); 169 return; 170 } 171 172 int32_t startChildOffset = GetChildOffset(startChildIdx); 173 if (startChildOffset == -1) { 174 return; 175 } 176 177 Accessible* startChild = thisAcc->ChildAt(startChildIdx); 178 startChild->AppendTextTo(aText, startOffset - startChildOffset); 179 180 for (int32_t childIdx = startChildIdx + 1; childIdx < endChildIdx; 181 childIdx++) { 182 Accessible* child = thisAcc->ChildAt(childIdx); 183 child->AppendTextTo(aText); 184 } 185 186 int32_t endChildOffset = GetChildOffset(endChildIdx); 187 if (endChildOffset == -1) { 188 return; 189 } 190 191 Accessible* endChild = thisAcc->ChildAt(endChildIdx); 192 endChild->AppendTextTo(aText, 0, endOffset - endChildOffset); 193 } 194 195 bool HyperTextAccessibleBase::CharAt(int32_t aOffset, nsAString& aChar, 196 int32_t* aStartOffset, 197 int32_t* aEndOffset) { 198 MOZ_ASSERT(!aStartOffset == !aEndOffset, 199 "Offsets should be both defined or both undefined!"); 200 201 int32_t childIdx = GetChildIndexAtOffset(aOffset); 202 if (childIdx == -1) { 203 return false; 204 } 205 206 Accessible* child = Acc()->ChildAt(childIdx); 207 child->AppendTextTo(aChar, aOffset - GetChildOffset(childIdx), 1); 208 209 if (aStartOffset && aEndOffset) { 210 *aStartOffset = aOffset; 211 *aEndOffset = aOffset + aChar.Length(); 212 } 213 return true; 214 } 215 216 LayoutDeviceIntRect HyperTextAccessibleBase::CharBounds(int32_t aOffset, 217 uint32_t aCoordType) { 218 index_t offset = ConvertMagicOffset(aOffset); 219 if (!offset.IsValid() || offset > CharacterCount()) { 220 return LayoutDeviceIntRect(); 221 } 222 TextLeafPoint point = ToTextLeafPoint(static_cast<int32_t>(offset), false); 223 if (!point.mAcc) { 224 return LayoutDeviceIntRect(); 225 } 226 227 LayoutDeviceIntRect bounds = point.CharBounds(); 228 if (!bounds.x && !bounds.y && bounds.IsZeroArea()) { 229 return bounds; 230 } 231 nsAccUtils::ConvertScreenCoordsTo(&bounds.x, &bounds.y, aCoordType, Acc()); 232 return bounds; 233 } 234 235 LayoutDeviceIntRect HyperTextAccessibleBase::TextBounds(int32_t aStartOffset, 236 int32_t aEndOffset, 237 uint32_t aCoordType) { 238 LayoutDeviceIntRect result; 239 if (CharacterCount() == 0) { 240 result = Acc()->Bounds(); 241 nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc()); 242 return result; 243 } 244 245 index_t startOffset = ConvertMagicOffset(aStartOffset); 246 index_t endOffset = ConvertMagicOffset(aEndOffset); 247 if (!startOffset.IsValid() || startOffset > endOffset) { 248 return LayoutDeviceIntRect(); 249 } 250 251 TextLeafPoint startPoint = 252 ToTextLeafPoint(static_cast<int32_t>(startOffset), false); 253 TextLeafPoint endPoint = 254 ToTextLeafPoint(static_cast<int32_t>(endOffset), true); 255 if (!endPoint) { 256 // The caller provided an invalid offset. 257 return LayoutDeviceIntRect(); 258 } 259 260 TextLeafRange range(startPoint, endPoint); 261 result = range.Bounds(); 262 263 // Calls to TextLeafRange::Bounds() will construct screen coordinates. 264 // Perform any additional conversions here. 265 nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc()); 266 return result; 267 } 268 269 int32_t HyperTextAccessibleBase::OffsetAtPoint(int32_t aX, int32_t aY, 270 uint32_t aCoordType) { 271 Accessible* thisAcc = Acc(); 272 LayoutDeviceIntPoint coords = 273 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc); 274 if (!thisAcc->Bounds().Contains(coords.x, coords.y)) { 275 // The requested point does not exist in this accessible. 276 // Check if we used fuzzy hittesting to get here and, if 277 // so, return 0 to indicate this text leaf is a valid match. 278 LayoutDeviceIntPoint p(aX, aY); 279 if (aCoordType != nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE) { 280 p = nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc); 281 } 282 if (Accessible* doc = nsAccUtils::DocumentFor(thisAcc)) { 283 Accessible* hittestMatch = doc->ChildAtPoint( 284 p.x, p.y, Accessible::EWhichChildAtPoint::DeepestChild); 285 if (hittestMatch && thisAcc == hittestMatch->Parent()) { 286 return 0; 287 } 288 } 289 return -1; 290 } 291 292 TextLeafPoint startPoint = ToTextLeafPoint(0, false); 293 // Walk to the very end of the text contained in this hypertext in order to 294 // hit test it in its entirety. 295 TextLeafPoint endPoint = 296 ToTextLeafPoint(static_cast<int32_t>(CharacterCount()), true); 297 TextLeafRange range{startPoint, endPoint}; 298 TextLeafPoint point = range.TextLeafPointAtScreenPoint(coords.x, coords.y); 299 if (!point.ContainsPoint(coords.x, coords.y)) { 300 LayoutDeviceIntRect startRect = startPoint.CharBounds(); 301 if (coords.x < startRect.x || coords.y < startRect.y) { 302 // Bug 1816601: The point is within the container but above or to the left 303 // of the rectangle at offset 0. We should really return -1, but we've 304 // returned 0 for many years due to a bug. Some users have unfortunately 305 // come to rely on this, so perpetuate this here. 306 return 0; 307 } 308 return -1; 309 } 310 DebugOnly<bool> ok = false; 311 int32_t htOffset; 312 std::tie(ok, htOffset) = 313 TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false); 314 MOZ_ASSERT(ok, "point should be a descendant of this"); 315 return htOffset; 316 } 317 318 TextLeafPoint HyperTextAccessibleBase::ToTextLeafPoint(int32_t aOffset, 319 bool aDescendToEnd) { 320 Accessible* thisAcc = Acc(); 321 if (!thisAcc->HasChildren()) { 322 return TextLeafPoint(thisAcc, 0); 323 } 324 Accessible* child = GetChildAtOffset(aOffset); 325 if (!child) { 326 return TextLeafPoint(); 327 } 328 int32_t offset = aOffset - GetChildOffset(child); 329 if (HyperTextAccessibleBase* childHt = child->AsHyperTextBase()) { 330 // This child is an embedded object, so the offset can only be 0 or 1. 331 MOZ_ASSERT(offset == 0 || offset == 1); 332 // Offset 1 refers to the end of this container, so descend to its end. 333 const bool end = aDescendToEnd || offset == 1; 334 return childHt->ToTextLeafPoint( 335 end ? static_cast<int32_t>(childHt->CharacterCount()) : 0, end); 336 } 337 return TextLeafPoint(child, offset); 338 } 339 340 std::pair<bool, int32_t> HyperTextAccessibleBase::TransformOffset( 341 Accessible* aDescendant, int32_t aOffset, bool aIsEndOffset) const { 342 const Accessible* thisAcc = Acc(); 343 // From the descendant, go up and get the immediate child of this hypertext. 344 int32_t offset = aOffset; 345 Accessible* descendant = aDescendant; 346 while (descendant) { 347 Accessible* parent = descendant->Parent(); 348 if (parent == thisAcc) { 349 return {true, GetChildOffset(descendant) + offset}; 350 } 351 352 // This offset no longer applies because the passed-in text object is not 353 // a child of the hypertext. This happens when there are nested hypertexts, 354 // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset 355 // to make it relative the hypertext. 356 // If the end offset is not supposed to be inclusive and the original point 357 // is not at 0 offset then the returned offset should be after an embedded 358 // character the original point belongs to. 359 if (aIsEndOffset) { 360 offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0; 361 } else { 362 offset = 0; 363 } 364 365 descendant = parent; 366 } 367 368 // The given a11y point cannot be mapped to an offset relative to this 369 // hypertext accessible. Return the start or the end depending on whether this 370 // is a start ofset or an end offset, thus clipping to the relevant endpoint. 371 return {false, aIsEndOffset ? static_cast<int32_t>(CharacterCount()) : 0}; 372 } 373 374 void HyperTextAccessibleBase::AdjustOriginIfEndBoundary( 375 TextLeafPoint& aOrigin, AccessibleTextBoundary aBoundaryType, 376 bool aAtOffset) const { 377 if (aBoundaryType != nsIAccessibleText::BOUNDARY_LINE_END && 378 aBoundaryType != nsIAccessibleText::BOUNDARY_WORD_END) { 379 return; 380 } 381 TextLeafPoint actualOrig = aOrigin; 382 // We explicitly care about the character at this offset. We don't want 383 // FindBoundary to behave differently even if this is the insertion point at 384 // the end of a line. 385 actualOrig.mIsEndOfLineInsertionPoint = false; 386 if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) { 387 if (!actualOrig.IsLineFeedChar()) { 388 return; 389 } 390 aOrigin = 391 actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); 392 } else { // BOUNDARY_WORD_END 393 if (aAtOffset) { 394 // For TextAtOffset with BOUNDARY_WORD_END, we follow WebKitGtk here and 395 // return the word which ends after the origin if the origin is a word end 396 // boundary. Also, if the caret is at the end of a line, our tests expect 397 // the word after the caret, not the word before. The reason for that 398 // is a mystery lost to history. We can do that by explicitly using the 399 // caret without adjusting for end of line. 400 aOrigin = actualOrig; 401 return; 402 } 403 if (!actualOrig.IsSpace()) { 404 return; 405 } 406 TextLeafPoint prevChar = 407 actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); 408 if (prevChar != actualOrig && !prevChar.IsSpace()) { 409 // aOrigin is a word end boundary. 410 aOrigin = prevChar; 411 } 412 } 413 } 414 415 void HyperTextAccessibleBase::TextBeforeOffset( 416 int32_t aOffset, AccessibleTextBoundary aBoundaryType, 417 int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) { 418 *aStartOffset = *aEndOffset = 0; 419 aText.Truncate(); 420 421 if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START || 422 aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) { 423 return; // Not implemented. 424 } 425 426 uint32_t adjustedOffset = ConvertMagicOffset(aOffset); 427 if (adjustedOffset == std::numeric_limits<uint32_t>::max()) { 428 NS_ERROR("Wrong given offset!"); 429 return; 430 } 431 432 if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) { 433 if (adjustedOffset > 0) { 434 CharAt(static_cast<int32_t>(adjustedOffset) - 1, aText, aStartOffset, 435 aEndOffset); 436 } 437 return; 438 } 439 440 TextLeafPoint orig; 441 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { 442 orig = TextLeafPoint::GetCaret(Acc()); 443 } else { 444 orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset)); 445 } 446 if (!orig) { 447 // This can happen if aOffset is invalid. 448 return; 449 } 450 AdjustOriginIfEndBoundary(orig, aBoundaryType); 451 TextLeafPoint end = 452 orig.FindBoundary(aBoundaryType, eDirPrevious, 453 TextLeafPoint::BoundaryFlags::eIncludeOrigin); 454 bool ok; 455 std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, 456 /* aIsEndOffset */ true); 457 if (!ok) { 458 // There is no previous boundary inside this HyperText. 459 *aStartOffset = *aEndOffset = 0; 460 return; 461 } 462 TextLeafPoint start = end.FindBoundary(aBoundaryType, eDirPrevious); 463 // If TransformOffset fails because start is outside this HyperText, 464 // *aStartOffset will be 0, which is what we want. 465 std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, 466 /* aIsEndOffset */ false); 467 TextSubstring(*aStartOffset, *aEndOffset, aText); 468 } 469 470 void HyperTextAccessibleBase::TextAtOffset(int32_t aOffset, 471 AccessibleTextBoundary aBoundaryType, 472 int32_t* aStartOffset, 473 int32_t* aEndOffset, 474 nsAString& aText) { 475 *aStartOffset = *aEndOffset = 0; 476 aText.Truncate(); 477 478 if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START || 479 aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) { 480 return; // Not implemented. 481 } 482 483 uint32_t adjustedOffset = ConvertMagicOffset(aOffset); 484 if (adjustedOffset == std::numeric_limits<uint32_t>::max()) { 485 NS_ERROR("Wrong given offset!"); 486 return; 487 } 488 489 if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) { 490 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { 491 TextLeafPoint caret = TextLeafPoint::GetCaret(Acc()); 492 if (caret.mIsEndOfLineInsertionPoint) { 493 // The caret is at the end of the line. Return no character. 494 *aStartOffset = *aEndOffset = static_cast<int32_t>(adjustedOffset); 495 return; 496 } 497 } 498 CharAt(adjustedOffset, aText, aStartOffset, aEndOffset); 499 return; 500 } 501 502 TextLeafPoint start, end; 503 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { 504 start = TextLeafPoint::GetCaret(Acc()); 505 AdjustOriginIfEndBoundary(start, aBoundaryType, /* aAtOffset */ true); 506 end = start; 507 } else { 508 start = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset)); 509 Accessible* childAcc = GetChildAtOffset(adjustedOffset); 510 if (childAcc && childAcc->IsHyperText()) { 511 // We're searching for boundaries enclosing an embedded object. 512 // An embedded object might contain several boundaries itself. 513 // Thus, we must ensure we search for the end boundary from the last 514 // text in the subtree, not just the first. 515 // For example, if the embedded object is a link and it contains two 516 // words, but the second word expands beyond the link, we want to 517 // include the part of the second word which is outside of the link. 518 end = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset), 519 /* aDescendToEnd */ true); 520 } else { 521 AdjustOriginIfEndBoundary(start, aBoundaryType, 522 /* aAtOffset */ true); 523 end = start; 524 } 525 } 526 if (!start) { 527 // This can happen if aOffset is invalid. 528 return; 529 } 530 start = start.FindBoundary(aBoundaryType, eDirPrevious, 531 TextLeafPoint::BoundaryFlags::eIncludeOrigin); 532 bool ok; 533 std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, 534 /* aIsEndOffset */ false); 535 end = end.FindBoundary(aBoundaryType, eDirNext); 536 std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, 537 /* aIsEndOffset */ true); 538 TextSubstring(*aStartOffset, *aEndOffset, aText); 539 } 540 541 void HyperTextAccessibleBase::TextAfterOffset( 542 int32_t aOffset, AccessibleTextBoundary aBoundaryType, 543 int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) { 544 *aStartOffset = *aEndOffset = 0; 545 aText.Truncate(); 546 547 if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START || 548 aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) { 549 return; // Not implemented. 550 } 551 552 uint32_t adjustedOffset = ConvertMagicOffset(aOffset); 553 if (adjustedOffset == std::numeric_limits<uint32_t>::max()) { 554 NS_ERROR("Wrong given offset!"); 555 return; 556 } 557 558 if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) { 559 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET && adjustedOffset > 0 && 560 TextLeafPoint::GetCaret(Acc()).mIsEndOfLineInsertionPoint) { 561 --adjustedOffset; 562 } 563 uint32_t count = CharacterCount(); 564 if (adjustedOffset >= count) { 565 *aStartOffset = *aEndOffset = static_cast<int32_t>(count); 566 } else { 567 CharAt(static_cast<int32_t>(adjustedOffset) + 1, aText, aStartOffset, 568 aEndOffset); 569 } 570 return; 571 } 572 573 TextLeafPoint orig; 574 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { 575 orig = TextLeafPoint::GetCaret(Acc()); 576 } else { 577 orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset), 578 /* aDescendToEnd */ true); 579 } 580 if (!orig) { 581 // This can happen if aOffset is invalid. 582 return; 583 } 584 AdjustOriginIfEndBoundary(orig, aBoundaryType); 585 TextLeafPoint start = orig.FindBoundary(aBoundaryType, eDirNext); 586 bool ok; 587 std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, 588 /* aIsEndOffset */ false); 589 if (!ok) { 590 // There is no next boundary inside this HyperText. 591 *aStartOffset = *aEndOffset = static_cast<int32_t>(CharacterCount()); 592 return; 593 } 594 TextLeafPoint end = start.FindBoundary(aBoundaryType, eDirNext); 595 // If TransformOffset fails because end is outside this HyperText, 596 // *aEndOffset will be CharacterCount(), which is what we want. 597 std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, 598 /* aIsEndOffset */ true); 599 TextSubstring(*aStartOffset, *aEndOffset, aText); 600 } 601 602 int32_t HyperTextAccessibleBase::CaretOffset() const { 603 TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc())); 604 if (point.mOffset == 0 && point.mAcc == Acc()) { 605 // If a text box is empty, there will be no children, so point.mAcc will be 606 // this HyperText. 607 return 0; 608 } 609 auto [ok, htOffset] = 610 TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false); 611 if (!ok) { 612 // The caret is not within this HyperText. 613 return -1; 614 } 615 return htOffset; 616 } 617 618 void HyperTextAccessibleBase::SetCaretOffset(int32_t aOffset) { 619 TextLeafPoint point = ToTextLeafPoint(aOffset); 620 TextLeafRange range(point, point); 621 if (!range) { 622 NS_ERROR("Wrong in offset"); 623 return; 624 } 625 range.SetSelection(TextLeafRange::kRemoveAllExistingSelectedRanges); 626 } 627 628 int32_t HyperTextAccessibleBase::CaretLineNumber() { 629 TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc())); 630 if (point.mOffset == 0 && point.mAcc == Acc()) { 631 MOZ_ASSERT(CharacterCount() == 0); 632 // If a text box is empty, there will be no children, so point.mAcc will be 633 // this HyperText. 634 return 1; 635 } 636 637 if (!point.mAcc || 638 (point.mAcc != Acc() && !Acc()->IsAncestorOf(point.mAcc))) { 639 // The caret is not within this HyperText. 640 return -1; 641 } 642 643 // Walk forward by line from the start of the container. 644 TextLeafPoint line = TextLeafPoint(Acc(), 0); 645 int32_t lineNumber = 0; 646 for (; line && line < point; 647 line = line.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START, 648 eDirNext)) { 649 ++lineNumber; 650 } 651 // The caret might be right at the start of a line, in which case we should 652 // increment the line number. We shouldn't do that if the caret is at the end 653 // of a line or container, though. 654 if (line == point && !point.mIsEndOfLineInsertionPoint && 655 point.mOffset < 656 static_cast<int32_t>(nsAccUtils::TextLength(point.mAcc))) { 657 ++lineNumber; 658 } 659 660 return lineNumber; 661 } 662 663 bool HyperTextAccessibleBase::IsValidOffset(int32_t aOffset) { 664 index_t offset = ConvertMagicOffset(aOffset); 665 return offset.IsValid() && offset <= CharacterCount(); 666 } 667 668 bool HyperTextAccessibleBase::IsValidRange(int32_t aStartOffset, 669 int32_t aEndOffset) { 670 index_t startOffset = ConvertMagicOffset(aStartOffset); 671 index_t endOffset = ConvertMagicOffset(aEndOffset); 672 return startOffset.IsValid() && endOffset.IsValid() && 673 startOffset <= endOffset && endOffset <= CharacterCount(); 674 } 675 676 uint32_t HyperTextAccessibleBase::LinkCount() { 677 return Acc()->EmbeddedChildCount(); 678 } 679 680 Accessible* HyperTextAccessibleBase::LinkAt(uint32_t aIndex) { 681 return Acc()->EmbeddedChildAt(aIndex); 682 } 683 684 int32_t HyperTextAccessibleBase::LinkIndexOf(Accessible* aLink) { 685 return Acc()->IndexOfEmbeddedChild(aLink); 686 } 687 688 already_AddRefed<AccAttributes> HyperTextAccessibleBase::TextAttributes( 689 bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset, 690 int32_t* aEndOffset) { 691 *aStartOffset = *aEndOffset = 0; 692 index_t offset = ConvertMagicOffset(aOffset); 693 if (!offset.IsValid() || offset > CharacterCount()) { 694 NS_ERROR("Wrong in offset!"); 695 return RefPtr{new AccAttributes()}.forget(); 696 } 697 698 Accessible* originAcc = GetChildAtOffset(offset); 699 if (!originAcc) { 700 // Offset 0 is correct offset when accessible has empty text. Include 701 // default attributes if they were requested, otherwise return empty set. 702 if (offset == 0) { 703 if (aIncludeDefAttrs) { 704 return DefaultTextAttributes(); 705 } 706 } 707 return RefPtr{new AccAttributes()}.forget(); 708 } 709 710 if (!originAcc->IsText()) { 711 // This is an embedded object. One or more consecutive embedded objects 712 // form a single attrs run with no attributes. 713 *aStartOffset = aOffset; 714 *aEndOffset = aOffset + 1; 715 Accessible* parent = originAcc->Parent(); 716 if (!parent) { 717 return RefPtr{new AccAttributes()}.forget(); 718 } 719 int32_t originIdx = originAcc->IndexInParent(); 720 if (originIdx > 0) { 721 // Check for embedded objects before the origin. 722 for (uint32_t idx = originIdx - 1;; --idx) { 723 Accessible* sibling = parent->ChildAt(idx); 724 if (sibling->IsText()) { 725 break; 726 } 727 --*aStartOffset; 728 if (idx == 0) { 729 break; 730 } 731 } 732 } 733 // Check for embedded objects after the origin. 734 for (uint32_t idx = originIdx + 1;; ++idx) { 735 Accessible* sibling = parent->ChildAt(idx); 736 if (!sibling || sibling->IsText()) { 737 break; 738 } 739 ++*aEndOffset; 740 } 741 return RefPtr{new AccAttributes()}.forget(); 742 } 743 744 TextLeafPoint origin = ToTextLeafPoint(static_cast<int32_t>(offset)); 745 TextLeafPoint start = 746 origin.FindTextAttrsStart(eDirPrevious, /* aIncludeOrigin */ true); 747 bool ok; 748 std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, 749 /* aIsEndOffset */ false); 750 TextLeafPoint end = 751 origin.FindTextAttrsStart(eDirNext, /* aIncludeOrigin */ false); 752 std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, 753 /* aIsEndOffset */ true); 754 return origin.GetTextAttributes(aIncludeDefAttrs); 755 } 756 757 void HyperTextAccessibleBase::CroppedSelectionRanges( 758 nsTArray<TextRange>& aRanges) const { 759 SelectionRanges(&aRanges); 760 Accessible* acc = const_cast<Accessible*>(Acc()); 761 762 size_t startIndex = 0; 763 size_t endIndex = aRanges.Length(); 764 // If this is the document, it contains all ranges, so there's no need to 765 // search for overlapping ranges. 766 if (!acc->IsDoc()) { 767 // Find overlapping ranges. We use binary searches here, which is far more 768 // efficient than cropping every range in the list. 769 // Find the first range that ends after acc starts. 770 TextPoint thisPoint = TextPoint(acc, 0); 771 startIndex = 772 UpperBound(aRanges, 0, aRanges.Length(), [&](const TextRange& range) { 773 return thisPoint.Compare(range.EndPoint()); 774 }); 775 // Find the first range that starts after acc ends. This will be the first 776 // non-overlapping range after startIndex; i.e. the exclusive end of our 777 // desired span. 778 thisPoint = TextPoint(acc, CharacterCount()); 779 endIndex = UpperBound(aRanges, startIndex, aRanges.Length(), 780 [&](const TextRange& range) { 781 return thisPoint.Compare(range.StartPoint()); 782 }); 783 } 784 785 // Exclude ranges that don't overlap acc. 786 size_t nextIndex = 0; 787 aRanges.RemoveElementsBy([&](TextRange& range) { 788 // Set index to the index of range. Increment nextIndex ready for the next 789 // iteration. 790 size_t index = nextIndex++; 791 if (range.StartPoint() == range.EndPoint()) { 792 return true; // Collapsed, so remove this range. 793 } 794 // Remove (return true for) ranges that aren't between startIndex 795 // (inclusive) and endIndex (exclusive). 796 if (index < startIndex || index >= endIndex) { 797 return true; 798 } 799 // The first and last ranges might extend beyond acc, so crop them. 800 if (index == startIndex || index == endIndex - 1) { 801 DebugOnly<bool> cropped = range.Crop(const_cast<Accessible*>(acc)); 802 MOZ_ASSERT(cropped, "range should overlap and thus crop successfully"); 803 } 804 return false; 805 }); 806 } 807 808 int32_t HyperTextAccessibleBase::SelectionCount() { 809 nsTArray<TextRange> ranges; 810 CroppedSelectionRanges(ranges); 811 return static_cast<int32_t>(ranges.Length()); 812 } 813 814 bool HyperTextAccessibleBase::SelectionBoundsAt(int32_t aSelectionNum, 815 int32_t* aStartOffset, 816 int32_t* aEndOffset) { 817 nsTArray<TextRange> ranges; 818 CroppedSelectionRanges(ranges); 819 if (aSelectionNum >= static_cast<int32_t>(ranges.Length())) { 820 return false; 821 } 822 TextRange& range = ranges[aSelectionNum]; 823 Accessible* thisAcc = Acc(); 824 if (range.StartContainer() == thisAcc) { 825 *aStartOffset = range.StartOffset(); 826 } else { 827 bool ok; 828 // range.StartContainer() isn't a text leaf, so don't use its offset. 829 std::tie(ok, *aStartOffset) = 830 TransformOffset(range.StartContainer(), 0, /* aDescendToEnd */ false); 831 } 832 if (range.EndContainer() == thisAcc) { 833 *aEndOffset = range.EndOffset(); 834 } else { 835 bool ok; 836 // range.EndContainer() isn't a text leaf, so don't use its offset. If 837 // range.EndOffset() is > 0, we want to include this container, so pas 838 // offset 1. 839 std::tie(ok, *aEndOffset) = 840 TransformOffset(range.EndContainer(), range.EndOffset() == 0 ? 0 : 1, 841 /* aDescendToEnd */ true); 842 } 843 return true; 844 } 845 846 bool HyperTextAccessibleBase::SetSelectionBoundsAt(int32_t aSelectionNum, 847 int32_t aStartOffset, 848 int32_t aEndOffset) { 849 TextLeafRange range(ToTextLeafPoint(aStartOffset), 850 ToTextLeafPoint(aEndOffset, true)); 851 if (!range) { 852 NS_ERROR("Wrong in offset"); 853 return false; 854 } 855 856 return range.SetSelection(aSelectionNum); 857 } 858 859 void HyperTextAccessibleBase::ScrollSubstringTo(int32_t aStartOffset, 860 int32_t aEndOffset, 861 uint32_t aScrollType) { 862 TextLeafRange range(ToTextLeafPoint(aStartOffset), 863 ToTextLeafPoint(aEndOffset, true)); 864 range.ScrollIntoView(aScrollType); 865 } 866 867 } // namespace mozilla::a11y