blob: 7d1cda5a3692f129c226dfd6dd2c133d9e0f9be1 [file]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef UI_ACCESSIBILITY_AX_RANGE_H_
#define UI_ACCESSIBILITY_AX_RANGE_H_
#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include <utility>
#include <vector>
#include "ui/accessibility/ax_clipping_behavior.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_node_position.h"
#include "ui/accessibility/ax_offscreen_result.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_tree_manager_map.h"
namespace ui {
// Specifies how AXRange::GetText treats any formatting changes, such as
// paragraph breaks, that have been introduced by layout. For example, consider
// the following HTML snippet: "A<div>B</div>C".
enum class AXTextConcatenationBehavior {
// Preserve any introduced formatting, such as paragraph breaks, e.g. GetText
// = "A\nB\nC".
kWithParagraphBreaks,
// Ignore any introduced formatting, such as paragraph breaks, e.g. GetText =
// "ABC".
kWithoutParagraphBreaks
};
class AXRangeRectDelegate {
public:
virtual gfx::Rect GetInnerTextRangeBoundsRect(
AXTreeID tree_id,
AXNodeID node_id,
int start_offset,
int end_offset,
AXClippingBehavior clipping_behavior,
AXOffscreenResult* offscreen_result) = 0;
virtual gfx::Rect GetBoundsRect(AXTreeID tree_id,
AXNodeID node_id,
AXOffscreenResult* offscreen_result) = 0;
};
// A range delimited by two positions in the AXTree.
//
// In order to avoid any confusion regarding whether a deep or a shallow copy is
// being performed, this class can be moved, but not copied.
template <class AXPositionType>
class AXRange final {
public:
using AXPositionInstance = std::unique_ptr<AXPositionType>;
// Creates an `AXRange` encompassing the contents of the given `AXNode`.
static AXRange RangeOfContents(const AXNode& node) {
AXPositionInstance start_position = AXNodePosition::CreatePosition(
node, /* child_index_or_text_offset */ 0);
AXPositionInstance end_position =
start_position->CreatePositionAtEndOfAnchor();
return AXRange(std::move(start_position), std::move(end_position));
}
AXRange()
: anchor_(AXPositionType::CreateNullPosition()),
focus_(AXPositionType::CreateNullPosition()) {}
AXRange(AXPositionInstance anchor, AXPositionInstance focus) {
anchor_ = anchor ? std::move(anchor) : AXPositionType::CreateNullPosition();
focus_ = focus ? std::move(focus) : AXPositionType::CreateNullPosition();
}
AXRange(const AXRange& other) = delete;
AXRange(AXRange&& other) : AXRange() {
anchor_.swap(other.anchor_);
focus_.swap(other.focus_);
}
~AXRange() = default;
AXPositionType* anchor() const {
DCHECK(anchor_);
return anchor_.get();
}
AXPositionType* focus() const {
DCHECK(focus_);
return focus_.get();
}
AXRange& operator=(const AXRange& other) = delete;
AXRange& operator=(AXRange&& other) {
if (this != &other) {
anchor_ = AXPositionType::CreateNullPosition();
focus_ = AXPositionType::CreateNullPosition();
anchor_.swap(other.anchor_);
focus_.swap(other.focus_);
}
return *this;
}
bool operator==(const AXRange& other) const {
if (IsNull())
return other.IsNull();
return !other.IsNull() && *anchor_ == *other.anchor() &&
*focus_ == *other.focus();
}
bool operator!=(const AXRange& other) const { return !(*this == other); }
// Given a pair of AXPosition, determines how the first compares with the
// second, relative to the order they would be iterated over by using
// AXRange::Iterator to traverse all leaf text ranges in a tree.
//
// Notice that this method is different from using AXPosition::CompareTo since
// the following logic takes into account BOTH tree pre-order traversal and
// text offsets when both positions are located within the same anchor.
//
// Returns:
// 0 - If both positions are equivalent.
// <0 - If the first position would come BEFORE the second.
// >0 - If the first position would come AFTER the second.
// nullopt - If positions are not comparable (see AXPosition::CompareTo).
static std::optional<int> CompareEndpoints(const AXPositionType* first,
const AXPositionType* second) {
DCHECK(first->IsValid());
DCHECK(second->IsValid());
std::optional<int> tree_position_comparison =
first->AsTreePosition()->CompareTo(*second->AsTreePosition());
// When the tree comparison is nullopt, using value_or(1) forces a default
// value of 1, making the following statement return nullopt as well.
return (tree_position_comparison.value_or(1) != 0)
? tree_position_comparison
: first->CompareTo(*second);
}
AXRange AsForwardRange() const {
return (CompareEndpoints(anchor(), focus()).value_or(0) > 0)
? AXRange(focus_->Clone(), anchor_->Clone())
: AXRange(anchor_->Clone(), focus_->Clone());
}
AXRange AsBackwardRange() const {
return (CompareEndpoints(anchor(), focus()).value_or(0) < 0)
? AXRange(focus_->Clone(), anchor_->Clone())
: AXRange(anchor_->Clone(), focus_->Clone());
}
bool IsCollapsed() const { return !IsNull() && *anchor_ == *focus_; }
// We define a "leaf text range" as an AXRange whose endpoints are leaf text
// positions located within the same anchor of the AXTree.
bool IsLeafTextRange() const {
return !IsNull() && anchor_->GetAnchor() == focus_->GetAnchor() &&
anchor_->IsLeafTextPosition() && focus_->IsLeafTextPosition();
}
bool IsNull() const {
DCHECK(anchor_ && focus_);
return anchor_->IsNullPosition() || focus_->IsNullPosition();
}
std::string ToString() const {
return "Range\nAnchor:" + anchor_->ToString() +
"\nFocus:" + focus_->ToString();
}
// We can decompose any given AXRange into multiple "leaf text ranges".
// As an example, consider the following HTML code:
//
// <p>line with text<br><input type="checkbox">line with checkbox</p>
//
// It will produce the following AXTree; notice that the leaf text nodes
// (enclosed in parenthesis) compose its text representation:
//
// paragraph
// staticText name='line with text'
// (inlineTextBox name='line with text')
// lineBreak name='<newline>'
// (inlineTextBox name='<newline>')
// (checkBox)
// staticText name='line with checkbox'
// (inlineTextBox name='line with checkbox')
//
// Suppose we have an AXRange containing all elements from the example above.
// The text representation of such range, with AXRange's endpoints marked by
// opening and closing brackets, will look like the following:
//
// "[line with text\n{checkBox}line with checkbox]"
//
// Note that in the text representation {checkBox} is not visible, but it is
// effectively a "leaf text range", so we include it in the example above only
// to visualize how the iterator should work.
//
// Decomposing the AXRange above into its "leaf text ranges" would result in:
//
// "[line with text][\n][{checkBox}][line with checkbox]"
//
// This class allows AXRange to be iterated through all "leaf text ranges"
// contained between its endpoints, composing the entire range.
class Iterator {
public:
using iterator_category = std::input_iterator_tag;
using value_type = AXRange;
using difference_type = std::ptrdiff_t;
using pointer = AXRange*;
using reference = AXRange&;
Iterator()
: current_start_(AXPositionType::CreateNullPosition()),
iterator_end_(AXPositionType::CreateNullPosition()) {}
Iterator(AXPositionInstance start, AXPositionInstance end) {
if (end && !end->IsNullPosition()) {
current_start_ = !start ? AXPositionType::CreateNullPosition()
: start->AsLeafTextPosition();
iterator_end_ = end->AsLeafTextPosition();
} else {
current_start_ = AXPositionType::CreateNullPosition();
iterator_end_ = AXPositionType::CreateNullPosition();
}
}
Iterator(const Iterator& other) = delete;
Iterator(Iterator&& other)
: current_start_(std::move(other.current_start_)),
iterator_end_(std::move(other.iterator_end_)) {}
~Iterator() = default;
bool operator==(const Iterator& other) const {
return current_start_->GetAnchor() == other.current_start_->GetAnchor() &&
iterator_end_->GetAnchor() == other.iterator_end_->GetAnchor() &&
*current_start_ == *other.current_start_ &&
*iterator_end_ == *other.iterator_end_;
}
bool operator!=(const Iterator& other) const { return !(*this == other); }
// Only forward iteration is supported, so operator-- is not implemented.
Iterator& operator++() {
DCHECK(!current_start_->IsNullPosition());
if (current_start_->GetAnchor() == iterator_end_->GetAnchor()) {
current_start_ = AXPositionType::CreateNullPosition();
} else {
current_start_ = current_start_->CreateNextLeafTreePosition();
DCHECK_LE(*current_start_, *iterator_end_);
}
return *this;
}
AXRange operator*() const {
DCHECK(!current_start_->IsNullPosition());
AXPositionInstance current_end =
(current_start_->GetAnchor() != iterator_end_->GetAnchor())
? current_start_->CreatePositionAtEndOfAnchor()
: iterator_end_->Clone();
DCHECK_LE(*current_end, *iterator_end_);
AXRange current_leaf_text_range(current_start_->AsTextPosition(),
current_end->AsTextPosition());
DCHECK(current_leaf_text_range.IsLeafTextRange());
return std::move(current_leaf_text_range);
}
private:
AXPositionInstance current_start_;
AXPositionInstance iterator_end_;
};
Iterator begin() const {
if (IsNull())
return Iterator(nullptr, nullptr);
AXRange forward_range = AsForwardRange();
return Iterator(std::move(forward_range.anchor_),
std::move(forward_range.focus_));
}
Iterator end() const {
if (IsNull())
return Iterator(nullptr, nullptr);
AXRange forward_range = AsForwardRange();
return Iterator(nullptr, std::move(forward_range.focus_));
}
// Similar to Iterator class, but moves in the reverse direction.
class ReverseIterator {
public:
using iterator_category = std::input_iterator_tag;
using value_type = AXRange;
using difference_type = std::ptrdiff_t;
using pointer = AXRange*;
using reference = AXRange&;
ReverseIterator()
: anchor_(AXPositionType::CreateNullPosition()),
focus_(AXPositionType::CreateNullPosition()) {}
ReverseIterator(AXPositionInstance start, AXPositionInstance end) {
if (start && !start->IsNullPosition()) {
anchor_ = start->AsLeafTextPosition();
focus_ = !end ? AXPositionType::CreateNullPosition()
: end->AsLeafTextPosition();
} else {
anchor_ = AXPositionType::CreateNullPosition();
focus_ = AXPositionType::CreateNullPosition();
}
}
ReverseIterator(const ReverseIterator& other) = delete;
ReverseIterator(ReverseIterator&& other)
: anchor_(std::move(other.anchor_)), focus_(std::move(other.focus_)) {}
~ReverseIterator() = default;
bool operator==(const ReverseIterator& other) const {
return anchor_->GetAnchor() == other.anchor_->GetAnchor() &&
focus_->GetAnchor() == other.focus_->GetAnchor() &&
*anchor_ == *other.anchor_ && *focus_ == *other.focus_;
}
bool operator!=(const ReverseIterator& other) const {
return !(*this == other);
}
ReverseIterator& operator--() {
DCHECK(!focus_->IsNullPosition());
if (anchor_->GetAnchor() == focus_->GetAnchor()) {
focus_ = AXPositionType::CreateNullPosition();
} else {
focus_ = focus_->CreatePreviousLeafTreePosition()
->CreatePositionAtEndOfAnchor();
DCHECK_LE(*anchor_, *focus_);
}
return *this;
}
AXRange operator*() const {
DCHECK(!focus_->IsNullPosition());
AXPositionInstance current_anchor =
(anchor_->GetAnchor() != focus_->GetAnchor())
? focus_->CreatePositionAtStartOfAnchor()
: anchor_->Clone();
DCHECK_LE(*anchor_, *current_anchor);
AXRange current_leaf_text_range(current_anchor->AsTextPosition(),
focus_->AsTextPosition());
DCHECK(current_leaf_text_range.IsLeafTextRange());
return std::move(current_leaf_text_range);
}
private:
AXPositionInstance anchor_;
AXPositionInstance focus_;
};
ReverseIterator rbegin() const {
if (IsNull()) {
return ReverseIterator(nullptr, nullptr);
}
AXRange forward_range = AsForwardRange();
return ReverseIterator(std::move(forward_range.anchor_),
std::move(forward_range.focus_));
}
ReverseIterator rend() const {
if (IsNull()) {
return ReverseIterator(AXPositionType::CreateNullPosition(),
AXPositionType::CreateNullPosition());
}
AXRange forward_range = AsForwardRange();
return ReverseIterator(std::move(forward_range.anchor_),
AXPositionType::CreateNullPosition());
}
// Returns the concatenation of the accessible names of all text nodes
// contained between this AXRange's endpoints.
// Pass a |max_count| of -1 to retrieve all text in the AXRange.
// Note that if this AXRange has its anchor or focus located at an ignored
// position, we shrink the range to the closest unignored positions.
std::u16string GetText(
AXTextConcatenationBehavior concatenation_behavior =
AXTextConcatenationBehavior::kWithoutParagraphBreaks,
AXEmbeddedObjectBehavior embedded_object_behavior =
AXEmbeddedObjectBehavior::kExposeCharacterForHypertext,
int max_count = -1,
bool include_ignored = false,
std::vector<size_t>* appended_newlines_indices = nullptr) const {
if (max_count == 0 || IsNull())
return std::u16string();
std::optional<int> endpoint_comparison =
CompareEndpoints(anchor(), focus());
if (!endpoint_comparison)
return std::u16string();
AXPositionInstance start = (endpoint_comparison.value() < 0)
? anchor_->AsLeafTextPosition()
: focus_->AsLeafTextPosition();
AXPositionInstance end = (endpoint_comparison.value() < 0)
? focus_->AsLeafTextPosition()
: anchor_->AsLeafTextPosition();
std::u16string range_text;
bool is_first_non_whitespace_leaf = true;
bool crossed_paragraph_boundary = false;
bool is_first_included_leaf = true;
bool found_trailing_newline = false;
// Tracks whether `found_trailing_newline` was set by an actual <br>
// element (as opposed to a preserved-whitespace newline). Only <br>
// elements can produce the "br between block containers" scenario.
bool found_trailing_newline_from_br = false;
const AXNode* prev_paragraph_container = nullptr;
// The paragraph container of the <br> element that set
// `found_trailing_newline_from_br`. Used to determine whether the <br>
// sits between two different block containers (br_container != prev)
// vs. inside the same block as the previous text (br_container == prev).
const AXNode* br_paragraph_container = nullptr;
// Tracks the atomic text field ancestor of the previous leaf position.
// Used to suppress generated paragraph newlines when both the current
// and previous positions are inside the same atomic text field, where
// line breaks are internal, not paragraph boundaries.
// Note: We intentionally only consider *atomic* text fields (e.g.
// <textarea>, <input>) here. Non-atomic text fields (contenteditables)
// contain block-level structure and their paragraph boundaries should
// be preserved.
auto get_atomic_text_field_ancestor = [](const AXNode* node) {
const AXNode* text_field = node->GetTextFieldAncestor();
return (text_field && text_field->data().IsAtomicTextField()) ? text_field
: nullptr;
};
const AXNode* prev_text_field_ancestor = nullptr;
while (!start->IsNullPosition()) {
DCHECK(start->IsLeafTextPosition());
DCHECK_GE(start->text_offset(), 0);
const bool start_is_unignored = !start->IsIgnored();
const bool start_is_in_white_space = start->IsInWhiteSpace();
if (include_ignored || start_is_unignored) {
if (concatenation_behavior ==
AXTextConcatenationBehavior::kWithParagraphBreaks &&
!start_is_in_white_space) {
if (is_first_non_whitespace_leaf && !is_first_included_leaf) {
// The first non-whitespace leaf in the range could be preceded by
// whitespace spanning even before the start of this range, we need
// to check such positions in order to correctly determine if this
// is a paragraph's start (see |AXPosition::AtStartOfParagraph|).
// However, if the first paragraph boundary in the range is ignored,
// e.g. <div aria-hidden="true"></div>, we do not take it into
// consideration even when `include_ignored` == true, because the
// beginning of the text range, as experienced by the user, is after
// any trailing ignored nodes.
crossed_paragraph_boundary =
start_is_unignored && start->AtStartOfParagraph();
}
// When preserving layout line breaks, don't append `\n` next if the
// previous leaf position was a line break (already ending with a
// newline). However, if the trailing newline came from a <br>
// element AND the paragraph container actually changed (e.g.
// <div>A</div><br><div>B</div>), the <br> represents a standalone
// line break between block elements, so the paragraph boundary
// newline should still be inserted.
bool should_insert_paragraph_newline = false;
if (crossed_paragraph_boundary) {
// Suppress paragraph newlines when both the current and previous
// non-whitespace positions are inside the same atomic text field.
// Line breaks within text fields are internal, not paragraph
// boundaries.
const AXNode* current_text_field =
get_atomic_text_field_ancestor(start->GetAnchor());
const bool same_text_field =
current_text_field &&
current_text_field == prev_text_field_ancestor;
if (same_text_field) {
// Inside the same text field: no generated paragraph newline.
} else if (!found_trailing_newline) {
should_insert_paragraph_newline = true;
} else if (found_trailing_newline_from_br) {
// The trailing newline came from a <br> element. Only insert
// the paragraph newline if the <br> is truly BETWEEN two
// different block containers — i.e. the <br>'s own paragraph
// container is an ancestor of BOTH the previous text's
// container AND the current text's container. This handles:
// <div>A</div><br><div>B</div> → br in body, A in div1,
// B in div2 → body is ancestor of both → \n\n
// <div>A</div><div><br>B</div> → br in div2, A in div1,
// B in div2 → div2 == div2 → \n (br is inside div2)
// <body>A<br>B</body> → br in body, A in body → \n
// <p>A</p><div><br></div><p>B</p> → br in div_br, A in p1,
// B in p2 → div_br not ancestor of p1 → \n
const AXNode* current_para_container =
start->GetAnchor()->GetParagraphContainerAncestor();
should_insert_paragraph_newline =
(br_paragraph_container != nullptr &&
prev_paragraph_container != nullptr &&
current_para_container != nullptr &&
br_paragraph_container != prev_paragraph_container &&
br_paragraph_container != current_para_container &&
prev_paragraph_container->IsDescendantOf(
br_paragraph_container) &&
current_para_container->IsDescendantOf(
br_paragraph_container));
}
// If `found_trailing_newline` but NOT from a <br> (e.g., preserved
// whitespace \n), don't insert — the preserved newline already
// serves as the separator.
}
if (should_insert_paragraph_newline) {
range_text += u"\n";
if (appended_newlines_indices) {
appended_newlines_indices->push_back(range_text.length() - 1);
}
}
is_first_non_whitespace_leaf = false;
prev_paragraph_container =
start->GetAnchor()->GetParagraphContainerAncestor();
crossed_paragraph_boundary = false;
}
int focus_offset = (start->GetAnchor() != end->GetAnchor())
? start->MaxTextOffset(embedded_object_behavior)
: end->text_offset();
if (focus_offset > start->text_offset()) {
int characters_to_append =
(max_count > 0)
? std::min(max_count - static_cast<int>(range_text.length()),
focus_offset - start->text_offset())
: focus_offset - start->text_offset();
std::u16string position_text =
start->GetText(embedded_object_behavior);
if (start->text_offset() < static_cast<int>(position_text.length())) {
range_text += position_text.substr(start->text_offset(),
characters_to_append);
}
// To minimize user confusion, collapse all whitespace following any
// line break unless it is a hard line break (<br> or a text node with
// a single '\n' character), or an empty object such as an empty text
// field.
if (start->GetAnchor()->IsLineBreak()) {
// Check if this is an actual <br> element (`Role::kLineBreak`) or
// its `kInlineTextBox` child, vs a preserved-whitespace newline.
bool is_br_element =
start->GetAnchor()->GetRole() == ax::mojom::Role::kLineBreak ||
(start->GetAnchor()->GetParent() &&
start->GetAnchor()->GetParent()->GetRole() ==
ax::mojom::Role::kLineBreak);
found_trailing_newline = true;
if (is_br_element) {
found_trailing_newline_from_br = true;
br_paragraph_container =
start->GetAnchor()->GetParagraphContainerAncestor();
} else {
// Non-`<br>` line break (e.g., preserved whitespace in `<pre>`).
// Reset `<br>` tracking to avoid stale container comparisons.
found_trailing_newline_from_br = false;
br_paragraph_container = nullptr;
}
} else if (found_trailing_newline && start_is_in_white_space) {
// Whitespace after a line break preserves the trailing newline
// state.
} else {
found_trailing_newline = false;
found_trailing_newline_from_br = false;
br_paragraph_container = nullptr;
}
}
DCHECK(max_count < 0 ||
static_cast<int>(range_text.length()) <= max_count);
is_first_included_leaf = false;
// Track the text field ancestor for every included leaf so that
// paragraph boundaries within the same text field can be suppressed
// even when the first leaf in the range is a zero-character line
// break (which skips the non-whitespace block above).
prev_text_field_ancestor =
get_atomic_text_field_ancestor(start->GetAnchor());
}
if (start->GetAnchor() == end->GetAnchor() ||
static_cast<int>(range_text.length()) == max_count) {
break;
}
ax::mojom::Role prev_role = start->GetAnchor()->GetRole();
start = start->CreateNextLeafTextPosition();
// We should not mark `cross_paragraph_boundary` as true if the start
// anchor is a `kListMarker` since there should be no newline added
// by default after the `kListMarker` node.
if (concatenation_behavior ==
AXTextConcatenationBehavior::kWithParagraphBreaks &&
!crossed_paragraph_boundary && !is_first_non_whitespace_leaf &&
prev_role != ax::mojom::Role::kListMarker) {
crossed_paragraph_boundary = start->AtStartOfParagraph();
// Suppress paragraph boundaries within the same atomic text field.
if (crossed_paragraph_boundary) {
const AXNode* current_text_field =
get_atomic_text_field_ancestor(start->GetAnchor());
if (current_text_field &&
current_text_field == prev_text_field_ancestor) {
crossed_paragraph_boundary = false;
}
}
}
}
// When the range ends exactly at a paragraph boundary—before any
// non-whitespace leaf in the next paragraph could consume the
// `crossed_paragraph_boundary` flag—the generated paragraph-break newline
// was never inserted. This happens for ranges that represent a single
// generated newline character, e.g. the range [ab:2, LineBreak:0] for
// <div>ab</div><br><div>cd</div>. Insert the newline now so that
// character-by-character navigation sees it.
if (concatenation_behavior ==
AXTextConcatenationBehavior::kWithParagraphBreaks &&
crossed_paragraph_boundary && !is_first_non_whitespace_leaf &&
range_text.empty()) {
range_text += u"\n";
if (appended_newlines_indices) {
appended_newlines_indices->push_back(range_text.length() - 1);
}
}
return range_text;
}
// Appends rects of all anchor nodes that span between anchor_ and focus_.
// Rects outside of the viewport are skipped.
// Coordinate system is determined by the passed-in delegate.
std::vector<gfx::Rect> GetRects(AXRangeRectDelegate* delegate) const {
std::vector<gfx::Rect> rects;
AXPositionInstance range_start = anchor()->AsLeafTextPosition();
AXPositionInstance range_end = focus()->AsLeafTextPosition();
// For a degenerate range, we want to fetch unclipped bounding rect, because
// text with the same start and end off set (i.e. degenerate) will have an
// inner text bounding rect with height of the character and width of 0,
// which the browser platform will consider as an empty rect and ends up
// clipping it, resulting in size 0x1 rect.
// After we retrieve the unclipped bounding rect, we want to set its width
// to 1 to represent a caret/insertion point.
//
// Note: The caller of this function is only UIA TextPattern, so displaying
// bounding rects for degenerate range is only limited for UIA currently.
if (IsCollapsed() && range_start->IsInTextObject()) {
AXOffscreenResult offscreen_result;
gfx::Rect degenerate_range_rect = delegate->GetInnerTextRangeBoundsRect(
range_start->tree_id(), range_start->anchor_id(),
range_start->text_offset(), range_end->text_offset(),
AXClippingBehavior::kUnclipped, &offscreen_result);
if (offscreen_result == AXOffscreenResult::kOnscreen) {
DCHECK(degenerate_range_rect.width() == 0);
degenerate_range_rect.set_width(1);
rects.push_back(degenerate_range_rect);
}
return rects;
}
for (const AXRange& leaf_text_range : *this) {
DCHECK(leaf_text_range.IsLeafTextRange());
AXPositionType* current_line_start = leaf_text_range.anchor();
AXPositionType* current_line_end = leaf_text_range.focus();
// We want to skip ranges from ignored nodes.
if (current_line_start->IsIgnored())
continue;
// For text anchors, we retrieve the bounding rectangles of its text
// content. For non-text anchors (such as checkboxes, images, etc.), we
// want to directly retrieve their bounding rectangles. Since text fields
// in Views do not have text nodes as children (the text is in the text
// field itself), we need to expose the inner bounds of those nodes too.
AXOffscreenResult offscreen_result;
gfx::Rect current_rect;
if (current_line_start->GetAnchor()->IsLineBreak() ||
current_line_start->IsInTextObject() ||
(current_line_start->GetAnchor()->IsView() &&
current_line_start->IsInTextField())) {
current_rect = delegate->GetInnerTextRangeBoundsRect(
current_line_start->tree_id(), current_line_start->anchor_id(),
current_line_start->text_offset(), current_line_end->text_offset(),
AXClippingBehavior::kClipped, &offscreen_result);
} else {
current_rect = delegate->GetBoundsRect(current_line_start->tree_id(),
current_line_start->anchor_id(),
&offscreen_result);
}
// If the bounding box of the current range is clipped because it lies
// outside an ancestor’s bounds, then the bounding box is pushed to the
// nearest edge of such ancestor's bounds, with its width and height
// forced to be 1, and the node will be marked as "offscreen".
//
// Only add rectangles that are not empty and not marked as "offscreen".
//
// See the documentation for how bounding boxes are calculated in AXTree:
// https://chromium.googlesource.com/chromium/src/+/HEAD/docs/accessibility/offscreen.md
// In cases where the bounding box has a collapsed width, we expand it to
// a width of 1 to ensure it isn't collapsed. Some screen readers rely on
// bounding boxes having a non-zero area to function correctly.
if (current_rect.width() == 0 && current_rect.height() > 0) {
current_rect.set_width(1);
}
if (!current_rect.IsEmpty() &&
offscreen_result == AXOffscreenResult::kOnscreen)
rects.push_back(current_rect);
}
return rects;
}
private:
AXPositionInstance anchor_;
AXPositionInstance focus_;
};
template <class AXPositionType>
std::ostream& operator<<(std::ostream& stream,
const AXRange<AXPositionType>& range) {
return stream << range.ToString();
}
} // namespace ui
#endif // UI_ACCESSIBILITY_AX_RANGE_H_