/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "HyperTextAccessibleBase.h" #include "mozilla/a11y/Accessible.h" #include "nsAccUtils.h" #include "TextLeafRange.h" #include "TextRange.h" namespace mozilla::a11y { int32_t HyperTextAccessibleBase::GetChildIndexAtOffset(uint32_t aOffset) const { auto& offsets = const_cast(this)->GetCachedHyperTextOffsets(); int32_t lastOffset = 0; const uint32_t offsetCount = offsets.Length(); if (offsetCount > 0) { lastOffset = offsets[offsetCount - 1]; if (static_cast(aOffset) < lastOffset) { // We've cached up to aOffset. size_t index; if (BinarySearch(offsets, 0, offsetCount, static_cast(aOffset), &index)) { // aOffset is the exclusive end of a child, so return the child before // it. return static_cast((index < offsetCount - 1) ? index + 1 : index); } if (index == offsetCount) { // aOffset is past the end of the text. return -1; } // index points at the exclusive end after aOffset. return static_cast(index); } } // We haven't yet cached up to aOffset. Find it, caching as we go. const Accessible* thisAcc = Acc(); uint32_t childCount = thisAcc->ChildCount(); // Even though we're only caching up to aOffset, it's likely that we'll // eventually cache offsets for all children. Pre-allocate thus to minimize // re-allocations. offsets.SetCapacity(childCount); while (offsets.Length() < childCount) { Accessible* child = thisAcc->ChildAt(offsets.Length()); lastOffset += static_cast(nsAccUtils::TextLength(child)); offsets.AppendElement(lastOffset); if (static_cast(aOffset) < lastOffset) { return static_cast(offsets.Length() - 1); } } if (static_cast(aOffset) == lastOffset) { return static_cast(offsets.Length() - 1); } return -1; } Accessible* HyperTextAccessibleBase::GetChildAtOffset(uint32_t aOffset) const { const Accessible* thisAcc = Acc(); return thisAcc->ChildAt(GetChildIndexAtOffset(aOffset)); } int32_t HyperTextAccessibleBase::GetChildOffset(const Accessible* aChild, bool aInvalidateAfter) const { const Accessible* thisAcc = Acc(); if (aChild->Parent() != thisAcc) { return -1; } int32_t index = aChild->IndexInParent(); if (index == -1) { return -1; } return GetChildOffset(index, aInvalidateAfter); } int32_t HyperTextAccessibleBase::GetChildOffset(uint32_t aChildIndex, bool aInvalidateAfter) const { auto& offsets = const_cast(this)->GetCachedHyperTextOffsets(); if (aChildIndex == 0) { if (aInvalidateAfter) { offsets.Clear(); } return 0; } int32_t countCachedAfterChild = static_cast(offsets.Length()) - static_cast(aChildIndex); if (countCachedAfterChild > 0) { // We've cached up to aChildIndex. if (aInvalidateAfter) { offsets.RemoveElementsAt(aChildIndex, countCachedAfterChild); } return offsets[aChildIndex - 1]; } // We haven't yet cached up to aChildIndex. Find it, caching as we go. const Accessible* thisAcc = Acc(); // Even though we're only caching up to aChildIndex, it's likely that we'll // eventually cache offsets for all children. Pre-allocate thus to minimize // re-allocations. offsets.SetCapacity(thisAcc->ChildCount()); uint32_t lastOffset = offsets.IsEmpty() ? 0 : offsets[offsets.Length() - 1]; while (offsets.Length() < aChildIndex) { Accessible* child = thisAcc->ChildAt(offsets.Length()); lastOffset += nsAccUtils::TextLength(child); offsets.AppendElement(lastOffset); } return offsets[aChildIndex - 1]; } uint32_t HyperTextAccessibleBase::CharacterCount() const { return GetChildOffset(Acc()->ChildCount()); } index_t HyperTextAccessibleBase::ConvertMagicOffset(int32_t aOffset) const { if (aOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT) { return CharacterCount(); } if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { return CaretOffset(); } return aOffset; } void HyperTextAccessibleBase::TextSubstring(int32_t aStartOffset, int32_t aEndOffset, nsAString& aText) const { aText.Truncate(); index_t startOffset = ConvertMagicOffset(aStartOffset); index_t endOffset = ConvertMagicOffset(aEndOffset); if (!startOffset.IsValid() || !endOffset.IsValid() || startOffset > endOffset || endOffset > CharacterCount()) { NS_ERROR("Wrong in offset"); return; } int32_t startChildIdx = GetChildIndexAtOffset(startOffset); if (startChildIdx == -1) { return; } int32_t endChildIdx = GetChildIndexAtOffset(endOffset); if (endChildIdx == -1) { return; } const Accessible* thisAcc = Acc(); if (startChildIdx == endChildIdx) { int32_t childOffset = GetChildOffset(startChildIdx); if (childOffset == -1) { return; } Accessible* child = thisAcc->ChildAt(startChildIdx); child->AppendTextTo(aText, startOffset - childOffset, endOffset - startOffset); return; } int32_t startChildOffset = GetChildOffset(startChildIdx); if (startChildOffset == -1) { return; } Accessible* startChild = thisAcc->ChildAt(startChildIdx); startChild->AppendTextTo(aText, startOffset - startChildOffset); for (int32_t childIdx = startChildIdx + 1; childIdx < endChildIdx; childIdx++) { Accessible* child = thisAcc->ChildAt(childIdx); child->AppendTextTo(aText); } int32_t endChildOffset = GetChildOffset(endChildIdx); if (endChildOffset == -1) { return; } Accessible* endChild = thisAcc->ChildAt(endChildIdx); endChild->AppendTextTo(aText, 0, endOffset - endChildOffset); } bool HyperTextAccessibleBase::CharAt(int32_t aOffset, nsAString& aChar, int32_t* aStartOffset, int32_t* aEndOffset) { MOZ_ASSERT(!aStartOffset == !aEndOffset, "Offsets should be both defined or both undefined!"); int32_t childIdx = GetChildIndexAtOffset(aOffset); if (childIdx == -1) { return false; } Accessible* child = Acc()->ChildAt(childIdx); child->AppendTextTo(aChar, aOffset - GetChildOffset(childIdx), 1); if (aStartOffset && aEndOffset) { *aStartOffset = aOffset; *aEndOffset = aOffset + aChar.Length(); } return true; } LayoutDeviceIntRect HyperTextAccessibleBase::CharBounds(int32_t aOffset, uint32_t aCoordType) { index_t offset = ConvertMagicOffset(aOffset); if (!offset.IsValid() || offset > CharacterCount()) { return LayoutDeviceIntRect(); } TextLeafPoint point = ToTextLeafPoint(static_cast(offset), false); if (!point.mAcc) { return LayoutDeviceIntRect(); } LayoutDeviceIntRect bounds = point.CharBounds(); if (!bounds.x && !bounds.y && bounds.IsZeroArea()) { return bounds; } nsAccUtils::ConvertScreenCoordsTo(&bounds.x, &bounds.y, aCoordType, Acc()); return bounds; } LayoutDeviceIntRect HyperTextAccessibleBase::TextBounds(int32_t aStartOffset, int32_t aEndOffset, uint32_t aCoordType) { LayoutDeviceIntRect result; if (CharacterCount() == 0) { result = Acc()->Bounds(); nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc()); return result; } index_t startOffset = ConvertMagicOffset(aStartOffset); index_t endOffset = ConvertMagicOffset(aEndOffset); if (!startOffset.IsValid() || startOffset > endOffset) { return LayoutDeviceIntRect(); } TextLeafPoint startPoint = ToTextLeafPoint(static_cast(startOffset), false); TextLeafPoint endPoint = ToTextLeafPoint(static_cast(endOffset), true); if (!endPoint) { // The caller provided an invalid offset. return LayoutDeviceIntRect(); } TextLeafRange range(startPoint, endPoint); result = range.Bounds(); // Calls to TextLeafRange::Bounds() will construct screen coordinates. // Perform any additional conversions here. nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc()); return result; } int32_t HyperTextAccessibleBase::OffsetAtPoint(int32_t aX, int32_t aY, uint32_t aCoordType) { Accessible* thisAcc = Acc(); LayoutDeviceIntPoint coords = nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc); if (!thisAcc->Bounds().Contains(coords.x, coords.y)) { // The requested point does not exist in this accessible. // Check if we used fuzzy hittesting to get here and, if // so, return 0 to indicate this text leaf is a valid match. LayoutDeviceIntPoint p(aX, aY); if (aCoordType != nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE) { p = nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc); } if (Accessible* doc = nsAccUtils::DocumentFor(thisAcc)) { Accessible* hittestMatch = doc->ChildAtPoint( p.x, p.y, Accessible::EWhichChildAtPoint::DeepestChild); if (hittestMatch && thisAcc == hittestMatch->Parent()) { return 0; } } return -1; } TextLeafPoint startPoint = ToTextLeafPoint(0, false); // Walk to the very end of the text contained in this hypertext in order to // hit test it in its entirety. TextLeafPoint endPoint = ToTextLeafPoint(static_cast(CharacterCount()), true); TextLeafRange range{startPoint, endPoint}; TextLeafPoint point = range.TextLeafPointAtScreenPoint(coords.x, coords.y); if (!point.ContainsPoint(coords.x, coords.y)) { LayoutDeviceIntRect startRect = startPoint.CharBounds(); if (coords.x < startRect.x || coords.y < startRect.y) { // Bug 1816601: The point is within the container but above or to the left // of the rectangle at offset 0. We should really return -1, but we've // returned 0 for many years due to a bug. Some users have unfortunately // come to rely on this, so perpetuate this here. return 0; } return -1; } DebugOnly ok = false; int32_t htOffset; std::tie(ok, htOffset) = TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false); MOZ_ASSERT(ok, "point should be a descendant of this"); return htOffset; } TextLeafPoint HyperTextAccessibleBase::ToTextLeafPoint(int32_t aOffset, bool aDescendToEnd) { Accessible* thisAcc = Acc(); if (!thisAcc->HasChildren()) { return TextLeafPoint(thisAcc, 0); } Accessible* child = GetChildAtOffset(aOffset); if (!child) { return TextLeafPoint(); } int32_t offset = aOffset - GetChildOffset(child); if (HyperTextAccessibleBase* childHt = child->AsHyperTextBase()) { // This child is an embedded object, so the offset can only be 0 or 1. MOZ_ASSERT(offset == 0 || offset == 1); // Offset 1 refers to the end of this container, so descend to its end. const bool end = aDescendToEnd || offset == 1; return childHt->ToTextLeafPoint( end ? static_cast(childHt->CharacterCount()) : 0, end); } return TextLeafPoint(child, offset); } std::pair HyperTextAccessibleBase::TransformOffset( Accessible* aDescendant, int32_t aOffset, bool aIsEndOffset) const { const Accessible* thisAcc = Acc(); // From the descendant, go up and get the immediate child of this hypertext. int32_t offset = aOffset; Accessible* descendant = aDescendant; while (descendant) { Accessible* parent = descendant->Parent(); if (parent == thisAcc) { return {true, GetChildOffset(descendant) + offset}; } // This offset no longer applies because the passed-in text object is not // a child of the hypertext. This happens when there are nested hypertexts, // e.g.
abc

