no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / accessible / basetypes / HyperTextAccessibleBase.cpp
blobc66180673b1f95625f687c1c2c4d4783c9fc31a0
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/. */
6 #include "HyperTextAccessibleBase.h"
8 #include "mozilla/a11y/Accessible.h"
9 #include "nsAccUtils.h"
10 #include "TextLeafRange.h"
11 #include "TextRange.h"
13 namespace mozilla::a11y {
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();
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);
33 if (index == offsetCount) {
34 // aOffset is past the end of the text.
35 return -1;
37 // index points at the exclusive end after aOffset.
38 return static_cast<int32_t>(index);
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);
58 if (static_cast<int32_t>(aOffset) == lastOffset) {
59 return static_cast<int32_t>(offsets.Length() - 1);
62 return -1;
65 Accessible* HyperTextAccessibleBase::GetChildAtOffset(uint32_t aOffset) const {
66 const Accessible* thisAcc = Acc();
67 return thisAcc->ChildAt(GetChildIndexAtOffset(aOffset));
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;
76 int32_t index = aChild->IndexInParent();
77 if (index == -1) {
78 return -1;
80 return GetChildOffset(index, aInvalidateAfter);
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();
91 return 0;
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);
101 return offsets[aChildIndex - 1];
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);
117 return offsets[aChildIndex - 1];
120 uint32_t HyperTextAccessibleBase::CharacterCount() const {
121 return GetChildOffset(Acc()->ChildCount());
124 index_t HyperTextAccessibleBase::ConvertMagicOffset(int32_t aOffset) const {
125 if (aOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT) {
126 return CharacterCount();
129 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
130 return CaretOffset();
133 return aOffset;
136 void HyperTextAccessibleBase::TextSubstring(int32_t aStartOffset,
137 int32_t aEndOffset,
138 nsAString& aText) const {
139 aText.Truncate();
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;
149 int32_t startChildIdx = GetChildIndexAtOffset(startOffset);
150 if (startChildIdx == -1) {
151 return;
154 int32_t endChildIdx = GetChildIndexAtOffset(endOffset);
155 if (endChildIdx == -1) {
156 return;
159 const Accessible* thisAcc = Acc();
160 if (startChildIdx == endChildIdx) {
161 int32_t childOffset = GetChildOffset(startChildIdx);
162 if (childOffset == -1) {
163 return;
166 Accessible* child = thisAcc->ChildAt(startChildIdx);
167 child->AppendTextTo(aText, startOffset - childOffset,
168 endOffset - startOffset);
169 return;
172 int32_t startChildOffset = GetChildOffset(startChildIdx);
173 if (startChildOffset == -1) {
174 return;
177 Accessible* startChild = thisAcc->ChildAt(startChildIdx);
178 startChild->AppendTextTo(aText, startOffset - startChildOffset);
180 for (int32_t childIdx = startChildIdx + 1; childIdx < endChildIdx;
181 childIdx++) {
182 Accessible* child = thisAcc->ChildAt(childIdx);
183 child->AppendTextTo(aText);
186 int32_t endChildOffset = GetChildOffset(endChildIdx);
187 if (endChildOffset == -1) {
188 return;
191 Accessible* endChild = thisAcc->ChildAt(endChildIdx);
192 endChild->AppendTextTo(aText, 0, endOffset - endChildOffset);
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!");
201 int32_t childIdx = GetChildIndexAtOffset(aOffset);
202 if (childIdx == -1) {
203 return false;
206 Accessible* child = Acc()->ChildAt(childIdx);
207 child->AppendTextTo(aChar, aOffset - GetChildOffset(childIdx), 1);
209 if (aStartOffset && aEndOffset) {
210 *aStartOffset = aOffset;
211 *aEndOffset = aOffset + aChar.Length();
213 return true;
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();
222 TextLeafPoint point = ToTextLeafPoint(static_cast<int32_t>(offset), false);
223 if (!point.mAcc) {
224 return LayoutDeviceIntRect();
227 LayoutDeviceIntRect bounds = point.CharBounds();
228 if (!bounds.x && !bounds.y && bounds.IsZeroArea()) {
229 return bounds;
231 nsAccUtils::ConvertScreenCoordsTo(&bounds.x, &bounds.y, aCoordType, Acc());
232 return bounds;
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;
245 index_t startOffset = ConvertMagicOffset(aStartOffset);
246 index_t endOffset = ConvertMagicOffset(aEndOffset);
247 if (!startOffset.IsValid() || startOffset >= endOffset) {
248 return LayoutDeviceIntRect();
251 // Here's where things get complicated. We can't simply query the first
252 // and last character, and union their bounds. They might reside on different
253 // lines, and a simple union may yield an incorrect width. We
254 // should use the length of the longest spanned line for our width.
256 TextLeafPoint startPoint =
257 ToTextLeafPoint(static_cast<int32_t>(startOffset), false);
258 TextLeafPoint endPoint =
259 ToTextLeafPoint(static_cast<int32_t>(endOffset), true);
260 if (!endPoint) {
261 // The caller provided an invalid offset.
262 return LayoutDeviceIntRect();
265 // Step backwards from the point returned by ToTextLeafPoint above.
266 // For our purposes, `endPoint` should be inclusive.
267 endPoint =
268 endPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
269 if (endPoint < startPoint) {
270 return result;
273 if (endPoint == startPoint) {
274 result = startPoint.CharBounds();
275 } else {
276 TextLeafRange range(startPoint, endPoint);
277 result = range.Bounds();
280 // Calls to TextLeafRange::Bounds() will construct screen coordinates.
281 // Perform any additional conversions here.
282 nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc());
283 return result;
286 int32_t HyperTextAccessibleBase::OffsetAtPoint(int32_t aX, int32_t aY,
287 uint32_t aCoordType) {
288 Accessible* thisAcc = Acc();
289 LayoutDeviceIntPoint coords =
290 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc);
291 if (!thisAcc->Bounds().Contains(coords.x, coords.y)) {
292 // The requested point does not exist in this accessible.
293 // Check if we used fuzzy hittesting to get here and, if
294 // so, return 0 to indicate this text leaf is a valid match.
295 LayoutDeviceIntPoint p(aX, aY);
296 if (aCoordType != nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE) {
297 p = nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc);
299 if (Accessible* doc = nsAccUtils::DocumentFor(thisAcc)) {
300 Accessible* hittestMatch = doc->ChildAtPoint(
301 p.x, p.y, Accessible::EWhichChildAtPoint::DeepestChild);
302 if (hittestMatch && thisAcc == hittestMatch->Parent()) {
303 return 0;
306 return -1;
309 TextLeafPoint startPoint = ToTextLeafPoint(0, false);
310 // As with TextBounds, we walk to the very end of the text contained in this
311 // hypertext and then step backwards to make our endPoint inclusive.
312 TextLeafPoint endPoint =
313 ToTextLeafPoint(static_cast<int32_t>(CharacterCount()), true);
314 endPoint =
315 endPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
316 TextLeafPoint point = startPoint;
317 // XXX: We should create a TextLeafRange object for this hypertext and move
318 // this search inside the TextLeafRange class.
319 // If there are no characters in this container, we might have moved endPoint
320 // before startPoint. In that case, we shouldn't try to move further forward,
321 // as that might result in an infinite loop.
322 if (startPoint <= endPoint) {
323 for (; !point.ContainsPoint(coords.x, coords.y) && point != endPoint;
324 point =
325 point.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext)) {
328 if (!point.ContainsPoint(coords.x, coords.y)) {
329 LayoutDeviceIntRect startRect = startPoint.CharBounds();
330 if (coords.x < startRect.x || coords.y < startRect.y) {
331 // Bug 1816601: The point is within the container but above or to the left
332 // of the rectangle at offset 0. We should really return -1, but we've
333 // returned 0 for many years due to a bug. Some users have unfortunately
334 // come to rely on this, so perpetuate this here.
335 return 0;
337 return -1;
339 DebugOnly<bool> ok = false;
340 int32_t htOffset;
341 std::tie(ok, htOffset) =
342 TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false);
343 MOZ_ASSERT(ok, "point should be a descendant of this");
344 return htOffset;
347 TextLeafPoint HyperTextAccessibleBase::ToTextLeafPoint(int32_t aOffset,
348 bool aDescendToEnd) {
349 Accessible* thisAcc = Acc();
350 if (!thisAcc->HasChildren()) {
351 return TextLeafPoint(thisAcc, 0);
353 Accessible* child = GetChildAtOffset(aOffset);
354 if (!child) {
355 return TextLeafPoint();
357 if (HyperTextAccessibleBase* childHt = child->AsHyperTextBase()) {
358 return childHt->ToTextLeafPoint(
359 aDescendToEnd ? static_cast<int32_t>(childHt->CharacterCount()) : 0,
360 aDescendToEnd);
362 int32_t offset = aOffset - GetChildOffset(child);
363 return TextLeafPoint(child, offset);
366 std::pair<bool, int32_t> HyperTextAccessibleBase::TransformOffset(
367 Accessible* aDescendant, int32_t aOffset, bool aIsEndOffset) const {
368 const Accessible* thisAcc = Acc();
369 // From the descendant, go up and get the immediate child of this hypertext.
370 int32_t offset = aOffset;
371 Accessible* descendant = aDescendant;
372 while (descendant) {
373 Accessible* parent = descendant->Parent();
374 if (parent == thisAcc) {
375 return {true, GetChildOffset(descendant) + offset};
378 // This offset no longer applies because the passed-in text object is not
379 // a child of the hypertext. This happens when there are nested hypertexts,
380 // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
381 // to make it relative the hypertext.
382 // If the end offset is not supposed to be inclusive and the original point
383 // is not at 0 offset then the returned offset should be after an embedded
384 // character the original point belongs to.
385 if (aIsEndOffset) {
386 offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
387 } else {
388 offset = 0;
391 descendant = parent;
394 // The given a11y point cannot be mapped to an offset relative to this
395 // hypertext accessible. Return the start or the end depending on whether this
396 // is a start ofset or an end offset, thus clipping to the relevant endpoint.
397 return {false, aIsEndOffset ? static_cast<int32_t>(CharacterCount()) : 0};
400 void HyperTextAccessibleBase::AdjustOriginIfEndBoundary(
401 TextLeafPoint& aOrigin, AccessibleTextBoundary aBoundaryType,
402 bool aAtOffset) const {
403 if (aBoundaryType != nsIAccessibleText::BOUNDARY_LINE_END &&
404 aBoundaryType != nsIAccessibleText::BOUNDARY_WORD_END) {
405 return;
407 TextLeafPoint actualOrig =
408 aOrigin.IsCaret() ? aOrigin.ActualizeCaret(/* aAdjustAtEndOfLine */ false)
409 : aOrigin;
410 if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) {
411 if (!actualOrig.IsLineFeedChar()) {
412 return;
414 aOrigin =
415 actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
416 } else { // BOUNDARY_WORD_END
417 if (aAtOffset) {
418 // For TextAtOffset with BOUNDARY_WORD_END, we follow WebKitGtk here and
419 // return the word which ends after the origin if the origin is a word end
420 // boundary. Also, if the caret is at the end of a line, our tests expect
421 // the word after the caret, not the word before. The reason for that
422 // is a mystery lost to history. We can do that by explicitly using the
423 // actualized caret without adjusting for end of line.
424 aOrigin = actualOrig;
425 return;
427 if (!actualOrig.IsSpace()) {
428 return;
430 TextLeafPoint prevChar =
431 actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
432 if (prevChar != actualOrig && !prevChar.IsSpace()) {
433 // aOrigin is a word end boundary.
434 aOrigin = prevChar;
439 void HyperTextAccessibleBase::TextBeforeOffset(
440 int32_t aOffset, AccessibleTextBoundary aBoundaryType,
441 int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) {
442 *aStartOffset = *aEndOffset = 0;
443 aText.Truncate();
445 if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
446 aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
447 return; // Not implemented.
450 uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
451 if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
452 NS_ERROR("Wrong given offset!");
453 return;
456 if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
457 if (adjustedOffset > 0) {
458 CharAt(static_cast<int32_t>(adjustedOffset) - 1, aText, aStartOffset,
459 aEndOffset);
461 return;
464 TextLeafPoint orig;
465 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
466 orig = TextLeafPoint::GetCaret(Acc());
467 } else {
468 orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset));
470 if (!orig) {
471 // This can happen if aOffset is invalid.
472 return;
474 AdjustOriginIfEndBoundary(orig, aBoundaryType);
475 TextLeafPoint end =
476 orig.FindBoundary(aBoundaryType, eDirPrevious,
477 TextLeafPoint::BoundaryFlags::eIncludeOrigin);
478 bool ok;
479 std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
480 /* aIsEndOffset */ true);
481 if (!ok) {
482 // There is no previous boundary inside this HyperText.
483 *aStartOffset = *aEndOffset = 0;
484 return;
486 TextLeafPoint start = end.FindBoundary(aBoundaryType, eDirPrevious);
487 // If TransformOffset fails because start is outside this HyperText,
488 // *aStartOffset will be 0, which is what we want.
489 std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
490 /* aIsEndOffset */ false);
491 TextSubstring(*aStartOffset, *aEndOffset, aText);
494 void HyperTextAccessibleBase::TextAtOffset(int32_t aOffset,
495 AccessibleTextBoundary aBoundaryType,
496 int32_t* aStartOffset,
497 int32_t* aEndOffset,
498 nsAString& aText) {
499 *aStartOffset = *aEndOffset = 0;
500 aText.Truncate();
502 if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
503 aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
504 return; // Not implemented.
507 uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
508 if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
509 NS_ERROR("Wrong given offset!");
510 return;
513 if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
514 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
515 TextLeafPoint caret = TextLeafPoint::GetCaret(Acc());
516 if (caret.IsCaretAtEndOfLine()) {
517 // The caret is at the end of the line. Return no character.
518 *aStartOffset = *aEndOffset = static_cast<int32_t>(adjustedOffset);
519 return;
522 CharAt(adjustedOffset, aText, aStartOffset, aEndOffset);
523 return;
526 TextLeafPoint start, end;
527 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
528 start = TextLeafPoint::GetCaret(Acc());
529 AdjustOriginIfEndBoundary(start, aBoundaryType, /* aAtOffset */ true);
530 end = start;
531 } else {
532 start = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset));
533 Accessible* childAcc = GetChildAtOffset(adjustedOffset);
534 if (childAcc && childAcc->IsHyperText()) {
535 // We're searching for boundaries enclosing an embedded object.
536 // An embedded object might contain several boundaries itself.
537 // Thus, we must ensure we search for the end boundary from the last
538 // text in the subtree, not just the first.
539 // For example, if the embedded object is a link and it contains two
540 // words, but the second word expands beyond the link, we want to
541 // include the part of the second word which is outside of the link.
542 end = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset),
543 /* aDescendToEnd */ true);
544 } else {
545 AdjustOriginIfEndBoundary(start, aBoundaryType,
546 /* aAtOffset */ true);
547 end = start;
550 if (!start) {
551 // This can happen if aOffset is invalid.
552 return;
554 start = start.FindBoundary(aBoundaryType, eDirPrevious,
555 TextLeafPoint::BoundaryFlags::eIncludeOrigin);
556 bool ok;
557 std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
558 /* aIsEndOffset */ false);
559 end = end.FindBoundary(aBoundaryType, eDirNext);
560 std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
561 /* aIsEndOffset */ true);
562 TextSubstring(*aStartOffset, *aEndOffset, aText);
565 void HyperTextAccessibleBase::TextAfterOffset(
566 int32_t aOffset, AccessibleTextBoundary aBoundaryType,
567 int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) {
568 *aStartOffset = *aEndOffset = 0;
569 aText.Truncate();
571 if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
572 aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
573 return; // Not implemented.
576 uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
577 if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
578 NS_ERROR("Wrong given offset!");
579 return;
582 if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
583 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET && adjustedOffset > 0 &&
584 TextLeafPoint::GetCaret(Acc()).IsCaretAtEndOfLine()) {
585 --adjustedOffset;
587 uint32_t count = CharacterCount();
588 if (adjustedOffset >= count) {
589 *aStartOffset = *aEndOffset = static_cast<int32_t>(count);
590 } else {
591 CharAt(static_cast<int32_t>(adjustedOffset) + 1, aText, aStartOffset,
592 aEndOffset);
594 return;
597 TextLeafPoint orig;
598 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
599 orig = TextLeafPoint::GetCaret(Acc());
600 } else {
601 orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset),
602 /* aDescendToEnd */ true);
604 if (!orig) {
605 // This can happen if aOffset is invalid.
606 return;
608 AdjustOriginIfEndBoundary(orig, aBoundaryType);
609 TextLeafPoint start = orig.FindBoundary(aBoundaryType, eDirNext);
610 bool ok;
611 std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
612 /* aIsEndOffset */ false);
613 if (!ok) {
614 // There is no next boundary inside this HyperText.
615 *aStartOffset = *aEndOffset = static_cast<int32_t>(CharacterCount());
616 return;
618 TextLeafPoint end = start.FindBoundary(aBoundaryType, eDirNext);
619 // If TransformOffset fails because end is outside this HyperText,
620 // *aEndOffset will be CharacterCount(), which is what we want.
621 std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
622 /* aIsEndOffset */ true);
623 TextSubstring(*aStartOffset, *aEndOffset, aText);
626 int32_t HyperTextAccessibleBase::CaretOffset() const {
627 TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc()))
628 .ActualizeCaret(/* aAdjustAtEndOfLine */ false);
629 if (point.mOffset == 0 && point.mAcc == Acc()) {
630 // If a text box is empty, there will be no children, so point.mAcc will be
631 // this HyperText.
632 return 0;
634 auto [ok, htOffset] =
635 TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false);
636 if (!ok) {
637 // The caret is not within this HyperText.
638 return -1;
640 return htOffset;
643 int32_t HyperTextAccessibleBase::CaretLineNumber() {
644 TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc()))
645 .ActualizeCaret(/* aAdjustAtEndOfLine */ false);
646 if (point.mOffset == 0 && point.mAcc == Acc()) {
647 MOZ_ASSERT(CharacterCount() == 0);
648 // If a text box is empty, there will be no children, so point.mAcc will be
649 // this HyperText.
650 return 1;
653 if (!point.mAcc ||
654 (point.mAcc != Acc() && !Acc()->IsAncestorOf(point.mAcc))) {
655 // The caret is not within this HyperText.
656 return -1;
659 TextLeafPoint firstPointInThis = TextLeafPoint(Acc(), 0);
660 int32_t lineNumber = 1;
661 for (TextLeafPoint line = point; line && firstPointInThis < line;
662 line = line.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START,
663 eDirPrevious)) {
664 lineNumber++;
667 return lineNumber;
670 bool HyperTextAccessibleBase::IsValidOffset(int32_t aOffset) {
671 index_t offset = ConvertMagicOffset(aOffset);
672 return offset.IsValid() && offset <= CharacterCount();
675 bool HyperTextAccessibleBase::IsValidRange(int32_t aStartOffset,
676 int32_t aEndOffset) {
677 index_t startOffset = ConvertMagicOffset(aStartOffset);
678 index_t endOffset = ConvertMagicOffset(aEndOffset);
679 return startOffset.IsValid() && endOffset.IsValid() &&
680 startOffset <= endOffset && endOffset <= CharacterCount();
683 uint32_t HyperTextAccessibleBase::LinkCount() {
684 return Acc()->EmbeddedChildCount();
687 Accessible* HyperTextAccessibleBase::LinkAt(uint32_t aIndex) {
688 return Acc()->EmbeddedChildAt(aIndex);
691 int32_t HyperTextAccessibleBase::LinkIndexOf(Accessible* aLink) {
692 return Acc()->IndexOfEmbeddedChild(aLink);
695 already_AddRefed<AccAttributes> HyperTextAccessibleBase::TextAttributes(
696 bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset,
697 int32_t* aEndOffset) {
698 *aStartOffset = *aEndOffset = 0;
699 index_t offset = ConvertMagicOffset(aOffset);
700 if (!offset.IsValid() || offset > CharacterCount()) {
701 NS_ERROR("Wrong in offset!");
702 return RefPtr{new AccAttributes()}.forget();
705 Accessible* originAcc = GetChildAtOffset(offset);
706 if (!originAcc) {
707 // Offset 0 is correct offset when accessible has empty text. Include
708 // default attributes if they were requested, otherwise return empty set.
709 if (offset == 0) {
710 if (aIncludeDefAttrs) {
711 return DefaultTextAttributes();
714 return RefPtr{new AccAttributes()}.forget();
717 if (!originAcc->IsText()) {
718 // This is an embedded object. One or more consecutive embedded objects
719 // form a single attrs run with no attributes.
720 *aStartOffset = aOffset;
721 *aEndOffset = aOffset + 1;
722 Accessible* parent = originAcc->Parent();
723 if (!parent) {
724 return RefPtr{new AccAttributes()}.forget();
726 int32_t originIdx = originAcc->IndexInParent();
727 if (originIdx > 0) {
728 // Check for embedded objects before the origin.
729 for (uint32_t idx = originIdx - 1;; --idx) {
730 Accessible* sibling = parent->ChildAt(idx);
731 if (sibling->IsText()) {
732 break;
734 --*aStartOffset;
735 if (idx == 0) {
736 break;
740 // Check for embedded objects after the origin.
741 for (uint32_t idx = originIdx + 1;; ++idx) {
742 Accessible* sibling = parent->ChildAt(idx);
743 if (!sibling || sibling->IsText()) {
744 break;
746 ++*aEndOffset;
748 return RefPtr{new AccAttributes()}.forget();
751 TextLeafPoint origin = ToTextLeafPoint(static_cast<int32_t>(offset));
752 TextLeafPoint start =
753 origin.FindTextAttrsStart(eDirPrevious, /* aIncludeOrigin */ true);
754 bool ok;
755 std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
756 /* aIsEndOffset */ false);
757 TextLeafPoint end =
758 origin.FindTextAttrsStart(eDirNext, /* aIncludeOrigin */ false);
759 std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
760 /* aIsEndOffset */ true);
761 return origin.GetTextAttributes(aIncludeDefAttrs);
764 void HyperTextAccessibleBase::CroppedSelectionRanges(
765 nsTArray<TextRange>& aRanges) const {
766 SelectionRanges(&aRanges);
767 const Accessible* acc = Acc();
768 aRanges.RemoveElementsBy([acc](auto& range) {
769 if (range.StartPoint() == range.EndPoint()) {
770 return true; // Collapsed, so remove this range.
772 // If this is the document, it contains all ranges, so there's no need to
773 // crop.
774 if (!acc->IsDoc()) {
775 // If we fail to crop, the range is outside acc, so remove it.
776 return !range.Crop(const_cast<Accessible*>(acc));
778 return false;
782 int32_t HyperTextAccessibleBase::SelectionCount() {
783 nsTArray<TextRange> ranges;
784 CroppedSelectionRanges(ranges);
785 return static_cast<int32_t>(ranges.Length());
788 bool HyperTextAccessibleBase::SelectionBoundsAt(int32_t aSelectionNum,
789 int32_t* aStartOffset,
790 int32_t* aEndOffset) {
791 nsTArray<TextRange> ranges;
792 CroppedSelectionRanges(ranges);
793 if (aSelectionNum >= static_cast<int32_t>(ranges.Length())) {
794 return false;
796 TextRange& range = ranges[aSelectionNum];
797 Accessible* thisAcc = Acc();
798 if (range.StartContainer() == thisAcc) {
799 *aStartOffset = range.StartOffset();
800 } else {
801 bool ok;
802 // range.StartContainer() isn't a text leaf, so don't use its offset.
803 std::tie(ok, *aStartOffset) =
804 TransformOffset(range.StartContainer(), 0, /* aDescendToEnd */ false);
806 if (range.EndContainer() == thisAcc) {
807 *aEndOffset = range.EndOffset();
808 } else {
809 bool ok;
810 // range.EndContainer() isn't a text leaf, so don't use its offset. If
811 // range.EndOffset() is > 0, we want to include this container, so pas
812 // offset 1.
813 std::tie(ok, *aEndOffset) =
814 TransformOffset(range.EndContainer(), range.EndOffset() == 0 ? 0 : 1,
815 /* aDescendToEnd */ true);
817 return true;
820 bool HyperTextAccessibleBase::SetSelectionBoundsAt(int32_t aSelectionNum,
821 int32_t aStartOffset,
822 int32_t aEndOffset) {
823 TextLeafRange range(ToTextLeafPoint(aStartOffset),
824 ToTextLeafPoint(aEndOffset, true));
825 if (!range) {
826 NS_ERROR("Wrong in offset");
827 return false;
830 return range.SetSelection(aSelectionNum);
833 void HyperTextAccessibleBase::ScrollSubstringTo(int32_t aStartOffset,
834 int32_t aEndOffset,
835 uint32_t aScrollType) {
836 TextLeafRange range(ToTextLeafPoint(aStartOffset),
837 ToTextLeafPoint(aEndOffset, true));
838 range.ScrollIntoView(aScrollType);
841 } // namespace mozilla::a11y