Flutter Engine
The Flutter Engine
ax_range.h
Go to the documentation of this file.
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#ifndef UI_ACCESSIBILITY_AX_RANGE_H_
6#define UI_ACCESSIBILITY_AX_RANGE_H_
7
8#include <memory>
9#include <ostream>
10#include <string>
11#include <utility>
12#include <vector>
13
15#include "ax_enums.h"
16#include "ax_offscreen_result.h"
17#include "ax_role_properties.h"
18#include "ax_tree_manager_map.h"
19#include "base/string_utils.h"
20
21namespace ui {
22
23// Specifies how AXRange::GetText treats line breaks introduced by layout.
24// For example, consider the following HTML snippet: "A<div>B</div>C".
26 // Preserve any introduced line breaks, e.g. GetText = "A\nB\nC".
28 // Ignore any introduced line breaks, e.g. GetText = "ABC".
30};
31
33 public:
35 AXTreeID tree_id,
36 AXNode::AXID node_id,
37 int start_offset,
38 int end_offset,
39 ui::AXClippingBehavior clipping_behavior,
40 AXOffscreenResult* offscreen_result) = 0;
42 AXNode::AXID node_id,
43 AXOffscreenResult* offscreen_result) = 0;
44};
45
46// A range delimited by two positions in the AXTree.
47//
48// In order to avoid any confusion regarding whether a deep or a shallow copy is
49// being performed, this class can be moved, but not copied.
50template <class AXPositionType>
51class AXRange {
52 public:
53 using AXPositionInstance = std::unique_ptr<AXPositionType>;
54
56 : anchor_(AXPositionType::CreateNullPosition()),
57 focus_(AXPositionType::CreateNullPosition()) {}
58
60 anchor_ = anchor ? std::move(anchor) : AXPositionType::CreateNullPosition();
61 focus_ = focus ? std::move(focus) : AXPositionType::CreateNullPosition();
62 }
63
64 AXRange(const AXRange& other) = delete;
65
66 AXRange(AXRange&& other) : AXRange() {
67 anchor_.swap(other.anchor_);
68 focus_.swap(other.focus_);
69 }
70
71 virtual ~AXRange() = default;
72
73 AXPositionType* anchor() const {
74 BASE_DCHECK(anchor_);
75 return anchor_.get();
76 }
77
78 AXPositionType* focus() const {
79 BASE_DCHECK(focus_);
80 return focus_.get();
81 }
82
83 AXRange& operator=(const AXRange& other) = delete;
84
86 if (this != &other) {
87 anchor_ = AXPositionType::CreateNullPosition();
88 focus_ = AXPositionType::CreateNullPosition();
89 anchor_.swap(other.anchor_);
90 focus_.swap(other.focus_);
91 }
92 return *this;
93 }
94
95 bool operator==(const AXRange& other) const {
96 if (IsNull())
97 return other.IsNull();
98 return !other.IsNull() && *anchor_ == *other.anchor() &&
99 *focus_ == *other.focus();
100 }
101
102 bool operator!=(const AXRange& other) const { return !(*this == other); }
103
104 // Given a pair of AXPosition, determines how the first compares with the
105 // second, relative to the order they would be iterated over by using
106 // AXRange::Iterator to traverse all leaf text ranges in a tree.
107 //
108 // Notice that this method is different from using AXPosition::CompareTo since
109 // the following logic takes into account BOTH tree pre-order traversal and
110 // text offsets when both positions are located within the same anchor.
111 //
112 // Returns:
113 // 0 - If both positions are equivalent.
114 // <0 - If the first position would come BEFORE the second.
115 // >0 - If the first position would come AFTER the second.
116 // nullopt - If positions are not comparable (see AXPosition::CompareTo).
117 static std::optional<int> CompareEndpoints(const AXPositionType* first,
118 const AXPositionType* second) {
119 std::optional<int> tree_position_comparison =
120 first->AsTreePosition()->CompareTo(*second->AsTreePosition());
121
122 // When the tree comparison is nullopt, using value_or(1) forces a default
123 // value of 1, making the following statement return nullopt as well.
124 return (tree_position_comparison.value_or(1) != 0)
125 ? tree_position_comparison
126 : first->CompareTo(*second);
127 }
128
130 return (CompareEndpoints(anchor(), focus()).value_or(0) > 0)
131 ? AXRange(focus_->Clone(), anchor_->Clone())
132 : AXRange(anchor_->Clone(), focus_->Clone());
133 }
134
136 return (CompareEndpoints(anchor(), focus()).value_or(0) < 0)
137 ? AXRange(focus_->Clone(), anchor_->Clone())
138 : AXRange(anchor_->Clone(), focus_->Clone());
139 }
140
141 bool IsCollapsed() const { return !IsNull() && *anchor_ == *focus_; }
142
143 // We define a "leaf text range" as an AXRange whose endpoints are leaf text
144 // positions located within the same anchor of the AXTree.
145 bool IsLeafTextRange() const {
146 return !IsNull() && anchor_->GetAnchor() == focus_->GetAnchor() &&
147 anchor_->IsLeafTextPosition() && focus_->IsLeafTextPosition();
148 }
149
150 bool IsNull() const {
151 BASE_DCHECK(anchor_ && focus_);
152 return anchor_->IsNullPosition() || focus_->IsNullPosition();
153 }
154
155 std::string ToString() const {
156 return "Range\nAnchor:" + anchor_->ToString() +
157 "\nFocus:" + focus_->ToString();
158 }
159
160 // We can decompose any given AXRange into multiple "leaf text ranges".
161 // As an example, consider the following HTML code:
162 //
163 // <p>line with text<br><input type="checkbox">line with checkbox</p>
164 //
165 // It will produce the following AXTree; notice that the leaf text nodes
166 // (enclosed in parenthesis) compose its text representation:
167 //
168 // paragraph
169 // staticText name='line with text'
170 // (inlineTextBox name='line with text')
171 // lineBreak name='<newline>'
172 // (inlineTextBox name='<newline>')
173 // (checkBox)
174 // staticText name='line with checkbox'
175 // (inlineTextBox name='line with checkbox')
176 //
177 // Suppose we have an AXRange containing all elements from the example above.
178 // The text representation of such range, with AXRange's endpoints marked by
179 // opening and closing brackets, will look like the following:
180 //
181 // "[line with text\n{checkBox}line with checkbox]"
182 //
183 // Note that in the text representation {checkBox} is not visible, but it is
184 // effectively a "leaf text range", so we include it in the example above only
185 // to visualize how the iterator should work.
186 //
187 // Decomposing the AXRange above into its "leaf text ranges" would result in:
188 //
189 // "[line with text][\n][{checkBox}][line with checkbox]"
190 //
191 // This class allows AXRange to be iterated through all "leaf text ranges"
192 // contained between its endpoints, composing the entire range.
193 class Iterator : public std::iterator<std::input_iterator_tag, AXRange> {
194 public:
196 : current_start_(AXPositionType::CreateNullPosition()),
197 iterator_end_(AXPositionType::CreateNullPosition()) {}
198
200 if (end && !end->IsNullPosition()) {
201 current_start_ = !start ? AXPositionType::CreateNullPosition()
202 : start->AsLeafTextPosition();
203 iterator_end_ = end->AsLeafTextPosition();
204 } else {
205 current_start_ = AXPositionType::CreateNullPosition();
206 iterator_end_ = AXPositionType::CreateNullPosition();
207 }
208 }
209
210 Iterator(const Iterator& other) = delete;
211
213 : current_start_(std::move(other.current_start_)),
214 iterator_end_(std::move(other.iterator_end_)) {}
215
216 ~Iterator() = default;
217
218 bool operator==(const Iterator& other) const {
219 return current_start_->GetAnchor() == other.current_start_->GetAnchor() &&
220 iterator_end_->GetAnchor() == other.iterator_end_->GetAnchor() &&
221 *current_start_ == *other.current_start_ &&
222 *iterator_end_ == *other.iterator_end_;
223 }
224
225 bool operator!=(const Iterator& other) const { return !(*this == other); }
226
227 // Only forward iteration is supported, so operator-- is not implemented.
229 BASE_DCHECK(!current_start_->IsNullPosition());
230 if (current_start_->GetAnchor() == iterator_end_->GetAnchor()) {
231 current_start_ = AXPositionType::CreateNullPosition();
232 } else {
233 current_start_ = current_start_->CreateNextLeafTreePosition();
234 BASE_DCHECK(*current_start_ <= *iterator_end_);
235 }
236 return *this;
237 }
238
240 BASE_DCHECK(!current_start_->IsNullPosition());
241 AXPositionInstance current_end =
242 (current_start_->GetAnchor() != iterator_end_->GetAnchor())
243 ? current_start_->CreatePositionAtEndOfAnchor()
244 : iterator_end_->Clone();
245 BASE_DCHECK(*current_end <= *iterator_end_);
246
247 AXRange current_leaf_text_range(current_start_->AsTextPosition(),
248 current_end->AsTextPosition());
249 BASE_DCHECK(current_leaf_text_range.IsLeafTextRange());
250 return std::move(current_leaf_text_range);
251 }
252
253 private:
254 AXPositionInstance current_start_;
255 AXPositionInstance iterator_end_;
256 };
257
258 Iterator begin() const {
259 if (IsNull())
260 return Iterator(nullptr, nullptr);
261 AXRange forward_range = AsForwardRange();
262 return Iterator(std::move(forward_range.anchor_),
263 std::move(forward_range.focus_));
264 }
265
266 Iterator end() const {
267 if (IsNull())
268 return Iterator(nullptr, nullptr);
269 AXRange forward_range = AsForwardRange();
270 return Iterator(nullptr, std::move(forward_range.focus_));
271 }
272
273 // Returns the concatenation of the accessible names of all text nodes
274 // contained between this AXRange's endpoints.
275 // Pass a |max_count| of -1 to retrieve all text in the AXRange.
276 // Note that if this AXRange has its anchor or focus located at an ignored
277 // position, we shrink the range to the closest unignored positions.
278 std::u16string GetText(AXTextConcatenationBehavior concatenation_behavior =
280 int max_count = -1,
281 bool include_ignored = false,
282 size_t* appended_newlines_count = nullptr) const {
283 if (max_count == 0 || IsNull())
284 return std::u16string();
285
286 std::optional<int> endpoint_comparison =
288 if (!endpoint_comparison)
289 return std::u16string();
290
291 AXPositionInstance start = (endpoint_comparison.value() < 0)
292 ? anchor_->AsLeafTextPosition()
293 : focus_->AsLeafTextPosition();
294 AXPositionInstance end = (endpoint_comparison.value() < 0)
295 ? focus_->AsLeafTextPosition()
296 : anchor_->AsLeafTextPosition();
297
298 std::u16string range_text;
299 size_t computed_newlines_count = 0;
300 bool is_first_non_whitespace_leaf = true;
301 bool crossed_paragraph_boundary = false;
302 bool is_first_unignored_leaf = true;
303 bool found_trailing_newline = false;
304
305 while (!start->IsNullPosition()) {
306 BASE_DCHECK(start->IsLeafTextPosition());
307 BASE_DCHECK(start->text_offset() >= 0);
308
309 if (include_ignored || !start->IsIgnored()) {
310 if (concatenation_behavior ==
312 !start->IsInWhiteSpace()) {
313 if (is_first_non_whitespace_leaf) {
314 // The first non-whitespace leaf in the range could be preceded by
315 // whitespace spanning even before the start of this range, we need
316 // to check such positions in order to correctly determine if this
317 // is a paragraph's start (see |AXPosition::AtStartOfParagraph|).
318 crossed_paragraph_boundary =
319 !is_first_unignored_leaf && start->AtStartOfParagraph();
320 }
321
322 // When preserving layout line breaks, don't append `\n` next if the
323 // previous leaf position was a <br> (already ending with a newline).
324 if (crossed_paragraph_boundary && !found_trailing_newline) {
325 range_text += base::ASCIIToUTF16("\n");
326 computed_newlines_count++;
327 }
328
329 is_first_non_whitespace_leaf = false;
330 crossed_paragraph_boundary = false;
331 }
332
333 int current_end_offset = (start->GetAnchor() != end->GetAnchor())
334 ? start->MaxTextOffset()
335 : end->text_offset();
336
337 if (current_end_offset > start->text_offset()) {
338 int characters_to_append =
339 (max_count > 0)
340 ? std::min(max_count - static_cast<int>(range_text.length()),
341 current_end_offset - start->text_offset())
342 : current_end_offset - start->text_offset();
343
344 range_text += start->GetText().substr(start->text_offset(),
345 characters_to_append);
346
347 // Collapse all whitespace following any line break.
348 found_trailing_newline =
349 start->IsInLineBreak() ||
350 (found_trailing_newline && start->IsInWhiteSpace());
351 }
352
353 BASE_DCHECK(max_count < 0 ||
354 static_cast<int>(range_text.length()) <= max_count);
355 is_first_unignored_leaf = false;
356 }
357
358 if (start->GetAnchor() == end->GetAnchor() ||
359 static_cast<int>(range_text.length()) == max_count) {
360 break;
361 } else if (concatenation_behavior ==
363 !crossed_paragraph_boundary && !is_first_non_whitespace_leaf) {
364 start = start->CreateNextLeafTextPosition(&crossed_paragraph_boundary);
365 } else {
366 start = start->CreateNextLeafTextPosition();
367 }
368 }
369
370 if (appended_newlines_count)
371 *appended_newlines_count = computed_newlines_count;
372 return range_text;
373 }
374
375 // Appends rects of all anchor nodes that span between anchor_ and focus_.
376 // Rects outside of the viewport are skipped.
377 // Coordinate system is determined by the passed-in delegate.
378 std::vector<gfx::Rect> GetRects(AXRangeRectDelegate* delegate) const {
379 std::vector<gfx::Rect> rects;
380
381 for (const AXRange& leaf_text_range : *this) {
382 BASE_DCHECK(leaf_text_range.IsLeafTextRange());
383 AXPositionType* current_line_start = leaf_text_range.anchor();
384 AXPositionType* current_line_end = leaf_text_range.focus();
385
386 // For text anchors, we retrieve the bounding rectangles of its text
387 // content. For non-text anchors (such as checkboxes, images, etc.), we
388 // want to directly retrieve their bounding rectangles.
389 AXOffscreenResult offscreen_result;
390 gfx::Rect current_rect =
391 (current_line_start->IsInLineBreak() ||
392 current_line_start->IsInTextObject())
393 ? delegate->GetInnerTextRangeBoundsRect(
394 current_line_start->tree_id(),
395 current_line_start->anchor_id(),
396 current_line_start->text_offset(),
397 current_line_end->text_offset(),
398 ui::AXClippingBehavior::kUnclipped, &offscreen_result)
399 : delegate->GetBoundsRect(current_line_start->tree_id(),
400 current_line_start->anchor_id(),
401 &offscreen_result);
402
403 // If the bounding box of the current range is clipped because it lies
404 // outside an ancestor’s bounds, then the bounding box is pushed to the
405 // nearest edge of such ancestor's bounds, with its width and height
406 // forced to be 1, and the node will be marked as "offscreen".
407 //
408 // Only add rectangles that are not empty and not marked as "offscreen".
409 //
410 // See the documentation for how bounding boxes are calculated in AXTree:
411 // https://chromium.googlesource.com/chromium/src/+/HEAD/docs/accessibility/offscreen.md
412 if (!current_rect.IsEmpty() &&
413 offscreen_result == AXOffscreenResult::kOnscreen)
414 rects.push_back(current_rect);
415 }
416 return rects;
417 }
418
419 private:
420 AXPositionInstance anchor_;
421 AXPositionInstance focus_;
422};
423
424template <class AXPositionType>
425std::ostream& operator<<(std::ostream& stream,
426 const AXRange<AXPositionType>& range) {
427 return stream << range.ToString();
428}
429
430} // namespace ui
431
432#endif // UI_ACCESSIBILITY_AX_RANGE_H_
Definition: rect.h:36
bool IsEmpty() const
Definition: rect.h:140
int32_t AXID
Definition: ax_node.h:36
virtual gfx::Rect GetInnerTextRangeBoundsRect(AXTreeID tree_id, AXNode::AXID node_id, int start_offset, int end_offset, ui::AXClippingBehavior clipping_behavior, AXOffscreenResult *offscreen_result)=0
virtual gfx::Rect GetBoundsRect(AXTreeID tree_id, AXNode::AXID node_id, AXOffscreenResult *offscreen_result)=0
bool operator!=(const Iterator &other) const
Definition: ax_range.h:225
Iterator(AXPositionInstance start, AXPositionInstance end)
Definition: ax_range.h:199
AXRange operator*() const
Definition: ax_range.h:239
Iterator(Iterator &&other)
Definition: ax_range.h:212
Iterator & operator++()
Definition: ax_range.h:228
bool operator==(const Iterator &other) const
Definition: ax_range.h:218
Iterator(const Iterator &other)=delete
Iterator begin() const
Definition: ax_range.h:258
AXPositionType * anchor() const
Definition: ax_range.h:73
AXRange & operator=(const AXRange &other)=delete
std::vector< gfx::Rect > GetRects(AXRangeRectDelegate *delegate) const
Definition: ax_range.h:378
static std::optional< int > CompareEndpoints(const AXPositionType *first, const AXPositionType *second)
Definition: ax_range.h:117
std::string ToString() const
Definition: ax_range.h:155
AXRange AsForwardRange() const
Definition: ax_range.h:129
bool IsLeafTextRange() const
Definition: ax_range.h:145
virtual ~AXRange()=default
std::u16string GetText(AXTextConcatenationBehavior concatenation_behavior=AXTextConcatenationBehavior::kAsTextContent, int max_count=-1, bool include_ignored=false, size_t *appended_newlines_count=nullptr) const
Definition: ax_range.h:278
Iterator end() const
Definition: ax_range.h:266
AXRange(AXRange &&other)
Definition: ax_range.h:66
std::unique_ptr< AXPositionType > AXPositionInstance
Definition: ax_range.h:53
AXPositionType * focus() const
Definition: ax_range.h:78
bool IsNull() const
Definition: ax_range.h:150
AXRange(AXPositionInstance anchor, AXPositionInstance focus)
Definition: ax_range.h:59
bool operator==(const AXRange &other) const
Definition: ax_range.h:95
AXRange & operator=(AXRange &&other)
Definition: ax_range.h:85
AXRange(const AXRange &other)=delete
bool IsCollapsed() const
Definition: ax_range.h:141
bool operator!=(const AXRange &other) const
Definition: ax_range.h:102
AXRange AsBackwardRange() const
Definition: ax_range.h:135
static float min(float r, float g, float b)
Definition: hsl.cpp:48
std::u16string ASCIIToUTF16(std::string src)
Definition: string_utils.cc:63
Definition: ref_ptr.h:256
std::ostream & operator<<(std::ostream &os, AXEventGenerator::Event event)
AXTextConcatenationBehavior
Definition: ax_range.h:25
#define BASE_DCHECK(condition)
Definition: logging.h:63