def

ghi
. Thus we need to adjust the offset // to make it relative the hypertext. // If the end offset is not supposed to be inclusive and the original point // is not at 0 offset then the returned offset should be after an embedded // character the original point belongs to. if (aIsEndOffset) { offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0; } else { offset = 0; } descendant = parent; } // The given a11y point cannot be mapped to an offset relative to this // hypertext accessible. Return the start or the end depending on whether this // is a start ofset or an end offset, thus clipping to the relevant endpoint. return {false, aIsEndOffset ? static_cast(CharacterCount()) : 0}; } void HyperTextAccessibleBase::AdjustOriginIfEndBoundary( TextLeafPoint& aOrigin, AccessibleTextBoundary aBoundaryType, bool aAtOffset) const { if (aBoundaryType != nsIAccessibleText::BOUNDARY_LINE_END && aBoundaryType != nsIAccessibleText::BOUNDARY_WORD_END) { return; } TextLeafPoint actualOrig = aOrigin; // We explicitly care about the character at this offset. We don't want // FindBoundary to behave differently even if this is the insertion point at // the end of a line. actualOrig.mIsEndOfLineInsertionPoint = false; if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) { if (!actualOrig.IsLineFeedChar()) { return; } aOrigin = actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); } else { // BOUNDARY_WORD_END if (aAtOffset) { // For TextAtOffset with BOUNDARY_WORD_END, we follow WebKitGtk here and // return the word which ends after the origin if the origin is a word end // boundary. Also, if the caret is at the end of a line, our tests expect // the word after the caret, not the word before. The reason for that // is a mystery lost to history. We can do that by explicitly using the // caret without adjusting for end of line. aOrigin = actualOrig; return; } if (!actualOrig.IsSpace()) { return; } TextLeafPoint prevChar = actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); if (prevChar != actualOrig && !prevChar.IsSpace()) { // aOrigin is a word end boundary. aOrigin = prevChar; } } } void HyperTextAccessibleBase::TextBeforeOffset( int32_t aOffset, AccessibleTextBoundary aBoundaryType, int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) { *aStartOffset = *aEndOffset = 0; aText.Truncate(); if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START || aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) { return; // Not implemented. } uint32_t adjustedOffset = ConvertMagicOffset(aOffset); if (adjustedOffset == std::numeric_limits::max()) { NS_ERROR("Wrong given offset!"); return; } if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) { if (adjustedOffset > 0) { CharAt(static_cast(adjustedOffset) - 1, aText, aStartOffset, aEndOffset); } return; } TextLeafPoint orig; if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { orig = TextLeafPoint::GetCaret(Acc()); } else { orig = ToTextLeafPoint(static_cast(adjustedOffset)); } if (!orig) { // This can happen if aOffset is invalid. return; } AdjustOriginIfEndBoundary(orig, aBoundaryType); TextLeafPoint end = orig.FindBoundary(aBoundaryType, eDirPrevious, TextLeafPoint::BoundaryFlags::eIncludeOrigin); bool ok; std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, /* aIsEndOffset */ true); if (!ok) { // There is no previous boundary inside this HyperText. *aStartOffset = *aEndOffset = 0; return; } TextLeafPoint start = end.FindBoundary(aBoundaryType, eDirPrevious); // If TransformOffset fails because start is outside this HyperText, // *aStartOffset will be 0, which is what we want. std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, /* aIsEndOffset */ false); TextSubstring(*aStartOffset, *aEndOffset, aText); } void HyperTextAccessibleBase::TextAtOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType, int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) { *aStartOffset = *aEndOffset = 0; aText.Truncate(); if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START || aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) { return; // Not implemented. } uint32_t adjustedOffset = ConvertMagicOffset(aOffset); if (adjustedOffset == std::numeric_limits::max()) { NS_ERROR("Wrong given offset!"); return; } if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) { if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { TextLeafPoint caret = TextLeafPoint::GetCaret(Acc()); if (caret.mIsEndOfLineInsertionPoint) { // The caret is at the end of the line. Return no character. *aStartOffset = *aEndOffset = static_cast(adjustedOffset); return; } } CharAt(adjustedOffset, aText, aStartOffset, aEndOffset); return; } TextLeafPoint start, end; if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { start = TextLeafPoint::GetCaret(Acc()); AdjustOriginIfEndBoundary(start, aBoundaryType, /* aAtOffset */ true); end = start; } else { start = ToTextLeafPoint(static_cast(adjustedOffset)); Accessible* childAcc = GetChildAtOffset(adjustedOffset); if (childAcc && childAcc->IsHyperText()) { // We're searching for boundaries enclosing an embedded object. // An embedded object might contain several boundaries itself. // Thus, we must ensure we search for the end boundary from the last // text in the subtree, not just the first. // For example, if the embedded object is a link and it contains two // words, but the second word expands beyond the link, we want to // include the part of the second word which is outside of the link. end = ToTextLeafPoint(static_cast(adjustedOffset), /* aDescendToEnd */ true); } else { AdjustOriginIfEndBoundary(start, aBoundaryType, /* aAtOffset */ true); end = start; } } if (!start) { // This can happen if aOffset is invalid. return; } start = start.FindBoundary(aBoundaryType, eDirPrevious, TextLeafPoint::BoundaryFlags::eIncludeOrigin); bool ok; std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, /* aIsEndOffset */ false); end = end.FindBoundary(aBoundaryType, eDirNext); std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, /* aIsEndOffset */ true); TextSubstring(*aStartOffset, *aEndOffset, aText); } void HyperTextAccessibleBase::TextAfterOffset( int32_t aOffset, AccessibleTextBoundary aBoundaryType, int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) { *aStartOffset = *aEndOffset = 0; aText.Truncate(); if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START || aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) { return; // Not implemented. } uint32_t adjustedOffset = ConvertMagicOffset(aOffset); if (adjustedOffset == std::numeric_limits::max()) { NS_ERROR("Wrong given offset!"); return; } if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) { if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET && adjustedOffset > 0 && TextLeafPoint::GetCaret(Acc()).mIsEndOfLineInsertionPoint) { --adjustedOffset; } uint32_t count = CharacterCount(); if (adjustedOffset >= count) { *aStartOffset = *aEndOffset = static_cast(count); } else { CharAt(static_cast(adjustedOffset) + 1, aText, aStartOffset, aEndOffset); } return; } TextLeafPoint orig; if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { orig = TextLeafPoint::GetCaret(Acc()); } else { orig = ToTextLeafPoint(static_cast(adjustedOffset), /* aDescendToEnd */ true); } if (!orig) { // This can happen if aOffset is invalid. return; } AdjustOriginIfEndBoundary(orig, aBoundaryType); TextLeafPoint start = orig.FindBoundary(aBoundaryType, eDirNext); bool ok; std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, /* aIsEndOffset */ false); if (!ok) { // There is no next boundary inside this HyperText. *aStartOffset = *aEndOffset = static_cast(CharacterCount()); return; } TextLeafPoint end = start.FindBoundary(aBoundaryType, eDirNext); // If TransformOffset fails because end is outside this HyperText, // *aEndOffset will be CharacterCount(), which is what we want. std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, /* aIsEndOffset */ true); TextSubstring(*aStartOffset, *aEndOffset, aText); } int32_t HyperTextAccessibleBase::CaretOffset() const { TextLeafPoint point = TextLeafPoint::GetCaret(const_cast(Acc())); if (point.mOffset == 0 && point.mAcc == Acc()) { // If a text box is empty, there will be no children, so point.mAcc will be // this HyperText. return 0; } auto [ok, htOffset] = TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false); if (!ok) { // The caret is not within this HyperText. return -1; } return htOffset; } void HyperTextAccessibleBase::SetCaretOffset(int32_t aOffset) { TextLeafPoint point = ToTextLeafPoint(aOffset); TextLeafRange range(point, point); if (!range) { NS_ERROR("Wrong in offset"); return; } range.SetSelection(TextLeafRange::kRemoveAllExistingSelectedRanges); } int32_t HyperTextAccessibleBase::CaretLineNumber() { TextLeafPoint point = TextLeafPoint::GetCaret(const_cast(Acc())); if (point.mOffset == 0 && point.mAcc == Acc()) { MOZ_ASSERT(CharacterCount() == 0); // If a text box is empty, there will be no children, so point.mAcc will be // this HyperText. return 1; } if (!point.mAcc || (point.mAcc != Acc() && !Acc()->IsAncestorOf(point.mAcc))) { // The caret is not within this HyperText. return -1; } // Walk forward by line from the start of the container. TextLeafPoint line = TextLeafPoint(Acc(), 0); int32_t lineNumber = 0; for (; line && line < point; line = line.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START, eDirNext)) { ++lineNumber; } // The caret might be right at the start of a line, in which case we should // increment the line number. We shouldn't do that if the caret is at the end // of a line or container, though. if (line == point && !point.mIsEndOfLineInsertionPoint && point.mOffset < static_cast(nsAccUtils::TextLength(point.mAcc))) { ++lineNumber; } return lineNumber; } bool HyperTextAccessibleBase::IsValidOffset(int32_t aOffset) { index_t offset = ConvertMagicOffset(aOffset); return offset.IsValid() && offset <= CharacterCount(); } bool HyperTextAccessibleBase::IsValidRange(int32_t aStartOffset, int32_t aEndOffset) { index_t startOffset = ConvertMagicOffset(aStartOffset); index_t endOffset = ConvertMagicOffset(aEndOffset); return startOffset.IsValid() && endOffset.IsValid() && startOffset <= endOffset && endOffset <= CharacterCount(); } uint32_t HyperTextAccessibleBase::LinkCount() { return Acc()->EmbeddedChildCount(); } Accessible* HyperTextAccessibleBase::LinkAt(uint32_t aIndex) { return Acc()->EmbeddedChildAt(aIndex); } int32_t HyperTextAccessibleBase::LinkIndexOf(Accessible* aLink) { return Acc()->IndexOfEmbeddedChild(aLink); } already_AddRefed HyperTextAccessibleBase::TextAttributes( bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset, int32_t* aEndOffset) { *aStartOffset = *aEndOffset = 0; index_t offset = ConvertMagicOffset(aOffset); if (!offset.IsValid() || offset > CharacterCount()) { NS_ERROR("Wrong in offset!"); return RefPtr{new AccAttributes()}.forget(); } Accessible* originAcc = GetChildAtOffset(offset); if (!originAcc) { // Offset 0 is correct offset when accessible has empty text. Include // default attributes if they were requested, otherwise return empty set. if (offset == 0) { if (aIncludeDefAttrs) { return DefaultTextAttributes(); } } return RefPtr{new AccAttributes()}.forget(); } if (!originAcc->IsText()) { // This is an embedded object. One or more consecutive embedded objects // form a single attrs run with no attributes. *aStartOffset = aOffset; *aEndOffset = aOffset + 1; Accessible* parent = originAcc->Parent(); if (!parent) { return RefPtr{new AccAttributes()}.forget(); } int32_t originIdx = originAcc->IndexInParent(); if (originIdx > 0) { // Check for embedded objects before the origin. for (uint32_t idx = originIdx - 1;; --idx) { Accessible* sibling = parent->ChildAt(idx); if (sibling->IsText()) { break; } --*aStartOffset; if (idx == 0) { break; } } } // Check for embedded objects after the origin. for (uint32_t idx = originIdx + 1;; ++idx) { Accessible* sibling = parent->ChildAt(idx); if (!sibling || sibling->IsText()) { break; } ++*aEndOffset; } return RefPtr{new AccAttributes()}.forget(); } TextLeafPoint origin = ToTextLeafPoint(static_cast(offset)); TextLeafPoint start = origin.FindTextAttrsStart(eDirPrevious, /* aIncludeOrigin */ true); bool ok; std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, /* aIsEndOffset */ false); TextLeafPoint end = origin.FindTextAttrsStart(eDirNext, /* aIncludeOrigin */ false); std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, /* aIsEndOffset */ true); return origin.GetTextAttributes(aIncludeDefAttrs); } void HyperTextAccessibleBase::CroppedSelectionRanges( nsTArray& aRanges) const { SelectionRanges(&aRanges); Accessible* acc = const_cast(Acc()); size_t startIndex = 0; size_t endIndex = aRanges.Length(); // If this is the document, it contains all ranges, so there's no need to // search for overlapping ranges. if (!acc->IsDoc()) { // Find overlapping ranges. We use binary searches here, which is far more // efficient than cropping every range in the list. // Find the first range that ends after acc starts. TextPoint thisPoint = TextPoint(acc, 0); startIndex = UpperBound(aRanges, 0, aRanges.Length(), [&](const TextRange& range) { return thisPoint.Compare(range.EndPoint()); }); // Find the first range that starts after acc ends. This will be the first // non-overlapping range after startIndex; i.e. the exclusive end of our // desired span. thisPoint = TextPoint(acc, CharacterCount()); endIndex = UpperBound(aRanges, startIndex, aRanges.Length(), [&](const TextRange& range) { return thisPoint.Compare(range.StartPoint()); }); } // Exclude ranges that don't overlap acc. size_t nextIndex = 0; aRanges.RemoveElementsBy([&](TextRange& range) { // Set index to the index of range. Increment nextIndex ready for the next // iteration. size_t index = nextIndex++; if (range.StartPoint() == range.EndPoint()) { return true; // Collapsed, so remove this range. } // Remove (return true for) ranges that aren't between startIndex // (inclusive) and endIndex (exclusive). if (index < startIndex || index >= endIndex) { return true; } // The first and last ranges might extend beyond acc, so crop them. if (index == startIndex || index == endIndex - 1) { DebugOnly cropped = range.Crop(const_cast(acc)); MOZ_ASSERT(cropped, "range should overlap and thus crop successfully"); } return false; }); } int32_t HyperTextAccessibleBase::SelectionCount() { nsTArray ranges; CroppedSelectionRanges(ranges); return static_cast(ranges.Length()); } bool HyperTextAccessibleBase::SelectionBoundsAt(int32_t aSelectionNum, int32_t* aStartOffset, int32_t* aEndOffset) { nsTArray ranges; CroppedSelectionRanges(ranges); if (aSelectionNum >= static_cast(ranges.Length())) { return false; } TextRange& range = ranges[aSelectionNum]; Accessible* thisAcc = Acc(); if (range.StartContainer() == thisAcc) { *aStartOffset = range.StartOffset(); } else { bool ok; // range.StartContainer() isn't a text leaf, so don't use its offset. std::tie(ok, *aStartOffset) = TransformOffset(range.StartContainer(), 0, /* aDescendToEnd */ false); } if (range.EndContainer() == thisAcc) { *aEndOffset = range.EndOffset(); } else { bool ok; // range.EndContainer() isn't a text leaf, so don't use its offset. If // range.EndOffset() is > 0, we want to include this container, so pas // offset 1. std::tie(ok, *aEndOffset) = TransformOffset(range.EndContainer(), range.EndOffset() == 0 ? 0 : 1, /* aDescendToEnd */ true); } return true; } bool HyperTextAccessibleBase::SetSelectionBoundsAt(int32_t aSelectionNum, int32_t aStartOffset, int32_t aEndOffset) { TextLeafRange range(ToTextLeafPoint(aStartOffset), ToTextLeafPoint(aEndOffset, true)); if (!range) { NS_ERROR("Wrong in offset"); return false; } return range.SetSelection(aSelectionNum); } void HyperTextAccessibleBase::ScrollSubstringTo(int32_t aStartOffset, int32_t aEndOffset, uint32_t aScrollType) { TextLeafRange range(ToTextLeafPoint(aStartOffset), ToTextLeafPoint(aEndOffset, true)); range.ScrollIntoView(aScrollType); } } // namespace mozilla::a11y