2021-09-23 14:38:09 +03:00
|
|
|
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
|
|
/* vim: set ts=2 et sw=2 tw=80: */
|
|
|
|
/* 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 "TextLeafRange.h"
|
|
|
|
|
2022-02-11 09:27:02 +03:00
|
|
|
#include "HyperTextAccessible-inl.h"
|
2021-09-23 14:38:09 +03:00
|
|
|
#include "mozilla/a11y/Accessible.h"
|
2022-05-27 13:56:41 +03:00
|
|
|
#include "mozilla/a11y/CacheConstants.h"
|
2021-09-23 14:38:09 +03:00
|
|
|
#include "mozilla/a11y/DocAccessible.h"
|
2021-10-06 07:53:54 +03:00
|
|
|
#include "mozilla/a11y/DocAccessibleParent.h"
|
2021-09-23 14:38:09 +03:00
|
|
|
#include "mozilla/a11y/LocalAccessible.h"
|
2021-10-06 07:53:54 +03:00
|
|
|
#include "mozilla/BinarySearch.h"
|
2021-11-09 04:14:15 +03:00
|
|
|
#include "mozilla/Casting.h"
|
2022-05-27 13:56:40 +03:00
|
|
|
#include "mozilla/dom/CharacterData.h"
|
2022-05-27 13:56:41 +03:00
|
|
|
#include "mozilla/dom/Document.h"
|
2022-08-18 20:10:43 +03:00
|
|
|
#include "mozilla/dom/HTMLInputElement.h"
|
2023-03-09 22:59:00 +03:00
|
|
|
#include "mozilla/PresShell.h"
|
2021-11-09 04:14:15 +03:00
|
|
|
#include "mozilla/intl/Segmenter.h"
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
#include "mozilla/intl/WordBreaker.h"
|
|
|
|
#include "mozilla/StaticPrefs_layout.h"
|
2023-01-27 23:15:11 +03:00
|
|
|
#include "mozilla/TextEditor.h"
|
2022-05-27 13:56:41 +03:00
|
|
|
#include "nsAccessibilityService.h"
|
2021-09-23 14:38:09 +03:00
|
|
|
#include "nsAccUtils.h"
|
2022-05-27 04:22:07 +03:00
|
|
|
#include "nsBlockFrame.h"
|
2021-09-23 14:38:09 +03:00
|
|
|
#include "nsContentUtils.h"
|
2022-05-27 13:56:40 +03:00
|
|
|
#include "nsFrameSelection.h"
|
2021-09-23 14:38:09 +03:00
|
|
|
#include "nsIAccessiblePivot.h"
|
|
|
|
#include "nsILineIterator.h"
|
2022-05-27 13:56:40 +03:00
|
|
|
#include "nsINode.h"
|
2022-05-27 13:56:41 +03:00
|
|
|
#include "nsRange.h"
|
2022-05-04 07:18:11 +03:00
|
|
|
#include "nsStyleStructInlines.h"
|
2021-09-23 14:38:09 +03:00
|
|
|
#include "nsTArray.h"
|
2021-09-23 14:38:09 +03:00
|
|
|
#include "nsTextFrame.h"
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
#include "nsUnicodeProperties.h"
|
2021-09-23 14:38:09 +03:00
|
|
|
#include "Pivot.h"
|
2021-11-02 02:27:37 +03:00
|
|
|
#include "TextAttrs.h"
|
2021-09-23 14:38:09 +03:00
|
|
|
|
2021-10-25 22:00:22 +03:00
|
|
|
using mozilla::intl::WordBreaker;
|
|
|
|
|
2021-09-23 14:38:09 +03:00
|
|
|
namespace mozilla::a11y {
|
|
|
|
|
2021-09-23 14:38:09 +03:00
|
|
|
/*** Helpers ***/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* These two functions convert between rendered and content text offsets.
|
|
|
|
* When text DOM nodes are rendered, the rendered text often does not contain
|
|
|
|
* all the whitespace from the source. For example, by default, the text
|
|
|
|
* "a b" will be rendered as "a b"; i.e. multiple spaces are compressed to
|
|
|
|
* one. TextLeafAccessibles contain rendered text, but when we query layout, we
|
|
|
|
* need to provide offsets into the original content text. Similarly, layout
|
|
|
|
* returns content offsets, but we need to convert them to rendered offsets to
|
|
|
|
* map them to TextLeafAccessibles.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int32_t RenderedToContentOffset(LocalAccessible* aAcc,
|
|
|
|
uint32_t aRenderedOffset) {
|
2022-05-04 07:18:11 +03:00
|
|
|
nsTextFrame* frame = do_QueryFrame(aAcc->GetFrame());
|
2022-06-12 09:59:18 +03:00
|
|
|
if (!frame) {
|
2023-01-27 23:15:11 +03:00
|
|
|
MOZ_ASSERT(!aAcc->HasOwnContent() || aAcc->IsHTMLBr(),
|
|
|
|
"No text frame because this is a XUL label[value] text leaf or "
|
|
|
|
"a BR element.");
|
2022-06-12 09:59:18 +03:00
|
|
|
return static_cast<int32_t>(aRenderedOffset);
|
|
|
|
}
|
|
|
|
|
2022-05-04 07:18:11 +03:00
|
|
|
if (frame->StyleText()->WhiteSpaceIsSignificant() &&
|
|
|
|
frame->StyleText()->NewlineIsSignificant(frame)) {
|
|
|
|
// Spaces and new lines aren't altered, so the content and rendered offsets
|
|
|
|
// are the same. This happens in pre-formatted text and text fields.
|
2021-09-23 14:38:09 +03:00
|
|
|
return static_cast<int32_t>(aRenderedOffset);
|
|
|
|
}
|
|
|
|
|
|
|
|
nsIFrame::RenderedText text =
|
|
|
|
frame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
|
|
|
|
nsIFrame::TextOffsetType::OffsetsInRenderedText,
|
|
|
|
nsIFrame::TrailingWhitespace::DontTrim);
|
|
|
|
return text.mOffsetWithinNodeText;
|
|
|
|
}
|
|
|
|
|
|
|
|
static uint32_t ContentToRenderedOffset(LocalAccessible* aAcc,
|
|
|
|
int32_t aContentOffset) {
|
2022-05-04 07:18:11 +03:00
|
|
|
nsTextFrame* frame = do_QueryFrame(aAcc->GetFrame());
|
2022-06-12 09:59:18 +03:00
|
|
|
if (!frame) {
|
|
|
|
MOZ_ASSERT(!aAcc->HasOwnContent(),
|
|
|
|
"No text frame because this is a XUL label[value] text leaf.");
|
|
|
|
return aContentOffset;
|
|
|
|
}
|
|
|
|
|
2022-05-04 07:18:11 +03:00
|
|
|
if (frame->StyleText()->WhiteSpaceIsSignificant() &&
|
|
|
|
frame->StyleText()->NewlineIsSignificant(frame)) {
|
|
|
|
// Spaces and new lines aren't altered, so the content and rendered offsets
|
|
|
|
// are the same. This happens in pre-formatted text and text fields.
|
2021-09-23 14:38:09 +03:00
|
|
|
return aContentOffset;
|
|
|
|
}
|
|
|
|
|
|
|
|
nsIFrame::RenderedText text =
|
|
|
|
frame->GetRenderedText(aContentOffset, aContentOffset + 1,
|
|
|
|
nsIFrame::TextOffsetType::OffsetsInContentText,
|
|
|
|
nsIFrame::TrailingWhitespace::DontTrim);
|
|
|
|
return text.mOffsetWithinNodeRenderedText;
|
|
|
|
}
|
|
|
|
|
|
|
|
class LeafRule : public PivotRule {
|
|
|
|
public:
|
2023-02-22 07:17:31 +03:00
|
|
|
explicit LeafRule(bool aIgnoreListItemMarker)
|
|
|
|
: mIgnoreListItemMarker(aIgnoreListItemMarker) {}
|
|
|
|
|
2021-09-23 14:38:09 +03:00
|
|
|
virtual uint16_t Match(Accessible* aAcc) override {
|
|
|
|
if (aAcc->IsOuterDoc()) {
|
2022-10-07 02:47:50 +03:00
|
|
|
// Treat an embedded doc as a single character in this document, but do
|
|
|
|
// not descend inside it.
|
|
|
|
return nsIAccessibleTraversalRule::FILTER_MATCH |
|
|
|
|
nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
|
2021-09-23 14:38:09 +03:00
|
|
|
}
|
2023-02-22 07:17:31 +03:00
|
|
|
|
|
|
|
if (mIgnoreListItemMarker && aAcc->Role() == roles::LISTITEM_MARKER) {
|
|
|
|
// Ignore list item markers if configured to do so.
|
|
|
|
return nsIAccessibleTraversalRule::FILTER_IGNORE;
|
|
|
|
}
|
|
|
|
|
2021-09-23 14:38:09 +03:00
|
|
|
// We deliberately include Accessibles such as empty input elements and
|
|
|
|
// empty containers, as these can be at the start of a line.
|
|
|
|
if (!aAcc->HasChildren()) {
|
|
|
|
return nsIAccessibleTraversalRule::FILTER_MATCH;
|
|
|
|
}
|
|
|
|
return nsIAccessibleTraversalRule::FILTER_IGNORE;
|
|
|
|
}
|
2023-02-22 07:17:31 +03:00
|
|
|
|
|
|
|
private:
|
|
|
|
bool mIgnoreListItemMarker;
|
2021-09-23 14:38:09 +03:00
|
|
|
};
|
|
|
|
|
2021-11-30 05:07:37 +03:00
|
|
|
static HyperTextAccessible* HyperTextFor(LocalAccessible* aAcc) {
|
|
|
|
for (LocalAccessible* acc = aAcc; acc; acc = acc->LocalParent()) {
|
|
|
|
if (HyperTextAccessible* ht = acc->AsHyperText()) {
|
|
|
|
return ht;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2023-02-22 07:17:31 +03:00
|
|
|
static Accessible* NextLeaf(Accessible* aOrigin, bool aIsEditable = false,
|
|
|
|
bool aIgnoreListItemMarker = false) {
|
2022-04-01 12:49:56 +03:00
|
|
|
MOZ_ASSERT(aOrigin);
|
|
|
|
Accessible* doc = nsAccUtils::DocumentFor(aOrigin);
|
2021-09-23 14:38:09 +03:00
|
|
|
Pivot pivot(doc);
|
2023-02-22 07:17:31 +03:00
|
|
|
auto rule = LeafRule(aIgnoreListItemMarker);
|
2022-08-26 19:26:31 +03:00
|
|
|
Accessible* leaf = pivot.Next(aOrigin, rule);
|
|
|
|
if (aIsEditable && leaf) {
|
|
|
|
return leaf->Parent() && (leaf->Parent()->State() & states::EDITABLE)
|
|
|
|
? leaf
|
|
|
|
: nullptr;
|
|
|
|
}
|
|
|
|
return leaf;
|
2021-09-23 14:38:09 +03:00
|
|
|
}
|
|
|
|
|
2023-02-22 07:17:31 +03:00
|
|
|
static Accessible* PrevLeaf(Accessible* aOrigin, bool aIsEditable = false,
|
|
|
|
bool aIgnoreListItemMarker = false) {
|
2022-04-01 12:49:56 +03:00
|
|
|
MOZ_ASSERT(aOrigin);
|
|
|
|
Accessible* doc = nsAccUtils::DocumentFor(aOrigin);
|
2021-09-23 14:38:09 +03:00
|
|
|
Pivot pivot(doc);
|
2023-02-22 07:17:31 +03:00
|
|
|
auto rule = LeafRule(aIgnoreListItemMarker);
|
2022-08-26 19:26:31 +03:00
|
|
|
Accessible* leaf = pivot.Prev(aOrigin, rule);
|
|
|
|
if (aIsEditable && leaf) {
|
|
|
|
return leaf->Parent() && (leaf->Parent()->State() & states::EDITABLE)
|
|
|
|
? leaf
|
|
|
|
: nullptr;
|
|
|
|
}
|
|
|
|
return leaf;
|
2021-09-23 14:38:09 +03:00
|
|
|
}
|
|
|
|
|
2022-08-18 20:10:43 +03:00
|
|
|
static nsIFrame* GetFrameInBlock(const LocalAccessible* aAcc) {
|
|
|
|
dom::HTMLInputElement* input =
|
|
|
|
dom::HTMLInputElement::FromNodeOrNull(aAcc->GetContent());
|
|
|
|
if (!input) {
|
|
|
|
if (LocalAccessible* parent = aAcc->LocalParent()) {
|
|
|
|
input = dom::HTMLInputElement::FromNodeOrNull(parent->GetContent());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input) {
|
|
|
|
// If this is a single line input (or a leaf of an input) we want to return
|
|
|
|
// the top frame of the input element and not the text leaf's frame because
|
|
|
|
// the leaf may be inside of an embedded block frame in the input's shadow
|
|
|
|
// DOM that we aren't interested in.
|
|
|
|
return input->GetPrimaryFrame();
|
|
|
|
}
|
|
|
|
|
|
|
|
return aAcc->GetFrame();
|
|
|
|
}
|
|
|
|
|
2021-09-23 14:38:09 +03:00
|
|
|
static bool IsLocalAccAtLineStart(LocalAccessible* aAcc) {
|
|
|
|
if (aAcc->NativeRole() == roles::LISTITEM_MARKER) {
|
|
|
|
// A bullet always starts a line.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// Splitting of content across lines is handled by layout.
|
|
|
|
// nsIFrame::IsLogicallyAtLineEdge queries whether a frame is the first frame
|
|
|
|
// on its line. However, we can't use that because the first frame on a line
|
|
|
|
// might not be included in the a11y tree; e.g. an empty span, or space
|
|
|
|
// in the DOM after a line break which is stripped when rendered. Instead, we
|
|
|
|
// get the line number for this Accessible's frame and the line number for the
|
|
|
|
// previous leaf Accessible's frame and compare them.
|
|
|
|
Accessible* prev = PrevLeaf(aAcc);
|
|
|
|
LocalAccessible* prevLocal = prev ? prev->AsLocal() : nullptr;
|
|
|
|
if (!prevLocal) {
|
|
|
|
// There's nothing before us, so this is the start of the first line.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (prevLocal->NativeRole() == roles::LISTITEM_MARKER) {
|
|
|
|
// If there is a bullet immediately before us and we're inside the same
|
|
|
|
// list item, this is not the start of a line.
|
|
|
|
LocalAccessible* listItem = prevLocal->LocalParent();
|
|
|
|
MOZ_ASSERT(listItem);
|
|
|
|
LocalAccessible* doc = listItem->Document();
|
|
|
|
MOZ_ASSERT(doc);
|
|
|
|
for (LocalAccessible* parent = aAcc->LocalParent(); parent && parent != doc;
|
|
|
|
parent = parent->LocalParent()) {
|
|
|
|
if (parent == listItem) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-18 20:10:43 +03:00
|
|
|
|
|
|
|
nsIFrame* thisFrame = GetFrameInBlock(aAcc);
|
2021-09-23 14:38:09 +03:00
|
|
|
if (!thisFrame) {
|
|
|
|
return false;
|
|
|
|
}
|
2022-08-18 20:10:43 +03:00
|
|
|
|
|
|
|
nsIFrame* prevFrame = GetFrameInBlock(prevLocal);
|
2021-09-23 14:38:09 +03:00
|
|
|
if (!prevFrame) {
|
|
|
|
return false;
|
|
|
|
}
|
2022-08-18 20:10:43 +03:00
|
|
|
|
2021-09-25 12:13:02 +03:00
|
|
|
auto [thisBlock, thisLineFrame] = thisFrame->GetContainingBlockForLine(
|
|
|
|
/* aLockScroll */ false);
|
2021-09-23 14:38:09 +03:00
|
|
|
if (!thisBlock) {
|
|
|
|
// We couldn't get the containing block for this frame. In that case, we
|
|
|
|
// play it safe and assume this is the beginning of a new line.
|
|
|
|
return true;
|
|
|
|
}
|
2022-08-18 20:10:43 +03:00
|
|
|
|
2022-04-28 05:09:16 +03:00
|
|
|
// The previous leaf might cross lines. We want to compare against the last
|
|
|
|
// line.
|
|
|
|
prevFrame = prevFrame->LastContinuation();
|
2021-09-25 12:13:02 +03:00
|
|
|
auto [prevBlock, prevLineFrame] = prevFrame->GetContainingBlockForLine(
|
|
|
|
/* aLockScroll */ false);
|
2021-09-23 14:38:09 +03:00
|
|
|
if (thisBlock != prevBlock) {
|
|
|
|
// If the blocks are different, that means there's nothing before us on the
|
|
|
|
// same line, so we're at the start.
|
|
|
|
return true;
|
|
|
|
}
|
2022-05-27 04:22:07 +03:00
|
|
|
if (nsBlockFrame* block = do_QueryFrame(thisBlock)) {
|
|
|
|
// If we have a block frame, it's faster for us to use
|
|
|
|
// BlockInFlowLineIterator because it uses the line cursor.
|
|
|
|
bool found = false;
|
|
|
|
block->SetupLineCursorForQuery();
|
|
|
|
nsBlockInFlowLineIterator prevIt(block, prevLineFrame, &found);
|
|
|
|
if (!found) {
|
|
|
|
// Error; play it safe.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
found = false;
|
|
|
|
nsBlockInFlowLineIterator thisIt(block, thisLineFrame, &found);
|
|
|
|
// if the lines are different, that means there's nothing before us on the
|
|
|
|
// same line, so we're at the start.
|
|
|
|
return !found || prevIt.GetLine() != thisIt.GetLine();
|
|
|
|
}
|
2022-06-04 01:05:36 +03:00
|
|
|
AutoAssertNoDomMutations guard;
|
|
|
|
nsILineIterator* it = prevBlock->GetLineIterator();
|
2021-09-24 17:20:05 +03:00
|
|
|
MOZ_ASSERT(it, "GetLineIterator impl in line-container blocks is infallible");
|
2021-09-27 11:35:35 +03:00
|
|
|
int32_t prevLineNum = it->FindLineContaining(prevLineFrame);
|
|
|
|
if (prevLineNum < 0) {
|
2021-09-23 14:38:09 +03:00
|
|
|
// Error; play it safe.
|
|
|
|
return true;
|
|
|
|
}
|
2021-09-27 11:35:35 +03:00
|
|
|
int32_t thisLineNum = it->FindLineContaining(thisLineFrame, prevLineNum);
|
2021-09-23 14:38:09 +03:00
|
|
|
// if the blocks and line numbers are different, that means there's nothing
|
|
|
|
// before us on the same line, so we're at the start.
|
|
|
|
return thisLineNum != prevLineNum;
|
|
|
|
}
|
|
|
|
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
/**
|
|
|
|
* There are many kinds of word break, but we only need to treat punctuation and
|
|
|
|
* space specially.
|
|
|
|
*/
|
|
|
|
enum WordBreakClass { eWbcSpace = 0, eWbcPunct, eWbcOther };
|
|
|
|
|
2022-02-11 09:27:03 +03:00
|
|
|
static WordBreakClass GetWordBreakClass(char16_t aChar) {
|
|
|
|
// Based on IsSelectionInlineWhitespace and IsSelectionNewline in
|
|
|
|
// layout/generic/nsTextFrame.cpp.
|
|
|
|
const char16_t kCharNbsp = 0xA0;
|
|
|
|
switch (aChar) {
|
|
|
|
case ' ':
|
|
|
|
case kCharNbsp:
|
|
|
|
case '\t':
|
|
|
|
case '\f':
|
|
|
|
case '\n':
|
|
|
|
case '\r':
|
|
|
|
return eWbcSpace;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// Based on ClusterIterator::IsPunctuation in
|
|
|
|
// layout/generic/nsTextFrame.cpp.
|
|
|
|
uint8_t cat = unicode::GetGeneralCategory(aChar);
|
|
|
|
switch (cat) {
|
|
|
|
case HB_UNICODE_GENERAL_CATEGORY_CONNECT_PUNCTUATION: /* Pc */
|
|
|
|
if (aChar == '_' &&
|
|
|
|
!StaticPrefs::layout_word_select_stop_at_underscore()) {
|
|
|
|
return eWbcOther;
|
|
|
|
}
|
|
|
|
[[fallthrough]];
|
|
|
|
case HB_UNICODE_GENERAL_CATEGORY_DASH_PUNCTUATION: /* Pd */
|
|
|
|
case HB_UNICODE_GENERAL_CATEGORY_CLOSE_PUNCTUATION: /* Pe */
|
|
|
|
case HB_UNICODE_GENERAL_CATEGORY_FINAL_PUNCTUATION: /* Pf */
|
|
|
|
case HB_UNICODE_GENERAL_CATEGORY_INITIAL_PUNCTUATION: /* Pi */
|
|
|
|
case HB_UNICODE_GENERAL_CATEGORY_OTHER_PUNCTUATION: /* Po */
|
|
|
|
case HB_UNICODE_GENERAL_CATEGORY_OPEN_PUNCTUATION: /* Ps */
|
|
|
|
case HB_UNICODE_GENERAL_CATEGORY_CURRENCY_SYMBOL: /* Sc */
|
|
|
|
case HB_UNICODE_GENERAL_CATEGORY_MATH_SYMBOL: /* Sm */
|
|
|
|
case HB_UNICODE_GENERAL_CATEGORY_OTHER_SYMBOL: /* So */
|
|
|
|
return eWbcPunct;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return eWbcOther;
|
|
|
|
}
|
|
|
|
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
/**
|
|
|
|
* Words can cross Accessibles. To work out whether we're at the start of a
|
|
|
|
* word, we might have to check the previous leaf. This class handles querying
|
|
|
|
* the previous WordBreakClass, crossing Accessibles if necessary.
|
|
|
|
*/
|
|
|
|
class PrevWordBreakClassWalker {
|
|
|
|
public:
|
|
|
|
PrevWordBreakClassWalker(Accessible* aAcc, const nsAString& aText,
|
|
|
|
int32_t aOffset)
|
|
|
|
: mAcc(aAcc), mText(aText), mOffset(aOffset) {
|
2022-02-11 09:27:03 +03:00
|
|
|
mClass = GetWordBreakClass(mText.CharAt(mOffset));
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
WordBreakClass CurClass() { return mClass; }
|
|
|
|
|
|
|
|
Maybe<WordBreakClass> PrevClass() {
|
|
|
|
for (;;) {
|
|
|
|
if (!PrevChar()) {
|
|
|
|
return Nothing();
|
|
|
|
}
|
2022-02-11 09:27:03 +03:00
|
|
|
WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset));
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
if (curClass != mClass) {
|
|
|
|
mClass = curClass;
|
|
|
|
return Some(curClass);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
MOZ_ASSERT_UNREACHABLE();
|
|
|
|
return Nothing();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool IsStartOfGroup() {
|
2021-10-27 01:25:29 +03:00
|
|
|
if (!PrevChar()) {
|
|
|
|
// There are no characters before us.
|
|
|
|
return true;
|
|
|
|
}
|
2022-02-11 09:27:03 +03:00
|
|
|
WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset));
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
// We wanted to peek at the previous character, not really move to it.
|
|
|
|
++mOffset;
|
|
|
|
return curClass != mClass;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
bool PrevChar() {
|
|
|
|
if (mOffset > 0) {
|
|
|
|
--mOffset;
|
|
|
|
return true;
|
|
|
|
}
|
2021-10-27 01:25:29 +03:00
|
|
|
if (!mAcc) {
|
|
|
|
// PrevChar was called already and failed.
|
|
|
|
return false;
|
|
|
|
}
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
mAcc = PrevLeaf(mAcc);
|
|
|
|
if (!mAcc) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
mText.Truncate();
|
2021-10-06 07:53:54 +03:00
|
|
|
mAcc->AppendTextTo(mText);
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
mOffset = static_cast<int32_t>(mText.Length()) - 1;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
Accessible* mAcc;
|
|
|
|
nsAutoString mText;
|
|
|
|
int32_t mOffset;
|
|
|
|
WordBreakClass mClass;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* WordBreaker breaks at all space, punctuation, etc. We want to emulate
|
|
|
|
* layout, so that's not what we want. This function determines whether this
|
|
|
|
* is acceptable as the start of a word for our purposes.
|
|
|
|
*/
|
2021-10-22 21:41:45 +03:00
|
|
|
static bool IsAcceptableWordStart(Accessible* aAcc, const nsAutoString& aText,
|
|
|
|
int32_t aOffset) {
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
PrevWordBreakClassWalker walker(aAcc, aText, aOffset);
|
|
|
|
if (!walker.IsStartOfGroup()) {
|
|
|
|
// If we're not at the start of a WordBreaker group, this can't be the
|
|
|
|
// start of a word.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
WordBreakClass curClass = walker.CurClass();
|
|
|
|
if (curClass == eWbcSpace) {
|
|
|
|
// Space isn't the start of a word.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
Maybe<WordBreakClass> prevClass = walker.PrevClass();
|
|
|
|
if (curClass == eWbcPunct && (!prevClass || prevClass.value() != eWbcSpace)) {
|
|
|
|
// Punctuation isn't the start of a word (unless it is after space).
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (!prevClass || prevClass.value() != eWbcPunct) {
|
|
|
|
// If there's nothing before this or the group before this isn't
|
|
|
|
// punctuation, this is the start of a word.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// At this point, we know the group before this is punctuation.
|
|
|
|
if (!StaticPrefs::layout_word_select_stop_at_punctuation()) {
|
|
|
|
// When layout.word_select.stop_at_punctuation is false (defaults to true),
|
|
|
|
// if there is punctuation before this, this is not the start of a word.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
Maybe<WordBreakClass> prevPrevClass = walker.PrevClass();
|
|
|
|
if (!prevPrevClass || prevPrevClass.value() == eWbcSpace) {
|
|
|
|
// If there is punctuation before this and space (or nothing) before the
|
|
|
|
// punctuation, this is not the start of a word.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-02-18 14:41:41 +03:00
|
|
|
class BlockRule : public PivotRule {
|
|
|
|
public:
|
|
|
|
virtual uint16_t Match(Accessible* aAcc) override {
|
|
|
|
if (RefPtr<nsAtom>(aAcc->DisplayStyle()) == nsGkAtoms::block ||
|
2023-03-10 06:48:34 +03:00
|
|
|
aAcc->IsHTMLListItem() || aAcc->IsTableRow() || aAcc->IsTableCell()) {
|
2022-02-18 14:41:41 +03:00
|
|
|
return nsIAccessibleTraversalRule::FILTER_MATCH;
|
|
|
|
}
|
|
|
|
return nsIAccessibleTraversalRule::FILTER_IGNORE;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-05-27 13:56:40 +03:00
|
|
|
/**
|
|
|
|
* Find spelling error DOM ranges overlapping the requested LocalAccessible and
|
|
|
|
* offsets. This includes ranges that begin or end outside of the given
|
|
|
|
* LocalAccessible. Note that the offset arguments are rendered offsets, but
|
|
|
|
* because the returned ranges are DOM ranges, those offsets are content
|
|
|
|
* offsets. See the documentation for dom::Selection::GetRangesForIntervalArray
|
|
|
|
* for information about the aAllowAdjacent argument.
|
|
|
|
*/
|
|
|
|
static nsTArray<nsRange*> FindDOMSpellingErrors(LocalAccessible* aAcc,
|
|
|
|
int32_t aRenderedStart,
|
|
|
|
int32_t aRenderedEnd,
|
|
|
|
bool aAllowAdjacent = false) {
|
2022-06-12 09:59:18 +03:00
|
|
|
if (!aAcc->IsTextLeaf() || !aAcc->HasOwnContent()) {
|
2022-05-27 13:56:40 +03:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
nsIFrame* frame = aAcc->GetFrame();
|
|
|
|
RefPtr<nsFrameSelection> frameSel =
|
|
|
|
frame ? frame->GetFrameSelection() : nullptr;
|
|
|
|
dom::Selection* domSel =
|
|
|
|
frameSel ? frameSel->GetSelection(SelectionType::eSpellCheck) : nullptr;
|
|
|
|
if (!domSel) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
nsINode* node = aAcc->GetNode();
|
|
|
|
uint32_t contentStart = RenderedToContentOffset(aAcc, aRenderedStart);
|
|
|
|
uint32_t contentEnd =
|
|
|
|
aRenderedEnd == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
|
|
|
|
? dom::CharacterData::FromNode(node)->TextLength()
|
|
|
|
: RenderedToContentOffset(aAcc, aRenderedEnd);
|
|
|
|
nsTArray<nsRange*> domRanges;
|
2023-03-03 17:59:47 +03:00
|
|
|
domSel->GetDynamicRangesForIntervalArray(node, contentStart, node, contentEnd,
|
|
|
|
aAllowAdjacent, &domRanges);
|
2022-05-27 13:56:40 +03:00
|
|
|
return domRanges;
|
|
|
|
}
|
|
|
|
|
2023-01-27 23:15:11 +03:00
|
|
|
/**
|
|
|
|
* Given two DOM nodes get DOM Selection object that is common
|
|
|
|
* to both of them.
|
|
|
|
*/
|
|
|
|
static dom::Selection* GetDOMSelection(const nsIContent* aStartContent,
|
|
|
|
const nsIContent* aEndContent) {
|
|
|
|
nsIFrame* startFrame = aStartContent->GetPrimaryFrame();
|
|
|
|
const nsFrameSelection* startFrameSel =
|
|
|
|
startFrame ? startFrame->GetConstFrameSelection() : nullptr;
|
|
|
|
nsIFrame* endFrame = aEndContent->GetPrimaryFrame();
|
|
|
|
const nsFrameSelection* endFrameSel =
|
|
|
|
endFrame ? endFrame->GetConstFrameSelection() : nullptr;
|
|
|
|
|
|
|
|
if (startFrameSel != endFrameSel) {
|
|
|
|
// Start and end point don't share the same selection state.
|
|
|
|
// This could happen when both points aren't in the same editable.
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
return startFrameSel ? startFrameSel->GetSelection(SelectionType::eNormal)
|
|
|
|
: nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Translate given TextLeafPoint into a DOM point that is kosher
|
|
|
|
* for text selection.
|
|
|
|
*/
|
|
|
|
MOZ_CAN_RUN_SCRIPT static std::pair<nsIContent*, int32_t> DOMPointForSelection(
|
|
|
|
const TextLeafPoint& aPoint) {
|
|
|
|
if (!aPoint || !aPoint.mAcc->IsLocal()) {
|
|
|
|
MOZ_ASSERT_UNREACHABLE("Invalid point");
|
|
|
|
return {nullptr, 0};
|
|
|
|
}
|
|
|
|
|
|
|
|
nsIContent* content = aPoint.mAcc->AsLocal()->GetContent();
|
|
|
|
nsIFrame* frame = content ? content->GetPrimaryFrame() : nullptr;
|
|
|
|
MOZ_ASSERT(frame);
|
|
|
|
|
|
|
|
if (frame && frame->IsGeneratedContentFrame()) {
|
|
|
|
// List markers accessibles represent the generated content element,
|
|
|
|
// before/after text accessibles represent the child text nodes.
|
|
|
|
auto generatedElement = content->IsGeneratedContentContainerForMarker()
|
|
|
|
? content
|
|
|
|
: content->GetParentElement();
|
|
|
|
auto parent = generatedElement ? generatedElement->GetParent() : nullptr;
|
|
|
|
MOZ_ASSERT(parent);
|
|
|
|
if (parent) {
|
|
|
|
if (generatedElement->IsGeneratedContentContainerForAfter()) {
|
|
|
|
// Use the end offset of the parent element for trailing generated
|
|
|
|
// content.
|
|
|
|
return {parent, parent->GetChildCount()};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (generatedElement->IsGeneratedContentContainerForBefore() ||
|
|
|
|
generatedElement->IsGeneratedContentContainerForMarker()) {
|
|
|
|
// Use the start offset of the parent element for leading generated
|
|
|
|
// content.
|
|
|
|
return {parent, 0};
|
|
|
|
}
|
|
|
|
|
|
|
|
MOZ_ASSERT_UNREACHABLE("Unknown generated content type!");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!aPoint.mAcc->IsTextLeaf() && !aPoint.mAcc->IsHTMLBr() &&
|
|
|
|
!aPoint.mAcc->HasChildren()) {
|
|
|
|
// If this is not a text leaf it can be an empty editable container,
|
|
|
|
// whitespace, or an empty doc. In any case, the offset inside should be 0.
|
|
|
|
MOZ_ASSERT(aPoint.mOffset == 0);
|
|
|
|
|
|
|
|
if (RefPtr<TextControlElement> textControlElement =
|
|
|
|
TextControlElement::FromNodeOrNull(content)) {
|
|
|
|
// This is an empty input, use the shadow root's element.
|
|
|
|
if (RefPtr<TextEditor> textEditor = textControlElement->GetTextEditor()) {
|
|
|
|
if (textEditor->IsEmpty()) {
|
|
|
|
MOZ_ASSERT(aPoint.mOffset == 0);
|
|
|
|
return {textEditor->GetRoot(), 0};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {content, 0};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {content,
|
|
|
|
RenderedToContentOffset(aPoint.mAcc->AsLocal(), aPoint.mOffset)};
|
|
|
|
}
|
|
|
|
|
2021-09-23 14:38:09 +03:00
|
|
|
/*** TextLeafPoint ***/
|
|
|
|
|
|
|
|
TextLeafPoint::TextLeafPoint(Accessible* aAcc, int32_t aOffset) {
|
2022-10-07 02:47:50 +03:00
|
|
|
// Even though an OuterDoc contains a document, we treat it as a leaf because
|
|
|
|
// we don't want to move into another document.
|
|
|
|
if (aOffset != nsIAccessibleText::TEXT_OFFSET_CARET && !aAcc->IsOuterDoc() &&
|
|
|
|
aAcc->HasChildren()) {
|
2021-09-23 14:38:09 +03:00
|
|
|
// Find a leaf. This might not necessarily be a TextLeafAccessible; it
|
|
|
|
// could be an empty container.
|
2022-08-02 19:29:07 +03:00
|
|
|
auto GetChild = [&aOffset](Accessible* acc) -> Accessible* {
|
2022-10-07 02:47:50 +03:00
|
|
|
if (acc->IsOuterDoc()) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
2022-08-02 19:29:07 +03:00
|
|
|
return aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
|
|
|
|
? acc->FirstChild()
|
|
|
|
: acc->LastChild();
|
|
|
|
};
|
|
|
|
|
|
|
|
for (Accessible* acc = GetChild(aAcc); acc; acc = GetChild(acc)) {
|
2021-09-23 14:38:09 +03:00
|
|
|
mAcc = acc;
|
|
|
|
}
|
2022-08-02 19:29:07 +03:00
|
|
|
mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
|
|
|
|
? 0
|
|
|
|
: nsAccUtils::TextLength(mAcc);
|
2021-09-23 14:38:09 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
mAcc = aAcc;
|
2022-08-02 19:29:07 +03:00
|
|
|
mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
|
|
|
|
? aOffset
|
|
|
|
: nsAccUtils::TextLength(mAcc);
|
2021-09-23 14:38:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
bool TextLeafPoint::operator<(const TextLeafPoint& aPoint) const {
|
|
|
|
if (mAcc == aPoint.mAcc) {
|
|
|
|
return mOffset < aPoint.mOffset;
|
|
|
|
}
|
2022-02-18 12:49:02 +03:00
|
|
|
return mAcc->IsBefore(aPoint.mAcc);
|
2021-09-23 14:38:09 +03:00
|
|
|
}
|
|
|
|
|
2022-05-09 23:30:27 +03:00
|
|
|
bool TextLeafPoint::operator<=(const TextLeafPoint& aPoint) const {
|
|
|
|
return *this == aPoint || *this < aPoint;
|
|
|
|
}
|
|
|
|
|
2023-02-21 19:37:24 +03:00
|
|
|
bool TextLeafPoint::IsDocEdge(nsDirection aDirection) const {
|
|
|
|
if (aDirection == eDirPrevious) {
|
|
|
|
return mOffset == 0 && !PrevLeaf(mAcc);
|
|
|
|
}
|
|
|
|
|
|
|
|
return mOffset == static_cast<int32_t>(nsAccUtils::TextLength(mAcc)) &&
|
|
|
|
!NextLeaf(mAcc);
|
|
|
|
}
|
|
|
|
|
2023-02-22 07:17:31 +03:00
|
|
|
bool TextLeafPoint::IsLeafAfterListItemMarker() const {
|
|
|
|
Accessible* prev = PrevLeaf(mAcc);
|
|
|
|
return prev && prev->Role() == roles::LISTITEM_MARKER &&
|
|
|
|
prev->Parent()->IsAncestorOf(mAcc);
|
|
|
|
}
|
|
|
|
|
2021-09-23 14:38:09 +03:00
|
|
|
bool TextLeafPoint::IsEmptyLastLine() const {
|
|
|
|
if (mAcc->IsHTMLBr() && mOffset == 1) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (!mAcc->IsTextLeaf()) {
|
|
|
|
return false;
|
|
|
|
}
|
2021-10-06 07:53:54 +03:00
|
|
|
if (mOffset < static_cast<int32_t>(nsAccUtils::TextLength(mAcc))) {
|
2021-09-23 14:38:09 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
nsAutoString text;
|
2021-10-06 07:53:54 +03:00
|
|
|
mAcc->AppendTextTo(text, mOffset - 1, 1);
|
2021-09-23 14:38:09 +03:00
|
|
|
return text.CharAt(0) == '\n';
|
|
|
|
}
|
|
|
|
|
2022-02-11 09:27:03 +03:00
|
|
|
char16_t TextLeafPoint::GetChar() const {
|
|
|
|
nsAutoString text;
|
|
|
|
mAcc->AppendTextTo(text, mOffset, 1);
|
|
|
|
return text.CharAt(0);
|
|
|
|
}
|
|
|
|
|
2021-09-23 14:38:09 +03:00
|
|
|
TextLeafPoint TextLeafPoint::FindPrevLineStartSameLocalAcc(
|
|
|
|
bool aIncludeOrigin) const {
|
|
|
|
LocalAccessible* acc = mAcc->AsLocal();
|
|
|
|
MOZ_ASSERT(acc);
|
|
|
|
if (mOffset == 0) {
|
|
|
|
if (aIncludeOrigin && IsLocalAccAtLineStart(acc)) {
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
nsIFrame* frame = acc->GetFrame();
|
|
|
|
if (!frame) {
|
2021-10-14 04:53:47 +03:00
|
|
|
// This can happen if this is an empty element with display: contents. In
|
|
|
|
// that case, this Accessible contains no lines.
|
2021-09-23 14:38:09 +03:00
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
if (!frame->IsTextFrame()) {
|
|
|
|
if (IsLocalAccAtLineStart(acc)) {
|
|
|
|
return TextLeafPoint(acc, 0);
|
|
|
|
}
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
// Each line of a text node is rendered as a continuation frame. Get the
|
|
|
|
// continuation containing the origin.
|
|
|
|
int32_t origOffset = mOffset;
|
|
|
|
origOffset = RenderedToContentOffset(acc, origOffset);
|
|
|
|
nsTextFrame* continuation = nullptr;
|
|
|
|
int32_t unusedOffsetInContinuation = 0;
|
|
|
|
frame->GetChildFrameContainingOffset(
|
|
|
|
origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation);
|
|
|
|
MOZ_ASSERT(continuation);
|
|
|
|
int32_t lineStart = continuation->GetContentOffset();
|
|
|
|
if (!aIncludeOrigin && lineStart > 0 && lineStart == origOffset) {
|
|
|
|
// A line starts at the origin, but the caller doesn't want this included.
|
|
|
|
// Go back one more.
|
|
|
|
continuation = continuation->GetPrevContinuation();
|
|
|
|
MOZ_ASSERT(continuation);
|
|
|
|
lineStart = continuation->GetContentOffset();
|
|
|
|
}
|
|
|
|
MOZ_ASSERT(lineStart >= 0);
|
|
|
|
if (lineStart == 0 && !IsLocalAccAtLineStart(acc)) {
|
|
|
|
// This is the first line of this text node, but there is something else
|
|
|
|
// on the same line before this text node, so don't return this as a line
|
|
|
|
// start.
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart));
|
|
|
|
return TextLeafPoint(acc, lineStart);
|
|
|
|
}
|
|
|
|
|
|
|
|
TextLeafPoint TextLeafPoint::FindNextLineStartSameLocalAcc(
|
|
|
|
bool aIncludeOrigin) const {
|
|
|
|
LocalAccessible* acc = mAcc->AsLocal();
|
|
|
|
MOZ_ASSERT(acc);
|
|
|
|
if (aIncludeOrigin && mOffset == 0 && IsLocalAccAtLineStart(acc)) {
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
nsIFrame* frame = acc->GetFrame();
|
|
|
|
if (!frame) {
|
2021-10-14 04:53:47 +03:00
|
|
|
// This can happen if this is an empty element with display: contents. In
|
|
|
|
// that case, this Accessible contains no lines.
|
2021-09-23 14:38:09 +03:00
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
if (!frame->IsTextFrame()) {
|
|
|
|
// There can't be multiple lines in a non-text leaf.
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
// Each line of a text node is rendered as a continuation frame. Get the
|
|
|
|
// continuation containing the origin.
|
|
|
|
int32_t origOffset = mOffset;
|
|
|
|
origOffset = RenderedToContentOffset(acc, origOffset);
|
|
|
|
nsTextFrame* continuation = nullptr;
|
|
|
|
int32_t unusedOffsetInContinuation = 0;
|
|
|
|
frame->GetChildFrameContainingOffset(
|
|
|
|
origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation);
|
|
|
|
MOZ_ASSERT(continuation);
|
|
|
|
if (
|
|
|
|
// A line starts at the origin and the caller wants this included.
|
|
|
|
aIncludeOrigin && continuation->GetContentOffset() == origOffset &&
|
|
|
|
// If this is the first line of this text node (offset 0), don't treat it
|
|
|
|
// as a line start if there's something else on the line before this text
|
|
|
|
// node.
|
|
|
|
!(origOffset == 0 && !IsLocalAccAtLineStart(acc))) {
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
continuation = continuation->GetNextContinuation();
|
|
|
|
if (!continuation) {
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
int32_t lineStart = continuation->GetContentOffset();
|
|
|
|
lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart));
|
|
|
|
return TextLeafPoint(acc, lineStart);
|
|
|
|
}
|
|
|
|
|
2021-10-06 07:53:54 +03:00
|
|
|
TextLeafPoint TextLeafPoint::FindLineStartSameRemoteAcc(
|
|
|
|
nsDirection aDirection, bool aIncludeOrigin) const {
|
|
|
|
RemoteAccessible* acc = mAcc->AsRemote();
|
|
|
|
MOZ_ASSERT(acc);
|
|
|
|
auto lines = acc->GetCachedTextLines();
|
|
|
|
if (!lines) {
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
size_t index;
|
|
|
|
// If BinarySearch returns true, mOffset is in the array and index points at
|
|
|
|
// it. If BinarySearch returns false, mOffset is not in the array and index
|
|
|
|
// points at the next line start after mOffset.
|
|
|
|
if (BinarySearch(*lines, 0, lines->Length(), mOffset, &index)) {
|
|
|
|
if (aIncludeOrigin) {
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
if (aDirection == eDirNext) {
|
|
|
|
// We don't want to include the origin. Get the next line start.
|
|
|
|
++index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
MOZ_ASSERT(index <= lines->Length());
|
2022-04-28 13:42:11 +03:00
|
|
|
if ((aDirection == eDirNext && index == lines->Length()) ||
|
|
|
|
(aDirection == eDirPrevious && index == 0)) {
|
2021-10-06 07:53:54 +03:00
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
// index points at the line start after mOffset.
|
|
|
|
if (aDirection == eDirPrevious) {
|
|
|
|
--index;
|
|
|
|
}
|
|
|
|
return TextLeafPoint(mAcc, lines->ElementAt(index));
|
|
|
|
}
|
|
|
|
|
2023-02-22 07:17:31 +03:00
|
|
|
TextLeafPoint TextLeafPoint::FindLineStartSameAcc(
|
|
|
|
nsDirection aDirection, bool aIncludeOrigin,
|
|
|
|
bool aIgnoreListItemMarker) const {
|
|
|
|
TextLeafPoint boundary;
|
|
|
|
if (aIgnoreListItemMarker && aIncludeOrigin && mOffset == 0 &&
|
|
|
|
IsLeafAfterListItemMarker()) {
|
|
|
|
// If:
|
|
|
|
// (1) we are ignoring list markers
|
|
|
|
// (2) we should include origin
|
|
|
|
// (3) we are at the start of a leaf that follows a list item marker
|
|
|
|
// ...then return this point.
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
|
2021-10-06 07:53:54 +03:00
|
|
|
if (mAcc->IsLocal()) {
|
2023-02-22 07:17:31 +03:00
|
|
|
boundary = aDirection == eDirNext
|
|
|
|
? FindNextLineStartSameLocalAcc(aIncludeOrigin)
|
|
|
|
: FindPrevLineStartSameLocalAcc(aIncludeOrigin);
|
|
|
|
} else {
|
|
|
|
boundary = FindLineStartSameRemoteAcc(aDirection, aIncludeOrigin);
|
2021-10-06 07:53:54 +03:00
|
|
|
}
|
2023-02-22 07:17:31 +03:00
|
|
|
|
|
|
|
if (aIgnoreListItemMarker && aDirection == eDirPrevious && !boundary &&
|
|
|
|
mOffset != 0 && IsLeafAfterListItemMarker()) {
|
|
|
|
// If:
|
|
|
|
// (1) we are ignoring list markers
|
|
|
|
// (2) we are searching backwards in accessible
|
|
|
|
// (3) we did not find a line start before this point
|
|
|
|
// (4) we are in a leaf that follows a list item marker
|
|
|
|
// ...then return the first point in this accessible.
|
|
|
|
boundary = TextLeafPoint(mAcc, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
return boundary;
|
2021-10-06 07:53:54 +03:00
|
|
|
}
|
|
|
|
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
TextLeafPoint TextLeafPoint::FindPrevWordStartSameAcc(
|
|
|
|
bool aIncludeOrigin) const {
|
|
|
|
if (mOffset == 0 && !aIncludeOrigin) {
|
|
|
|
// We can't go back any further and the caller doesn't want the origin
|
|
|
|
// included, so there's nothing more to do.
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
nsAutoString text;
|
2021-10-06 07:53:54 +03:00
|
|
|
mAcc->AppendTextTo(text);
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
TextLeafPoint lineStart = *this;
|
2022-02-04 05:10:28 +03:00
|
|
|
if (!aIncludeOrigin || (lineStart.mOffset == 1 && text.Length() == 1 &&
|
|
|
|
text.CharAt(0) == '\n')) {
|
|
|
|
// We're not interested in a line that starts here, either because
|
|
|
|
// aIncludeOrigin is false or because we're at the end of a line break
|
|
|
|
// node.
|
|
|
|
--lineStart.mOffset;
|
|
|
|
}
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
// A word never starts with a line feed character. If there are multiple
|
|
|
|
// consecutive line feed characters and we're after the first of them, the
|
|
|
|
// previous line start will be a line feed character. Skip this and any prior
|
|
|
|
// consecutive line feed first.
|
|
|
|
for (; lineStart.mOffset >= 0 && text.CharAt(lineStart.mOffset) == '\n';
|
|
|
|
--lineStart.mOffset) {
|
|
|
|
}
|
|
|
|
if (lineStart.mOffset < 0) {
|
|
|
|
// There's no line start for our purposes.
|
|
|
|
lineStart = TextLeafPoint();
|
|
|
|
} else {
|
2022-02-04 05:10:28 +03:00
|
|
|
lineStart =
|
|
|
|
lineStart.FindLineStartSameAcc(eDirPrevious, /* aIncludeOrigin */ true);
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
}
|
|
|
|
// Keep walking backward until we find an acceptable word start.
|
|
|
|
intl::WordRange word;
|
|
|
|
if (mOffset == 0) {
|
|
|
|
word.mBegin = 0;
|
|
|
|
} else if (mOffset == static_cast<int32_t>(text.Length())) {
|
2021-10-25 22:00:22 +03:00
|
|
|
word = WordBreaker::FindWord(text.get(), text.Length(), mOffset - 1);
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
} else {
|
2021-10-25 22:00:22 +03:00
|
|
|
word = WordBreaker::FindWord(text.get(), text.Length(), mOffset);
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
}
|
2021-10-25 22:00:22 +03:00
|
|
|
for (;; word = WordBreaker::FindWord(text.get(), text.Length(),
|
|
|
|
word.mBegin - 1)) {
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
if (!aIncludeOrigin && static_cast<int32_t>(word.mBegin) == mOffset) {
|
|
|
|
// A word possibly starts at the origin, but the caller doesn't want this
|
|
|
|
// included.
|
|
|
|
MOZ_ASSERT(word.mBegin != 0);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (lineStart && static_cast<int32_t>(word.mBegin) < lineStart.mOffset) {
|
|
|
|
// A line start always starts a new word.
|
|
|
|
return lineStart;
|
|
|
|
}
|
2021-10-22 21:41:45 +03:00
|
|
|
if (IsAcceptableWordStart(mAcc, text, static_cast<int32_t>(word.mBegin))) {
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (word.mBegin == 0) {
|
|
|
|
// We can't go back any further.
|
|
|
|
if (lineStart) {
|
|
|
|
// A line start always starts a new word.
|
|
|
|
return lineStart;
|
|
|
|
}
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return TextLeafPoint(mAcc, static_cast<int32_t>(word.mBegin));
|
|
|
|
}
|
|
|
|
|
|
|
|
TextLeafPoint TextLeafPoint::FindNextWordStartSameAcc(
|
|
|
|
bool aIncludeOrigin) const {
|
|
|
|
nsAutoString text;
|
2021-10-06 07:53:54 +03:00
|
|
|
mAcc->AppendTextTo(text);
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
int32_t wordStart = mOffset;
|
|
|
|
if (aIncludeOrigin) {
|
|
|
|
if (wordStart == 0) {
|
2021-10-22 21:41:45 +03:00
|
|
|
if (IsAcceptableWordStart(mAcc, text, 0)) {
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// The origin might start a word, so search from just before it.
|
|
|
|
--wordStart;
|
|
|
|
}
|
|
|
|
}
|
2021-10-06 07:53:54 +03:00
|
|
|
TextLeafPoint lineStart = FindLineStartSameAcc(eDirNext, aIncludeOrigin);
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
if (lineStart) {
|
|
|
|
// A word never starts with a line feed character. If there are multiple
|
|
|
|
// consecutive line feed characters, lineStart will point at the second of
|
|
|
|
// them. Skip this and any subsequent consecutive line feed.
|
|
|
|
for (; lineStart.mOffset < static_cast<int32_t>(text.Length()) &&
|
|
|
|
text.CharAt(lineStart.mOffset) == '\n';
|
|
|
|
++lineStart.mOffset) {
|
|
|
|
}
|
|
|
|
if (lineStart.mOffset == static_cast<int32_t>(text.Length())) {
|
|
|
|
// There's no line start for our purposes.
|
|
|
|
lineStart = TextLeafPoint();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Keep walking forward until we find an acceptable word start.
|
2021-11-09 04:14:15 +03:00
|
|
|
intl::WordBreakIteratorUtf16 wordBreakIter(text);
|
|
|
|
Maybe<uint32_t> nextBreak = wordBreakIter.Seek(wordStart);
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
for (;;) {
|
2021-11-09 04:14:15 +03:00
|
|
|
if (!nextBreak || *nextBreak == text.Length()) {
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
if (lineStart) {
|
|
|
|
// A line start always starts a new word.
|
|
|
|
return lineStart;
|
|
|
|
}
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
2021-11-09 04:14:15 +03:00
|
|
|
wordStart = AssertedCast<int32_t>(*nextBreak);
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
if (lineStart && wordStart > lineStart.mOffset) {
|
|
|
|
// A line start always starts a new word.
|
|
|
|
return lineStart;
|
|
|
|
}
|
2021-10-22 21:41:45 +03:00
|
|
|
if (IsAcceptableWordStart(mAcc, text, wordStart)) {
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
break;
|
|
|
|
}
|
2021-11-09 04:14:15 +03:00
|
|
|
nextBreak = wordBreakIter.Next();
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
}
|
|
|
|
return TextLeafPoint(mAcc, wordStart);
|
|
|
|
}
|
|
|
|
|
2021-11-30 05:07:37 +03:00
|
|
|
bool TextLeafPoint::IsCaretAtEndOfLine() const {
|
|
|
|
MOZ_ASSERT(IsCaret());
|
|
|
|
if (LocalAccessible* acc = mAcc->AsLocal()) {
|
|
|
|
HyperTextAccessible* ht = HyperTextFor(acc);
|
|
|
|
if (!ht) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// Use HyperTextAccessible::IsCaretAtEndOfLine. Eventually, we'll want to
|
|
|
|
// move that code into TextLeafPoint, but existing code depends on it living
|
|
|
|
// in HyperTextAccessible (including caret events).
|
|
|
|
return ht->IsCaretAtEndOfLine();
|
|
|
|
}
|
2021-12-01 07:48:34 +03:00
|
|
|
return mAcc->AsRemote()->Document()->IsCaretAtEndOfLine();
|
2021-11-30 05:07:37 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
TextLeafPoint TextLeafPoint::ActualizeCaret(bool aAdjustAtEndOfLine) const {
|
|
|
|
MOZ_ASSERT(IsCaret());
|
|
|
|
HyperTextAccessibleBase* ht;
|
|
|
|
int32_t htOffset;
|
|
|
|
if (LocalAccessible* acc = mAcc->AsLocal()) {
|
|
|
|
// Use HyperTextAccessible::CaretOffset. Eventually, we'll want to move
|
|
|
|
// that code into TextLeafPoint, but existing code depends on it living in
|
|
|
|
// HyperTextAccessible (including caret events).
|
|
|
|
ht = HyperTextFor(acc);
|
|
|
|
if (!ht) {
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
htOffset = ht->CaretOffset();
|
|
|
|
if (htOffset == -1) {
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
} else {
|
2021-12-01 07:48:34 +03:00
|
|
|
// Ideally, we'd cache the caret as a leaf, but our events are based on
|
|
|
|
// HyperText for now.
|
|
|
|
std::tie(ht, htOffset) = mAcc->AsRemote()->Document()->GetCaret();
|
|
|
|
if (!ht) {
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
2021-11-30 05:07:37 +03:00
|
|
|
}
|
|
|
|
if (aAdjustAtEndOfLine && htOffset > 0 && IsCaretAtEndOfLine()) {
|
|
|
|
// It is the same character offset when the caret is visually at the very
|
|
|
|
// end of a line or the start of a new line (soft line break). Getting text
|
|
|
|
// at the line should provide the line with the visual caret. Otherwise,
|
|
|
|
// screen readers will announce the wrong line as the user presses up or
|
|
|
|
// down arrow and land at the end of a line.
|
|
|
|
--htOffset;
|
|
|
|
}
|
|
|
|
return ht->ToTextLeafPoint(htOffset);
|
|
|
|
}
|
|
|
|
|
2021-09-23 14:38:09 +03:00
|
|
|
TextLeafPoint TextLeafPoint::FindBoundary(AccessibleTextBoundary aBoundaryType,
|
|
|
|
nsDirection aDirection,
|
2023-02-21 19:37:23 +03:00
|
|
|
BoundaryFlags aFlags) const {
|
2021-11-30 05:07:37 +03:00
|
|
|
if (IsCaret()) {
|
|
|
|
if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
|
2022-02-11 09:27:02 +03:00
|
|
|
if (IsCaretAtEndOfLine()) {
|
|
|
|
// The caret is at the end of the line. Return no character.
|
|
|
|
return ActualizeCaret(/* aAdjustAtEndOfLine */ false);
|
|
|
|
}
|
2021-11-30 05:07:37 +03:00
|
|
|
}
|
2023-02-21 19:37:23 +03:00
|
|
|
return ActualizeCaret().FindBoundary(
|
|
|
|
aBoundaryType, aDirection, aFlags & BoundaryFlags::eIncludeOrigin);
|
2021-11-30 05:07:37 +03:00
|
|
|
}
|
2022-08-26 19:26:31 +03:00
|
|
|
|
2023-02-21 19:37:23 +03:00
|
|
|
bool inEditableAndStopInIt = (aFlags & BoundaryFlags::eStopInEditable) &&
|
|
|
|
mAcc->Parent() &&
|
2022-08-26 19:26:31 +03:00
|
|
|
(mAcc->Parent()->State() & states::EDITABLE);
|
2022-02-11 09:27:03 +03:00
|
|
|
if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) {
|
2023-02-21 19:37:23 +03:00
|
|
|
return FindLineEnd(aDirection,
|
|
|
|
inEditableAndStopInIt
|
|
|
|
? aFlags
|
|
|
|
: (aFlags & ~BoundaryFlags::eStopInEditable));
|
2022-02-11 09:27:03 +03:00
|
|
|
}
|
2022-02-11 09:27:03 +03:00
|
|
|
if (aBoundaryType == nsIAccessibleText::BOUNDARY_WORD_END) {
|
2023-02-21 19:37:23 +03:00
|
|
|
return FindWordEnd(aDirection,
|
|
|
|
inEditableAndStopInIt
|
|
|
|
? aFlags
|
|
|
|
: (aFlags & ~BoundaryFlags::eStopInEditable));
|
2022-02-11 09:27:03 +03:00
|
|
|
}
|
2022-02-18 14:41:41 +03:00
|
|
|
if ((aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_START ||
|
|
|
|
aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) &&
|
2023-02-21 19:37:23 +03:00
|
|
|
(aFlags & BoundaryFlags::eIncludeOrigin) && aDirection == eDirPrevious &&
|
|
|
|
IsEmptyLastLine()) {
|
2021-09-23 14:38:09 +03:00
|
|
|
// If we're at an empty line at the end of an Accessible, we don't want to
|
|
|
|
// walk into the previous line. For example, this can happen if the caret
|
|
|
|
// is positioned on an empty line at the end of a textarea.
|
|
|
|
return *this;
|
|
|
|
}
|
2023-02-21 19:37:23 +03:00
|
|
|
bool includeOrigin = !!(aFlags & BoundaryFlags::eIncludeOrigin);
|
2023-02-22 07:17:31 +03:00
|
|
|
bool ignoreListItemMarker = !!(aFlags & BoundaryFlags::eIgnoreListItemMarker);
|
|
|
|
Accessible* lastAcc = nullptr;
|
|
|
|
for (TextLeafPoint searchFrom = *this; searchFrom;
|
|
|
|
searchFrom = searchFrom.NeighborLeafPoint(
|
|
|
|
aDirection, inEditableAndStopInIt, ignoreListItemMarker)) {
|
|
|
|
lastAcc = searchFrom.mAcc;
|
|
|
|
if (ignoreListItemMarker && searchFrom == *this &&
|
|
|
|
searchFrom.mAcc->Role() == roles::LISTITEM_MARKER) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-09-23 14:38:09 +03:00
|
|
|
TextLeafPoint boundary;
|
|
|
|
// Search for the boundary within the current Accessible.
|
|
|
|
switch (aBoundaryType) {
|
2021-09-23 14:38:10 +03:00
|
|
|
case nsIAccessibleText::BOUNDARY_CHAR:
|
2023-02-22 07:17:31 +03:00
|
|
|
if (includeOrigin) {
|
|
|
|
boundary = searchFrom;
|
|
|
|
} else if (aDirection == eDirPrevious && searchFrom.mOffset > 0) {
|
2021-09-23 14:38:10 +03:00
|
|
|
boundary.mAcc = searchFrom.mAcc;
|
|
|
|
boundary.mOffset = searchFrom.mOffset - 1;
|
2023-02-22 07:17:31 +03:00
|
|
|
} else if (aDirection == eDirNext &&
|
|
|
|
searchFrom.mOffset + 1 <
|
|
|
|
static_cast<int32_t>(
|
|
|
|
nsAccUtils::TextLength(searchFrom.mAcc))) {
|
|
|
|
boundary.mAcc = searchFrom.mAcc;
|
|
|
|
boundary.mOffset = searchFrom.mOffset + 1;
|
2021-09-23 14:38:10 +03:00
|
|
|
}
|
|
|
|
break;
|
Bug 1729407 part 3: Add TextLeafPoint support for finding word boundaries. r=eeejay
This implementation uses WordBreaker and a bunch of code to emulate layout's handling of punctuation, space, words spanning across nodes, words broken due to lines, etc.
While this does mean we're effectively duplicating layout, there are a couple of advantages.
First, PeekOffset has a lot of quirks which have caused us problems in the past and are difficult to debug.
They don't matter so much for other consumers, but a11y is very sensitive to these kinds of bugs.
Having our own logic means we can make changes as needed without potentially affecting other PeekOffset consumers.
Second, while the implementation currently only works on LocalAccessibles (because we don't have text in RemoteAccessible yet), it's been designed so that it should be very easy to adapt for RemoteAccessible.
It already walks the unified Accessible tree, so it just has to be taught how to get text from RemoteAccessible once that's possible.
This means we don't have to cache word boundaries; they can be calculated on demand in the parent process.
Differential Revision: https://phabricator.services.mozilla.com/D124775
2021-09-23 14:38:10 +03:00
|
|
|
case nsIAccessibleText::BOUNDARY_WORD_START:
|
|
|
|
if (aDirection == eDirPrevious) {
|
|
|
|
boundary = searchFrom.FindPrevWordStartSameAcc(includeOrigin);
|
|
|
|
} else {
|
|
|
|
boundary = searchFrom.FindNextWordStartSameAcc(includeOrigin);
|
|
|
|
}
|
|
|
|
break;
|
2021-09-23 14:38:09 +03:00
|
|
|
case nsIAccessibleText::BOUNDARY_LINE_START:
|
2023-02-22 07:17:31 +03:00
|
|
|
boundary = searchFrom.FindLineStartSameAcc(aDirection, includeOrigin,
|
|
|
|
ignoreListItemMarker);
|
2021-09-23 14:38:09 +03:00
|
|
|
break;
|
2022-02-18 14:41:41 +03:00
|
|
|
case nsIAccessibleText::BOUNDARY_PARAGRAPH:
|
2023-02-22 07:17:31 +03:00
|
|
|
boundary = searchFrom.FindParagraphSameAcc(aDirection, includeOrigin,
|
|
|
|
ignoreListItemMarker);
|
2022-02-18 14:41:41 +03:00
|
|
|
break;
|
2021-09-23 14:38:09 +03:00
|
|
|
default:
|
|
|
|
MOZ_ASSERT_UNREACHABLE();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (boundary) {
|
|
|
|
return boundary;
|
|
|
|
}
|
2023-02-22 07:17:31 +03:00
|
|
|
|
2021-09-23 14:38:09 +03:00
|
|
|
// The start/end of the Accessible might be a boundary. If so, we must stop
|
|
|
|
// on it.
|
|
|
|
includeOrigin = true;
|
|
|
|
}
|
2023-02-22 07:17:31 +03:00
|
|
|
|
|
|
|
MOZ_ASSERT(lastAcc);
|
|
|
|
// No further leaf was found. Use the start/end of the first/last leaf.
|
|
|
|
return TextLeafPoint(
|
|
|
|
lastAcc, aDirection == eDirPrevious
|
|
|
|
? 0
|
|
|
|
: static_cast<int32_t>(nsAccUtils::TextLength(lastAcc)));
|
2021-09-23 14:38:09 +03:00
|
|
|
}
|
|
|
|
|
2022-02-11 09:27:03 +03:00
|
|
|
TextLeafPoint TextLeafPoint::FindLineEnd(nsDirection aDirection,
|
2023-02-21 19:37:23 +03:00
|
|
|
BoundaryFlags aFlags) const {
|
2022-02-11 09:27:03 +03:00
|
|
|
if (aDirection == eDirPrevious && IsEmptyLastLine()) {
|
|
|
|
// If we're at an empty line at the end of an Accessible, we don't want to
|
|
|
|
// walk into the previous line. For example, this can happen if the caret
|
|
|
|
// is positioned on an empty line at the end of a textarea.
|
|
|
|
// Because we want the line end, we must walk back to the line feed
|
|
|
|
// character.
|
2023-02-21 19:37:23 +03:00
|
|
|
return FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
|
|
|
|
aFlags & ~BoundaryFlags::eIncludeOrigin);
|
2022-02-11 09:27:03 +03:00
|
|
|
}
|
2023-02-21 19:37:23 +03:00
|
|
|
if ((aFlags & BoundaryFlags::eIncludeOrigin) && IsLineFeedChar()) {
|
2022-02-11 09:27:03 +03:00
|
|
|
return *this;
|
|
|
|
}
|
2023-02-21 19:37:23 +03:00
|
|
|
if (aDirection == eDirPrevious && !(aFlags & BoundaryFlags::eIncludeOrigin)) {
|
2022-02-11 09:27:03 +03:00
|
|
|
// If there is a line feed immediately before us, return that.
|
2023-02-21 19:37:23 +03:00
|
|
|
TextLeafPoint prevChar =
|
|
|
|
FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
|
|
|
|
aFlags & ~BoundaryFlags::eIncludeOrigin);
|
2022-02-11 09:27:03 +03:00
|
|
|
if (prevChar.IsLineFeedChar()) {
|
|
|
|
return prevChar;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
TextLeafPoint searchFrom = *this;
|
|
|
|
if (aDirection == eDirNext && (IsLineFeedChar() || IsEmptyLastLine())) {
|
|
|
|
// If we search for the next line start from a line feed, we'll get the
|
|
|
|
// character immediately following the line feed. We actually want the
|
|
|
|
// next line start after that. Skip the line feed.
|
2023-02-21 19:37:23 +03:00
|
|
|
searchFrom = FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext,
|
|
|
|
aFlags & ~BoundaryFlags::eIncludeOrigin);
|
2022-02-11 09:27:03 +03:00
|
|
|
}
|
2023-02-21 19:37:23 +03:00
|
|
|
TextLeafPoint lineStart = searchFrom.FindBoundary(
|
|
|
|
nsIAccessibleText::BOUNDARY_LINE_START, aDirection, aFlags);
|
2022-02-11 09:27:03 +03:00
|
|
|
// If there is a line feed before this line start (at the end of the previous
|
|
|
|
// line), we must return that.
|
2023-02-21 19:37:23 +03:00
|
|
|
TextLeafPoint prevChar =
|
|
|
|
lineStart.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
|
|
|
|
aFlags & ~BoundaryFlags::eIncludeOrigin);
|
2022-02-11 09:27:03 +03:00
|
|
|
if (prevChar && prevChar.IsLineFeedChar()) {
|
|
|
|
return prevChar;
|
|
|
|
}
|
|
|
|
return lineStart;
|
|
|
|
}
|
|
|
|
|
2022-02-11 09:27:03 +03:00
|
|
|
bool TextLeafPoint::IsSpace() const {
|
|
|
|
return GetWordBreakClass(GetChar()) == eWbcSpace;
|
|
|
|
}
|
|
|
|
|
|
|
|
TextLeafPoint TextLeafPoint::FindWordEnd(nsDirection aDirection,
|
2023-02-21 19:37:23 +03:00
|
|
|
BoundaryFlags aFlags) const {
|
2022-02-11 09:27:03 +03:00
|
|
|
char16_t origChar = GetChar();
|
|
|
|
const bool origIsSpace = GetWordBreakClass(origChar) == eWbcSpace;
|
|
|
|
bool prevIsSpace = false;
|
2023-02-21 19:37:23 +03:00
|
|
|
if (aDirection == eDirPrevious ||
|
|
|
|
((aFlags & BoundaryFlags::eIncludeOrigin) && origIsSpace) || !origChar) {
|
|
|
|
TextLeafPoint prev =
|
|
|
|
FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
|
|
|
|
aFlags & ~BoundaryFlags::eIncludeOrigin);
|
2022-02-11 09:27:03 +03:00
|
|
|
if (aDirection == eDirPrevious && prev == *this) {
|
|
|
|
return *this; // Can't go any further.
|
|
|
|
}
|
|
|
|
prevIsSpace = prev.IsSpace();
|
2023-02-21 19:37:24 +03:00
|
|
|
if ((aFlags & BoundaryFlags::eIncludeOrigin) &&
|
|
|
|
(origIsSpace || IsDocEdge(eDirNext)) && !prevIsSpace) {
|
|
|
|
// The origin is space or end of document, but the previous
|
|
|
|
// character is not. This means we're at the end of a word.
|
2022-02-11 09:27:03 +03:00
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
TextLeafPoint boundary = *this;
|
|
|
|
if (aDirection == eDirPrevious && !prevIsSpace) {
|
|
|
|
// If there isn't space immediately before us, first find the start of the
|
|
|
|
// previous word.
|
|
|
|
boundary = FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START,
|
2023-02-21 19:37:23 +03:00
|
|
|
eDirPrevious, aFlags);
|
2022-02-11 09:27:03 +03:00
|
|
|
} else if (aDirection == eDirNext &&
|
|
|
|
(origIsSpace || (!origChar && prevIsSpace))) {
|
|
|
|
// We're within the space at the end of the word. Skip over the space. We
|
|
|
|
// can do that by searching for the next word start.
|
2022-08-26 19:26:31 +03:00
|
|
|
boundary = FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirNext,
|
2023-02-21 19:37:23 +03:00
|
|
|
aFlags & ~BoundaryFlags::eIncludeOrigin);
|
2022-02-11 09:27:03 +03:00
|
|
|
if (boundary.IsSpace()) {
|
|
|
|
// The next word starts with a space. This can happen if there is a space
|
|
|
|
// after or at the start of a block element.
|
|
|
|
return boundary;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (aDirection == eDirNext) {
|
2023-02-21 19:37:24 +03:00
|
|
|
BoundaryFlags flags = aFlags;
|
|
|
|
if (IsDocEdge(eDirPrevious)) {
|
|
|
|
// If this is the start of the doc don't be inclusive in the word-start
|
|
|
|
// search because there is no preceding block where this could be a
|
|
|
|
// word-end for.
|
|
|
|
flags &= ~BoundaryFlags::eIncludeOrigin;
|
|
|
|
}
|
2022-02-11 09:27:03 +03:00
|
|
|
boundary = boundary.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START,
|
2023-02-21 19:37:24 +03:00
|
|
|
eDirNext, flags);
|
2022-02-11 09:27:03 +03:00
|
|
|
}
|
|
|
|
// At this point, boundary is either the start of a word or at a space. A
|
|
|
|
// word ends at the beginning of consecutive space. Therefore, skip back to
|
|
|
|
// the start of any space before us.
|
|
|
|
TextLeafPoint prev = boundary;
|
|
|
|
for (;;) {
|
2022-08-26 19:26:31 +03:00
|
|
|
prev = prev.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
|
2023-02-21 19:37:23 +03:00
|
|
|
aFlags & ~BoundaryFlags::eIncludeOrigin);
|
2022-02-11 09:27:03 +03:00
|
|
|
if (prev == boundary) {
|
|
|
|
break; // Can't go any further.
|
|
|
|
}
|
|
|
|
if (!prev.IsSpace()) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
boundary = prev;
|
|
|
|
}
|
|
|
|
return boundary;
|
|
|
|
}
|
|
|
|
|
2023-02-22 07:17:31 +03:00
|
|
|
TextLeafPoint TextLeafPoint::FindParagraphSameAcc(
|
|
|
|
nsDirection aDirection, bool aIncludeOrigin,
|
|
|
|
bool aIgnoreListItemMarker) const {
|
2023-02-21 19:37:24 +03:00
|
|
|
if (aIncludeOrigin && IsDocEdge(eDirPrevious)) {
|
|
|
|
// The top of the document is a paragraph boundary.
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
|
2023-02-22 07:17:31 +03:00
|
|
|
if (aIgnoreListItemMarker && aIncludeOrigin && mOffset == 0 &&
|
|
|
|
IsLeafAfterListItemMarker()) {
|
|
|
|
// If we are in a list item and the previous sibling is
|
|
|
|
// a bullet, the 0 offset in this leaf is a line start.
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
|
2022-02-18 14:41:41 +03:00
|
|
|
if (mAcc->IsTextLeaf() &&
|
|
|
|
// We don't want to copy strings unnecessarily. See below for the context
|
|
|
|
// of these individual conditions.
|
|
|
|
((aIncludeOrigin && mOffset > 0) || aDirection == eDirNext ||
|
|
|
|
mOffset >= 2)) {
|
|
|
|
// If there is a line feed, a new paragraph begins after it.
|
|
|
|
nsAutoString text;
|
|
|
|
mAcc->AppendTextTo(text);
|
|
|
|
if (aIncludeOrigin && mOffset > 0 && text.CharAt(mOffset - 1) == '\n') {
|
|
|
|
return TextLeafPoint(mAcc, mOffset);
|
|
|
|
}
|
|
|
|
int32_t lfOffset = -1;
|
|
|
|
if (aDirection == eDirNext) {
|
|
|
|
lfOffset = text.FindChar('\n', mOffset);
|
|
|
|
} else if (mOffset >= 2) {
|
|
|
|
// A line feed at mOffset - 1 means the origin begins a new paragraph,
|
|
|
|
// but we already handled aIncludeOrigin above. Therefore, we search from
|
|
|
|
// mOffset - 2.
|
|
|
|
lfOffset = text.RFindChar('\n', mOffset - 2);
|
|
|
|
}
|
|
|
|
if (lfOffset != -1 && lfOffset + 1 < static_cast<int32_t>(text.Length())) {
|
|
|
|
return TextLeafPoint(mAcc, lfOffset + 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-22 07:17:31 +03:00
|
|
|
if (aIgnoreListItemMarker && mOffset > 0 && aDirection == eDirPrevious &&
|
|
|
|
IsLeafAfterListItemMarker()) {
|
|
|
|
// No line breaks were found in the preceding text to this offset.
|
|
|
|
// If we are in a list item and the previous sibling is
|
|
|
|
// a bullet, the 0 offset in this leaf is a line start.
|
|
|
|
return TextLeafPoint(mAcc, 0);
|
|
|
|
}
|
|
|
|
|
2022-02-18 14:41:41 +03:00
|
|
|
// Check whether this Accessible begins a paragraph.
|
|
|
|
if ((!aIncludeOrigin && mOffset == 0) ||
|
|
|
|
(aDirection == eDirNext && mOffset > 0)) {
|
|
|
|
// The caller isn't interested in whether this Accessible begins a
|
|
|
|
// paragraph.
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
Accessible* prevLeaf = PrevLeaf(mAcc);
|
|
|
|
BlockRule blockRule;
|
2022-04-01 12:49:56 +03:00
|
|
|
Pivot pivot(nsAccUtils::DocumentFor(mAcc));
|
2022-02-18 14:41:41 +03:00
|
|
|
Accessible* prevBlock = pivot.Prev(mAcc, blockRule);
|
|
|
|
// Check if we're the first leaf after a block element.
|
2022-05-10 02:41:02 +03:00
|
|
|
if (prevBlock) {
|
|
|
|
if (
|
|
|
|
// If there's no previous leaf, we must be the first leaf after the
|
|
|
|
// block.
|
|
|
|
!prevLeaf ||
|
|
|
|
// A block can be a leaf; e.g. an empty div or paragraph.
|
|
|
|
prevBlock == prevLeaf) {
|
|
|
|
return TextLeafPoint(mAcc, 0);
|
|
|
|
}
|
|
|
|
if (prevBlock->IsAncestorOf(mAcc)) {
|
|
|
|
// We're inside the block.
|
|
|
|
if (!prevBlock->IsAncestorOf(prevLeaf)) {
|
|
|
|
// The previous leaf isn't inside the block. That means we're the first
|
|
|
|
// leaf in the block.
|
|
|
|
return TextLeafPoint(mAcc, 0);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// We aren't inside the block, so the block ends before us.
|
|
|
|
if (prevBlock->IsAncestorOf(prevLeaf)) {
|
|
|
|
// The previous leaf is inside the block. That means we're the first
|
|
|
|
// leaf after the block. This case is necessary because a block causes a
|
|
|
|
// paragraph break both before and after it.
|
|
|
|
return TextLeafPoint(mAcc, 0);
|
|
|
|
}
|
|
|
|
}
|
2022-02-18 14:41:41 +03:00
|
|
|
}
|
|
|
|
if (!prevLeaf || prevLeaf->IsHTMLBr()) {
|
|
|
|
// We're the first leaf after a line break or the start of the document.
|
|
|
|
return TextLeafPoint(mAcc, 0);
|
|
|
|
}
|
|
|
|
if (prevLeaf->IsTextLeaf()) {
|
|
|
|
// There's a text leaf before us. Check if it ends with a line feed.
|
|
|
|
nsAutoString text;
|
|
|
|
prevLeaf->AppendTextTo(text, nsAccUtils::TextLength(prevLeaf) - 1, 1);
|
|
|
|
if (text.CharAt(0) == '\n') {
|
|
|
|
return TextLeafPoint(mAcc, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
|
2022-05-27 13:56:40 +03:00
|
|
|
bool TextLeafPoint::IsInSpellingError() const {
|
|
|
|
if (LocalAccessible* acc = mAcc->AsLocal()) {
|
|
|
|
auto domRanges = FindDOMSpellingErrors(acc, mOffset, mOffset + 1);
|
|
|
|
// If there is a spelling error overlapping this character, we're in a
|
|
|
|
// spelling error.
|
|
|
|
return !domRanges.IsEmpty();
|
|
|
|
}
|
2022-05-27 13:56:41 +03:00
|
|
|
|
|
|
|
RemoteAccessible* acc = mAcc->AsRemote();
|
|
|
|
MOZ_ASSERT(acc);
|
|
|
|
if (!acc->mCachedFields) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
auto spellingErrors =
|
|
|
|
acc->mCachedFields->GetAttribute<nsTArray<int32_t>>(nsGkAtoms::spelling);
|
|
|
|
if (!spellingErrors) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
size_t index;
|
|
|
|
const bool foundOrigin = BinarySearch(
|
|
|
|
*spellingErrors, 0, spellingErrors->Length(), mOffset, &index);
|
|
|
|
// In spellingErrors, even indices are start offsets, odd indices are end
|
|
|
|
// offsets.
|
|
|
|
const bool foundStart = index % 2 == 0;
|
|
|
|
if (foundOrigin) {
|
|
|
|
// mOffset is a spelling error boundary. If it's a start offset, we're in a
|
|
|
|
// spelling error.
|
|
|
|
return foundStart;
|
|
|
|
}
|
|
|
|
// index points at the next spelling error boundary after mOffset.
|
|
|
|
if (index == 0) {
|
|
|
|
return false; // No spelling errors before mOffset.
|
|
|
|
}
|
|
|
|
if (foundStart) {
|
|
|
|
// We're not in a spelling error because it starts after mOffset.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// A spelling error ends after mOffset.
|
|
|
|
return true;
|
2022-05-27 13:56:40 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
TextLeafPoint TextLeafPoint::FindSpellingErrorSameAcc(
|
|
|
|
nsDirection aDirection, bool aIncludeOrigin) const {
|
|
|
|
if (!aIncludeOrigin && mOffset == 0 && aDirection == eDirPrevious) {
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
if (LocalAccessible* acc = mAcc->AsLocal()) {
|
|
|
|
// We want to find both start and end points, so we pass true for
|
|
|
|
// aAllowAdjacent.
|
|
|
|
auto domRanges =
|
|
|
|
aDirection == eDirNext
|
|
|
|
? FindDOMSpellingErrors(acc, mOffset,
|
|
|
|
nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT,
|
|
|
|
/* aAllowAdjacent */ true)
|
|
|
|
: FindDOMSpellingErrors(acc, 0, mOffset,
|
|
|
|
/* aAllowAdjacent */ true);
|
|
|
|
nsINode* node = acc->GetNode();
|
|
|
|
if (aDirection == eDirNext) {
|
|
|
|
for (nsRange* domRange : domRanges) {
|
|
|
|
if (domRange->GetStartContainer() == node) {
|
|
|
|
int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
|
|
|
|
acc, static_cast<int32_t>(domRange->StartOffset())));
|
|
|
|
if ((aIncludeOrigin && matchOffset == mOffset) ||
|
|
|
|
matchOffset > mOffset) {
|
|
|
|
return TextLeafPoint(mAcc, matchOffset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (domRange->GetEndContainer() == node) {
|
|
|
|
int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
|
|
|
|
acc, static_cast<int32_t>(domRange->EndOffset())));
|
|
|
|
if ((aIncludeOrigin && matchOffset == mOffset) ||
|
|
|
|
matchOffset > mOffset) {
|
|
|
|
return TextLeafPoint(mAcc, matchOffset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for (nsRange* domRange : Reversed(domRanges)) {
|
|
|
|
if (domRange->GetEndContainer() == node) {
|
|
|
|
int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
|
|
|
|
acc, static_cast<int32_t>(domRange->EndOffset())));
|
|
|
|
if ((aIncludeOrigin && matchOffset == mOffset) ||
|
|
|
|
matchOffset < mOffset) {
|
|
|
|
return TextLeafPoint(mAcc, matchOffset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (domRange->GetStartContainer() == node) {
|
|
|
|
int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
|
|
|
|
acc, static_cast<int32_t>(domRange->StartOffset())));
|
|
|
|
if ((aIncludeOrigin && matchOffset == mOffset) ||
|
|
|
|
matchOffset < mOffset) {
|
|
|
|
return TextLeafPoint(mAcc, matchOffset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-05-27 13:56:41 +03:00
|
|
|
return TextLeafPoint();
|
2022-05-27 13:56:40 +03:00
|
|
|
}
|
2022-05-27 13:56:41 +03:00
|
|
|
|
|
|
|
RemoteAccessible* acc = mAcc->AsRemote();
|
|
|
|
MOZ_ASSERT(acc);
|
|
|
|
if (!acc->mCachedFields) {
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
auto spellingErrors =
|
|
|
|
acc->mCachedFields->GetAttribute<nsTArray<int32_t>>(nsGkAtoms::spelling);
|
|
|
|
if (!spellingErrors) {
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
size_t index;
|
|
|
|
if (BinarySearch(*spellingErrors, 0, spellingErrors->Length(), mOffset,
|
|
|
|
&index)) {
|
|
|
|
// mOffset is in spellingErrors.
|
|
|
|
if (aIncludeOrigin) {
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
if (aDirection == eDirNext) {
|
|
|
|
// We don't want the origin, so move to the next spelling error boundary
|
|
|
|
// after mOffset.
|
|
|
|
++index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// index points at the next spelling error boundary after mOffset.
|
|
|
|
if (aDirection == eDirNext) {
|
|
|
|
if (spellingErrors->Length() == index) {
|
|
|
|
return TextLeafPoint(); // No spelling error boundary after us.
|
|
|
|
}
|
|
|
|
return TextLeafPoint(mAcc, (*spellingErrors)[index]);
|
|
|
|
}
|
|
|
|
if (index == 0) {
|
|
|
|
return TextLeafPoint(); // No spelling error boundary before us.
|
|
|
|
}
|
|
|
|
// Decrement index so it points at a spelling error boundary before mOffset.
|
|
|
|
--index;
|
|
|
|
if ((*spellingErrors)[index] == -1) {
|
|
|
|
MOZ_ASSERT(index == 0);
|
|
|
|
// A spelling error starts before mAcc.
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
return TextLeafPoint(mAcc, (*spellingErrors)[index]);
|
|
|
|
}
|
|
|
|
|
2023-02-22 07:17:31 +03:00
|
|
|
TextLeafPoint TextLeafPoint::NeighborLeafPoint(
|
|
|
|
nsDirection aDirection, bool aIsEditable,
|
|
|
|
bool aIgnoreListItemMarker) const {
|
|
|
|
Accessible* acc = aDirection == eDirPrevious
|
|
|
|
? PrevLeaf(mAcc, aIsEditable, aIgnoreListItemMarker)
|
|
|
|
: NextLeaf(mAcc, aIsEditable, aIgnoreListItemMarker);
|
|
|
|
if (!acc) {
|
|
|
|
return TextLeafPoint();
|
|
|
|
}
|
|
|
|
|
|
|
|
return TextLeafPoint(
|
|
|
|
acc, aDirection == eDirPrevious
|
|
|
|
? static_cast<int32_t>(nsAccUtils::TextLength(acc)) - 1
|
|
|
|
: 0);
|
|
|
|
}
|
|
|
|
|
2023-03-09 22:59:00 +03:00
|
|
|
LayoutDeviceIntRect TextLeafPoint::ComputeBoundsFromFrame() const {
|
|
|
|
LocalAccessible* local = mAcc->AsLocal();
|
|
|
|
MOZ_ASSERT(local, "Can't compute bounds in frame from non-local acc");
|
|
|
|
nsIFrame* frame = local->GetFrame();
|
|
|
|
MOZ_ASSERT(frame, "No frame found for acc!");
|
|
|
|
|
|
|
|
if (!frame->IsTextFrame()) {
|
|
|
|
return local->Bounds();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Substring must be entirely within the same text node.
|
|
|
|
MOZ_ASSERT(frame->IsPrimaryFrame(),
|
|
|
|
"Cannot compute content offset on non-primary frame");
|
|
|
|
nsIFrame::RenderedText text = frame->GetRenderedText(
|
|
|
|
mOffset, mOffset + 1, nsIFrame::TextOffsetType::OffsetsInRenderedText,
|
|
|
|
nsIFrame::TrailingWhitespace::DontTrim);
|
|
|
|
int32_t contentOffset = text.mOffsetWithinNodeText;
|
|
|
|
int32_t contentOffsetInFrame;
|
|
|
|
// Get the right frame continuation -- not really a child, but a sibling of
|
|
|
|
// the primary frame passed in
|
|
|
|
nsresult rv = frame->GetChildFrameContainingOffset(
|
2023-03-21 04:23:40 +03:00
|
|
|
contentOffset, true, &contentOffsetInFrame, &frame);
|
2023-03-09 22:59:00 +03:00
|
|
|
NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
|
|
|
|
|
|
|
|
// Start with this frame's screen rect, which we will shrink based on
|
|
|
|
// the char we care about within it.
|
|
|
|
nsRect frameScreenRect = frame->GetScreenRectInAppUnits();
|
|
|
|
|
|
|
|
// Add the point where the char starts to the frameScreenRect
|
|
|
|
nsPoint frameTextStartPoint;
|
|
|
|
rv = frame->GetPointFromOffset(contentOffset, &frameTextStartPoint);
|
|
|
|
NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
|
|
|
|
|
|
|
|
// Use the next offset to calculate the width
|
|
|
|
// XXX(morgan) does this work for vertical text?
|
|
|
|
nsPoint frameTextEndPoint;
|
|
|
|
rv = frame->GetPointFromOffset(contentOffset + 1, &frameTextEndPoint);
|
|
|
|
NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
|
|
|
|
|
|
|
|
frameScreenRect.SetRectX(
|
|
|
|
frameScreenRect.X() +
|
|
|
|
std::min(frameTextStartPoint.x, frameTextEndPoint.x),
|
|
|
|
mozilla::Abs(frameTextStartPoint.x - frameTextEndPoint.x));
|
|
|
|
|
|
|
|
nsPresContext* presContext = local->Document()->PresContext();
|
|
|
|
return LayoutDeviceIntRect::FromAppUnitsToNearest(
|
|
|
|
frameScreenRect, presContext->AppUnitsPerDevPixel());
|
|
|
|
}
|
|
|
|
|
2022-05-27 13:56:41 +03:00
|
|
|
/* static */
|
|
|
|
nsTArray<int32_t> TextLeafPoint::GetSpellingErrorOffsets(
|
|
|
|
LocalAccessible* aAcc) {
|
|
|
|
nsINode* node = aAcc->GetNode();
|
|
|
|
auto domRanges = FindDOMSpellingErrors(
|
|
|
|
aAcc, 0, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
|
|
|
|
// Our offsets array will contain two offsets for each range: one for the
|
|
|
|
// start, one for the end. That is, the array is of the form:
|
|
|
|
// [r1start, r1end, r2start, r2end, ...]
|
|
|
|
nsTArray<int32_t> offsets(domRanges.Length() * 2);
|
|
|
|
for (nsRange* domRange : domRanges) {
|
|
|
|
if (domRange->GetStartContainer() == node) {
|
|
|
|
offsets.AppendElement(static_cast<int32_t>(ContentToRenderedOffset(
|
|
|
|
aAcc, static_cast<int32_t>(domRange->StartOffset()))));
|
|
|
|
} else {
|
|
|
|
// This range overlaps aAcc, but starts before it.
|
|
|
|
// This can only happen for the first range.
|
|
|
|
MOZ_ASSERT(domRange == *domRanges.begin() && offsets.IsEmpty());
|
|
|
|
// Using -1 here means this won't be treated as the start of a spelling
|
|
|
|
// error range, while still indicating that we're within a spelling error.
|
|
|
|
offsets.AppendElement(-1);
|
|
|
|
}
|
|
|
|
if (domRange->GetEndContainer() == node) {
|
|
|
|
offsets.AppendElement(static_cast<int32_t>(ContentToRenderedOffset(
|
|
|
|
aAcc, static_cast<int32_t>(domRange->EndOffset()))));
|
|
|
|
} else {
|
|
|
|
// This range overlaps aAcc, but ends after it.
|
|
|
|
// This can only happen for the last range.
|
|
|
|
MOZ_ASSERT(domRange == *domRanges.rbegin());
|
|
|
|
// We don't append -1 here because this would just make things harder for
|
|
|
|
// a binary search.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return offsets;
|
2022-05-27 13:56:40 +03:00
|
|
|
}
|
|
|
|
|
2022-05-27 13:56:41 +03:00
|
|
|
/* static */
|
|
|
|
void TextLeafPoint::UpdateCachedSpellingError(dom::Document* aDocument,
|
|
|
|
const nsRange& aRange) {
|
|
|
|
DocAccessible* docAcc = GetExistingDocAccessible(aDocument);
|
|
|
|
if (!docAcc) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
LocalAccessible* startAcc = docAcc->GetAccessible(aRange.GetStartContainer());
|
|
|
|
LocalAccessible* endAcc = docAcc->GetAccessible(aRange.GetEndContainer());
|
|
|
|
if (!startAcc || !endAcc) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (Accessible* acc = startAcc; acc; acc = NextLeaf(acc)) {
|
|
|
|
if (acc->IsTextLeaf()) {
|
|
|
|
docAcc->QueueCacheUpdate(acc->AsLocal(), CacheDomain::Spelling);
|
|
|
|
}
|
|
|
|
if (acc == endAcc) {
|
|
|
|
// Subtle: We check this here rather than in the loop condition because
|
|
|
|
// we want to include endAcc but stop once we reach it. Putting it in the
|
|
|
|
// loop condition would mean we stop at endAcc, but we would also exclude
|
|
|
|
// it; i.e. we wouldn't push the cache for it.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-02 02:27:37 +03:00
|
|
|
already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributesLocalAcc(
|
|
|
|
bool aIncludeDefaults) const {
|
|
|
|
LocalAccessible* acc = mAcc->AsLocal();
|
|
|
|
MOZ_ASSERT(acc);
|
|
|
|
MOZ_ASSERT(acc->IsText());
|
|
|
|
// TextAttrsMgr wants a HyperTextAccessible.
|
|
|
|
LocalAccessible* parent = acc->LocalParent();
|
|
|
|
HyperTextAccessible* hyperAcc = parent->AsHyperText();
|
|
|
|
MOZ_ASSERT(hyperAcc);
|
|
|
|
RefPtr<AccAttributes> attributes = new AccAttributes();
|
2022-03-25 21:41:12 +03:00
|
|
|
if (hyperAcc) {
|
|
|
|
TextAttrsMgr mgr(hyperAcc, aIncludeDefaults, acc,
|
|
|
|
acc ? acc->IndexInParent() : -1);
|
|
|
|
mgr.GetAttributes(attributes, nullptr, nullptr);
|
|
|
|
}
|
2021-11-02 02:27:37 +03:00
|
|
|
return attributes.forget();
|
|
|
|
}
|
|
|
|
|
|
|
|
already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributes(
|
|
|
|
bool aIncludeDefaults) const {
|
|
|
|
if (!mAcc->IsText()) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
2022-05-27 13:56:40 +03:00
|
|
|
RefPtr<AccAttributes> attrs;
|
2021-11-02 02:27:37 +03:00
|
|
|
if (mAcc->IsLocal()) {
|
2022-05-27 13:56:40 +03:00
|
|
|
attrs = GetTextAttributesLocalAcc(aIncludeDefaults);
|
|
|
|
} else {
|
|
|
|
attrs = new AccAttributes();
|
|
|
|
if (aIncludeDefaults) {
|
|
|
|
Accessible* parent = mAcc->Parent();
|
|
|
|
if (parent && parent->IsRemote() && parent->IsHyperText()) {
|
|
|
|
if (auto defAttrs = parent->AsRemote()->GetCachedTextAttributes()) {
|
|
|
|
defAttrs->CopyTo(attrs);
|
|
|
|
}
|
2021-11-02 02:27:39 +03:00
|
|
|
}
|
|
|
|
}
|
2022-05-27 13:56:40 +03:00
|
|
|
if (auto thisAttrs = mAcc->AsRemote()->GetCachedTextAttributes()) {
|
|
|
|
thisAttrs->CopyTo(attrs);
|
|
|
|
}
|
2021-11-02 02:27:39 +03:00
|
|
|
}
|
2022-05-27 13:56:40 +03:00
|
|
|
if (IsInSpellingError()) {
|
|
|
|
attrs->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::spelling);
|
2021-11-02 02:27:39 +03:00
|
|
|
}
|
|
|
|
return attrs.forget();
|
2021-11-02 02:27:37 +03:00
|
|
|
}
|
|
|
|
|
2022-05-27 13:56:40 +03:00
|
|
|
TextLeafPoint TextLeafPoint::FindTextAttrsStart(nsDirection aDirection,
|
|
|
|
bool aIncludeOrigin) const {
|
2021-11-30 05:07:37 +03:00
|
|
|
if (IsCaret()) {
|
2022-05-27 13:56:40 +03:00
|
|
|
return ActualizeCaret().FindTextAttrsStart(aDirection, aIncludeOrigin);
|
2021-11-30 05:07:37 +03:00
|
|
|
}
|
2021-11-02 02:27:39 +03:00
|
|
|
const bool isRemote = mAcc->IsRemote();
|
2022-05-27 13:56:40 +03:00
|
|
|
RefPtr<const AccAttributes> lastAttrs =
|
|
|
|
isRemote ? mAcc->AsRemote()->GetCachedTextAttributes()
|
|
|
|
: GetTextAttributesLocalAcc();
|
2021-11-02 02:27:37 +03:00
|
|
|
if (aIncludeOrigin && aDirection == eDirNext && mOffset == 0) {
|
|
|
|
// Even when searching forward, the only way to know whether the origin is
|
|
|
|
// the start of a text attrs run is to compare with the previous sibling.
|
|
|
|
// Anything other than text breaks an attrs run.
|
|
|
|
TextLeafPoint point;
|
|
|
|
point.mAcc = mAcc->PrevSibling();
|
|
|
|
if (!point.mAcc || !point.mAcc->IsText()) {
|
|
|
|
return *this;
|
|
|
|
}
|
2021-11-02 02:27:39 +03:00
|
|
|
// For RemoteAccessible, we can get attributes from the cache without any
|
|
|
|
// calculation or copying.
|
2021-11-02 02:27:37 +03:00
|
|
|
RefPtr<const AccAttributes> attrs =
|
2021-11-02 02:27:39 +03:00
|
|
|
isRemote ? point.mAcc->AsRemote()->GetCachedTextAttributes()
|
2022-05-27 13:56:40 +03:00
|
|
|
: point.GetTextAttributesLocalAcc();
|
2022-02-24 00:28:20 +03:00
|
|
|
if (attrs && lastAttrs && !attrs->Equal(lastAttrs)) {
|
2021-11-02 02:27:37 +03:00
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
}
|
2022-05-27 13:56:40 +03:00
|
|
|
TextLeafPoint lastPoint = *this;
|
2021-11-02 02:27:37 +03:00
|
|
|
for (;;) {
|
2022-05-27 13:56:40 +03:00
|
|
|
if (TextLeafPoint spelling = lastPoint.FindSpellingErrorSameAcc(
|
|
|
|
aDirection, aIncludeOrigin && lastPoint.mAcc == mAcc)) {
|
|
|
|
// A spelling error starts or ends somewhere in the Accessible we're
|
|
|
|
// considering. This causes an attribute change, so return that point.
|
|
|
|
return spelling;
|
|
|
|
}
|
2021-11-02 02:27:37 +03:00
|
|
|
TextLeafPoint point;
|
|
|
|
point.mAcc = aDirection == eDirNext ? lastPoint.mAcc->NextSibling()
|
|
|
|
: lastPoint.mAcc->PrevSibling();
|
|
|
|
if (!point.mAcc || !point.mAcc->IsText()) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
RefPtr<const AccAttributes> attrs =
|
2021-11-02 02:27:39 +03:00
|
|
|
isRemote ? point.mAcc->AsRemote()->GetCachedTextAttributes()
|
2022-05-27 13:56:40 +03:00
|
|
|
: point.GetTextAttributesLocalAcc();
|
2022-02-24 00:28:20 +03:00
|
|
|
if (attrs && lastAttrs && !attrs->Equal(lastAttrs)) {
|
2021-11-02 02:27:37 +03:00
|
|
|
// The attributes change here. If we're moving forward, we want to
|
|
|
|
// return this point. If we're moving backward, we've now moved before
|
2022-05-27 13:56:40 +03:00
|
|
|
// the start of the attrs run containing the origin, so return that start
|
|
|
|
// point; i.e. the start of the last Accessible we hit.
|
2021-11-02 02:27:37 +03:00
|
|
|
if (aDirection == eDirPrevious) {
|
|
|
|
point = lastPoint;
|
2022-05-27 13:56:40 +03:00
|
|
|
point.mOffset = 0;
|
2021-11-02 02:27:37 +03:00
|
|
|
}
|
|
|
|
if (!aIncludeOrigin && point == *this) {
|
|
|
|
MOZ_ASSERT(aDirection == eDirPrevious);
|
|
|
|
// The origin is the start of an attrs run, but the caller doesn't want
|
|
|
|
// the origin included.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
return point;
|
|
|
|
}
|
|
|
|
lastPoint = point;
|
2022-05-27 13:56:40 +03:00
|
|
|
if (aDirection == eDirPrevious) {
|
|
|
|
// On the next iteration, we want to search for spelling errors from the
|
|
|
|
// end of this Accessible.
|
|
|
|
lastPoint.mOffset =
|
|
|
|
static_cast<int32_t>(nsAccUtils::TextLength(point.mAcc));
|
|
|
|
}
|
2021-11-02 02:27:37 +03:00
|
|
|
lastAttrs = attrs;
|
|
|
|
}
|
|
|
|
// We couldn't move any further. Use the start/end.
|
|
|
|
return TextLeafPoint(
|
|
|
|
lastPoint.mAcc,
|
|
|
|
aDirection == eDirPrevious
|
|
|
|
? 0
|
|
|
|
: static_cast<int32_t>(nsAccUtils::TextLength(lastPoint.mAcc)));
|
|
|
|
}
|
|
|
|
|
2022-05-09 23:30:27 +03:00
|
|
|
LayoutDeviceIntRect TextLeafPoint::CharBounds() {
|
2022-05-24 00:43:12 +03:00
|
|
|
if (mAcc && !mAcc->IsText()) {
|
2022-07-05 22:50:45 +03:00
|
|
|
// If we're dealing with an empty container, return the
|
2022-05-24 00:43:12 +03:00
|
|
|
// accessible's non-text bounds.
|
|
|
|
return mAcc->Bounds();
|
|
|
|
}
|
|
|
|
|
2023-03-02 21:08:15 +03:00
|
|
|
if (!mAcc || (mAcc->IsRemote() && !mAcc->AsRemote()->mCachedFields)) {
|
2022-05-09 23:30:27 +03:00
|
|
|
return LayoutDeviceIntRect();
|
|
|
|
}
|
|
|
|
|
2023-03-02 21:08:15 +03:00
|
|
|
if (LocalAccessible* local = mAcc->AsLocal()) {
|
2023-03-09 22:59:00 +03:00
|
|
|
if (!local->IsTextLeaf() || nsAccUtils::TextLength(local) == 0) {
|
|
|
|
// Empty content, use our own bounds to at least get x,y coordinates
|
|
|
|
return local->Bounds();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mOffset >= 0 &&
|
2023-03-21 04:23:40 +03:00
|
|
|
static_cast<uint32_t>(mOffset) >= nsAccUtils::TextLength(local)) {
|
2023-03-23 03:36:49 +03:00
|
|
|
// It's valid for a caller to query the length because the caret might be
|
|
|
|
// at the end of editable text. In that case, we should just silently
|
|
|
|
// return. However, we assert that the offset isn't greater than the
|
|
|
|
// length.
|
|
|
|
NS_ASSERTION(
|
|
|
|
static_cast<uint32_t>(mOffset) <= nsAccUtils::TextLength(local),
|
|
|
|
"Wrong in offset");
|
2023-03-09 22:59:00 +03:00
|
|
|
return LayoutDeviceIntRect();
|
|
|
|
}
|
|
|
|
|
|
|
|
LayoutDeviceIntRect bounds = ComputeBoundsFromFrame();
|
|
|
|
|
|
|
|
// This document may have a resolution set, we will need to multiply
|
|
|
|
// the document-relative coordinates by that value and re-apply the doc's
|
|
|
|
// screen coordinates.
|
|
|
|
nsPresContext* presContext = local->Document()->PresContext();
|
|
|
|
nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame();
|
|
|
|
LayoutDeviceIntRect orgRectPixels =
|
|
|
|
LayoutDeviceIntRect::FromAppUnitsToNearest(
|
|
|
|
rootFrame->GetScreenRectInAppUnits(),
|
|
|
|
presContext->AppUnitsPerDevPixel());
|
|
|
|
bounds.MoveBy(-orgRectPixels.X(), -orgRectPixels.Y());
|
|
|
|
bounds.ScaleRoundOut(presContext->PresShell()->GetResolution());
|
|
|
|
bounds.MoveBy(orgRectPixels.X(), orgRectPixels.Y());
|
|
|
|
return bounds;
|
2023-03-02 21:08:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
RemoteAccessible* remote = mAcc->AsRemote();
|
2023-03-03 06:26:21 +03:00
|
|
|
nsRect charBounds = remote->GetCachedCharRect(mOffset);
|
|
|
|
if (!charBounds.IsEmpty()) {
|
|
|
|
return remote->BoundsWithOffset(Some(charBounds));
|
2022-05-09 23:30:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return LayoutDeviceIntRect();
|
|
|
|
}
|
|
|
|
|
2022-07-05 22:50:45 +03:00
|
|
|
bool TextLeafPoint::ContainsPoint(int32_t aX, int32_t aY) {
|
|
|
|
if (mAcc && !mAcc->IsText()) {
|
|
|
|
// If we're dealing with an empty embedded object, use the
|
|
|
|
// accessible's non-text bounds.
|
|
|
|
return mAcc->Bounds().Contains(aX, aY);
|
|
|
|
}
|
|
|
|
|
|
|
|
return CharBounds().Contains(aX, aY);
|
|
|
|
}
|
|
|
|
|
2023-01-09 22:56:20 +03:00
|
|
|
LayoutDeviceIntRect TextLeafRange::Bounds() const {
|
|
|
|
if (mEnd == mStart || mEnd < mStart) {
|
|
|
|
return LayoutDeviceIntRect();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool locatedFinalLine = false;
|
|
|
|
TextLeafPoint currPoint = mStart;
|
|
|
|
LayoutDeviceIntRect result = currPoint.CharBounds();
|
|
|
|
|
|
|
|
// Union the first and last chars of each line to create a line rect. Then,
|
|
|
|
// union the lines together.
|
|
|
|
while (!locatedFinalLine) {
|
|
|
|
// Fetch the last point in the current line by getting the
|
|
|
|
// start of the next line and going back one char. We don't
|
|
|
|
// use BOUNDARY_LINE_END here because it is equivalent to LINE_START when
|
|
|
|
// the line doesn't end with a line feed character.
|
2023-02-21 19:37:23 +03:00
|
|
|
TextLeafPoint lineStartPoint = currPoint.FindBoundary(
|
|
|
|
nsIAccessibleText::BOUNDARY_LINE_START, eDirNext);
|
2023-01-09 22:56:20 +03:00
|
|
|
TextLeafPoint lastPointInLine = lineStartPoint.FindBoundary(
|
2023-02-21 19:37:23 +03:00
|
|
|
nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
|
2023-01-09 22:56:20 +03:00
|
|
|
if (mEnd <= lastPointInLine) {
|
|
|
|
lastPointInLine = mEnd;
|
|
|
|
locatedFinalLine = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
LayoutDeviceIntRect currLine = currPoint.CharBounds();
|
|
|
|
currLine.UnionRect(currLine, lastPointInLine.CharBounds());
|
|
|
|
result.UnionRect(result, currLine);
|
|
|
|
|
|
|
|
currPoint = lineStartPoint;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2023-01-27 23:15:11 +03:00
|
|
|
bool TextLeafRange::SetSelection(int32_t aSelectionNum) const {
|
|
|
|
if (!mStart || !mEnd || mStart.mAcc->IsLocal() != mEnd.mAcc->IsLocal()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mStart.mAcc->IsRemote()) {
|
|
|
|
DocAccessibleParent* doc = mStart.mAcc->AsRemote()->Document();
|
|
|
|
if (doc != mEnd.mAcc->AsRemote()->Document()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Unused << doc->SendSetTextSelection(mStart.mAcc->ID(), mStart.mOffset,
|
|
|
|
mEnd.mAcc->ID(), mEnd.mOffset,
|
|
|
|
aSelectionNum);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool reversed = mEnd < mStart;
|
|
|
|
auto [startContent, startContentOffset] =
|
|
|
|
DOMPointForSelection(!reversed ? mStart : mEnd);
|
|
|
|
auto [endContent, endContentOffset] =
|
|
|
|
DOMPointForSelection(!reversed ? mEnd : mStart);
|
|
|
|
|
|
|
|
if (!startContent || !endContent) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
RefPtr<dom::Selection> domSel = GetDOMSelection(startContent, endContent);
|
|
|
|
if (!domSel) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint32_t rangeCount = domSel->RangeCount();
|
|
|
|
RefPtr<nsRange> domRange = nullptr;
|
|
|
|
if (aSelectionNum == static_cast<int32_t>(rangeCount) || aSelectionNum < 0) {
|
|
|
|
domRange = nsRange::Create(startContent);
|
|
|
|
} else {
|
|
|
|
domRange = domSel->GetRangeAt(AssertedCast<uint32_t>(aSelectionNum));
|
|
|
|
}
|
|
|
|
if (!domRange) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
domRange->SetStart(startContent, startContentOffset);
|
|
|
|
domRange->SetEnd(endContent, endContentOffset);
|
|
|
|
|
|
|
|
// If this is not a new range, notify selection listeners that the existing
|
|
|
|
// selection range has changed. Otherwise, just add the new range.
|
|
|
|
if (aSelectionNum != static_cast<int32_t>(rangeCount)) {
|
|
|
|
domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*domRange,
|
|
|
|
IgnoreErrors());
|
|
|
|
}
|
|
|
|
|
|
|
|
IgnoredErrorResult err;
|
|
|
|
domSel->AddRangeAndSelectFramesAndNotifyListeners(*domRange, err);
|
|
|
|
if (!err.Failed()) {
|
|
|
|
// Changing the direction of the selection assures that the caret
|
|
|
|
// will be at the logical end of the selection.
|
|
|
|
domSel->SetDirection(reversed ? eDirPrevious : eDirNext);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-08-31 23:34:18 +03:00
|
|
|
TextLeafRange::Iterator TextLeafRange::Iterator::BeginIterator(
|
|
|
|
const TextLeafRange& aRange) {
|
|
|
|
Iterator result(aRange);
|
|
|
|
|
|
|
|
result.mSegmentStart = aRange.mStart;
|
|
|
|
if (aRange.mStart.mAcc == aRange.mEnd.mAcc) {
|
|
|
|
result.mSegmentEnd = aRange.mEnd;
|
|
|
|
} else {
|
|
|
|
result.mSegmentEnd = TextLeafPoint(
|
|
|
|
aRange.mStart.mAcc, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
TextLeafRange::Iterator TextLeafRange::Iterator::EndIterator(
|
|
|
|
const TextLeafRange& aRange) {
|
|
|
|
Iterator result(aRange);
|
|
|
|
|
|
|
|
result.mSegmentEnd = TextLeafPoint();
|
|
|
|
result.mSegmentStart = TextLeafPoint();
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
TextLeafRange::Iterator& TextLeafRange::Iterator::operator++() {
|
|
|
|
if (mSegmentEnd.mAcc == mRange.mEnd.mAcc) {
|
|
|
|
mSegmentEnd = TextLeafPoint();
|
|
|
|
mSegmentStart = TextLeafPoint();
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Accessible* nextLeaf = NextLeaf(mSegmentEnd.mAcc)) {
|
|
|
|
mSegmentStart = TextLeafPoint(nextLeaf, 0);
|
|
|
|
if (nextLeaf == mRange.mEnd.mAcc) {
|
|
|
|
mSegmentEnd = TextLeafPoint(nextLeaf, mRange.mEnd.mOffset);
|
|
|
|
} else {
|
|
|
|
mSegmentEnd =
|
|
|
|
TextLeafPoint(nextLeaf, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
mSegmentEnd = TextLeafPoint();
|
|
|
|
mSegmentStart = TextLeafPoint();
|
|
|
|
}
|
|
|
|
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
|
2021-09-23 14:38:09 +03:00
|
|
|
} // namespace mozilla::a11y
|