// 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_POSITION_H_ #define UI_ACCESSIBILITY_AX_POSITION_H_ #include #include #include #include #include #include #include #include #include #include "base/containers/contains.h" #include "base/containers/fixed_flat_map.h" #include "base/containers/stack.h" #include "base/i18n/break_iterator.h" #include "base/no_destructor.h" #include "base/notreached.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "third_party/abseil-cpp/absl/types/optional.h" #include "ui/accessibility/ax_common.h" #include "ui/accessibility/ax_enum_util.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node.h" #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/ax_role_properties.h" #include "ui/accessibility/ax_table_info.h" #include "ui/accessibility/ax_text_attributes.h" #include "ui/accessibility/ax_tree_id.h" #include "ui/accessibility/ax_tree_manager.h" #include "ui/gfx/utf16_indexing.h" namespace ui { // Defines the type of position in the accessibility tree. // A tree position is used when referring to a specific child of a node in the // accessibility tree. // A text position is used when referring to a specific character of text inside // a particular node. // A null position is used to signify that the provided data is invalid or that // a boundary has been reached. enum class AXPositionKind { NULL_POSITION, TREE_POSITION, TEXT_POSITION }; // Defines how creating the next or previous position should behave whenever we // are at or are crossing a text boundary, (such as the start of a word or the // end of a sentence), or whenever we are crossing the initial position's // anchor. Note that the "anchor" is the node to which an AXPosition is attached // to. It is provided when a position is created. enum class AXBoundaryBehavior { // Crosses all boundaries. If the bounds of the current window-like container, // such as the current webpage, have been reached, returns a null position. kCrossBoundary, // Stops if the current anchor is crossed, regardless of how the resulting // position has been computed. For example, even though in order to find the // next or previous word start in a text field we need to descend to the leaf // equivalent position, this behavior will only stop when the bounds of the // original anchor, i.e. the text field, have been crossed. kStopAtAnchorBoundary, // Stops if we have reached the start or the end of of a window-like // container, such as a webpage, a PDF, a dialog, the browser's UI (AKA // Views), or the whole desktop. kStopAtLastAnchorBoundary }; // Defines whether moving to the next or previous position should consider the // initial position before testing for the given boundary/behavior. // kCheckInitialPosition should be used if the current position should be // maintained if it meets the boundary criteria. Otherwise, // kDontCheckInitialPosition will move to the next/previous position before // testing for the specified boundary. enum class AXBoundaryDetection { kCheckInitialPosition, kDontCheckInitialPosition, }; struct AXMovementOptions { AXMovementOptions(AXBoundaryBehavior boundary, AXBoundaryDetection detection) : boundary_behavior(boundary), boundary_detection(detection) {} AXBoundaryBehavior boundary_behavior; AXBoundaryDetection boundary_detection; }; // Describes in further detail what type of boundary a current position is on. // // For complex boundaries such as format boundaries, it can be useful to know // why a particular boundary was chosen. enum class AXBoundaryType { // Not at a unit boundary. kNone, // At a unit boundary (e.g. a format boundary). kUnitBoundary, // At the start of the whole content, possibly spanning multiple accessibility // trees. kContentStart, // At the end of the whole content, possibly spanning multiple accessibility // trees. kContentEnd }; // When converting to an unignored position, determines how to adjust the new // position in order to make it valid, either moving backward or forward in // the accessibility tree. enum class AXPositionAdjustmentBehavior { kMoveBackward, kMoveForward }; // Specifies how AXPosition::ExpandToEnclosingTextBoundary behaves. // // As an example, imagine we have the text "hello world" and a position before // the space character. We want to expand to the surrounding word boundary. // Since we are right at the end of the first word, we could either expand to // the left first, find the start of the first word and then use that to find // the corresponding word end, resulting in the word "Hello". Another // possibility is to expand to the right first, find the end of the next word // and use that as our starting point to find the previous word start, resulting // in the word "world". enum class AXRangeExpandBehavior { // Expands to the left boundary first and then uses that position as the // starting point to find the boundary to the right. kLeftFirst, // Expands to the right boundary first and then uses that position as the // starting point to find the boundary to the left. kRightFirst }; // Some platforms require most objects, including empty objects, to be // represented by an "embedded object character" in order for text navigation to // work correctly. This enum controls whether a replacement character will be // exposed for such objects. // // When an embedded object is replaced by this special character, the // expectations are the same with this character as with other ordinary // characters. // // For example, with UIA on Windows, we need to be able to navigate inside and // outside of this character as if it was an ordinary character, using the // `AXPlatformNodeTextRangeProvider` methods. Since an "embedded object // character" is the only character in a node, we also treat this character as a // word. enum class AXEmbeddedObjectBehavior { kExposeCharacter, kSuppressCharacter, }; // Controls whether embedded objects are represented by a replacement // character. This is initialized to a per-platform default but can be // overridden for testing. // // On some platforms, most objects are represented in the text of their parents // with a special "embedded object character" and not with their actual text // contents. Also on the same platforms, if a node has only ignored descendants, // i.e., it appears to be empty to assistive software, we need to treat it as a // character and a word boundary. For example, an empty text field should act as // a character and a word boundary when a screen reader user tries to navigate // through it, otherwise the text field would be missed by the user. // // Tests should use ScopedAXEmbeddedObjectBehaviorSetter to change this. // TODO(crbug.com/1204592) Don't export this so tests can't change it. extern AX_EXPORT AXEmbeddedObjectBehavior g_ax_embedded_object_behavior; class AX_EXPORT ScopedAXEmbeddedObjectBehaviorSetter { public: explicit ScopedAXEmbeddedObjectBehaviorSetter( AXEmbeddedObjectBehavior behavior); ~ScopedAXEmbeddedObjectBehaviorSetter(); private: AXEmbeddedObjectBehavior prev_behavior_; }; // Forward declarations. template class AXPosition; template class AXRange; template bool operator==(const AXPosition& first, const AXPosition& second); template bool operator!=(const AXPosition& first, const AXPosition& second); // A position in the accessibility tree. // // This class could either represent a tree position or a text position. // Tree positions point to either a child of a specific node or at the end of a // node (i.e. an "after children" position). // Text positions point to either a character offset in the text inside a // particular node including text from all its children, or to the end of the // node's text, (i.e. an "after text" position). // On tree positions that have a leaf node as their anchor, we also need to // distinguish between "before text" and "after text" positions. To do this, if // the child index is 0 and the anchor is a leaf node, then it's an "after text" // position. If the child index is |BEFORE_TEXT| and the anchor is a leaf node, // then this is a "before text" position. // It doesn't make sense to have a "before text" position on a text position, // because it is identical to setting its offset to the first character. // // To avoid re-computing either the text offset or the child index when // converting between the two types of positions, both values are saved after // the first conversion. // // This class template uses static polymorphism in order to allow sub-classes to // be created from the base class without the base class knowing the type of the // sub-class in advance. // The template argument |AXPositionType| should always be set to the type of // any class that inherits from this template, making this a // "curiously recursive template". // // This class can be copied using the |Clone| method. It is designed to be // immutable. template class AXPosition { public: using AXPositionInstance = std::unique_ptr>; using AXRangeType = AXRange>; using BoundaryConditionPredicate = base::RepeatingCallback; using BoundaryTextOffsetsFunc = base::RepeatingCallback&( const AXPositionInstance&)>; static const int BEFORE_TEXT = -1; static const int INVALID_INDEX = -2; static const int INVALID_OFFSET = -1; static AXPositionInstance CreateNullPosition() { AXPositionInstance new_position(new AXPositionType()); new_position->Initialize(AXPositionKind::NULL_POSITION, AXTreeIDUnknown(), kInvalidAXNodeID, INVALID_INDEX, INVALID_OFFSET, ax::mojom::TextAffinity::kDownstream); return new_position; } static AXPositionInstance CreateTreePosition(AXTreeID tree_id, AXNodeID anchor_id, int child_index) { AXPositionInstance new_position(new AXPositionType()); new_position->Initialize(AXPositionKind::TREE_POSITION, tree_id, anchor_id, child_index, INVALID_OFFSET, ax::mojom::TextAffinity::kDownstream); return new_position; } static AXPositionInstance CreateTreePosition(const AXTree& tree, const AXNode& anchor, int child_index) { return CreateTreePosition(tree.GetAXTreeID(), anchor.id(), child_index); } static AXPositionInstance CreateTreePositionAtStartOfAnchor( const AXNode& anchor) { // Initialize the child index: // - For a leaf, the child index will be BEFORE_TEXT. // - Otherwise the child index will be 0. int child_index = IsLeafNodeForTreePosition(anchor) ? BEFORE_TEXT : 0; return CreateTreePosition(*anchor.tree(), anchor, child_index); } static AXPositionInstance CreateTreePositionAtEndOfAnchor( const AXNode& anchor) { // Initialize the child index to the anchor's child count. return CreateTreePosition(*anchor.tree(), anchor, anchor.GetChildCountCrossingTreeBoundary()); } static AXPositionInstance CreateTextPosition( AXTreeID tree_id, AXNodeID anchor_id, int text_offset, ax::mojom::TextAffinity affinity) { AXPositionInstance new_position(new AXPositionType()); new_position->Initialize(AXPositionKind::TEXT_POSITION, tree_id, anchor_id, INVALID_INDEX, text_offset, affinity); return new_position; } static AXPositionInstance CreateTextPosition( const AXNode& anchor, int text_offset, ax::mojom::TextAffinity affinity) { DCHECK(anchor.tree()); DCHECK_NE(anchor.tree()->GetAXTreeID(), AXTreeIDUnknown()); DCHECK_NE(anchor.id(), kInvalidAXNodeID); return CreateTextPosition(anchor.tree()->GetAXTreeID(), anchor.id(), text_offset, affinity); } virtual ~AXPosition() = default; // Implemented based on the copy and swap idiom. AXPosition& operator=(const AXPosition& other) { AXPositionInstance clone = other.Clone(); swap(*clone); return *this; } virtual AXPositionInstance Clone() const = 0; AXPositionInstance CloneWithDownstreamAffinity() const { if (!IsTextPosition()) { NOTREACHED() << "Only text positions have affinity."; return CreateNullPosition(); } AXPositionInstance clone_with_downstream_affinity = Clone(); clone_with_downstream_affinity->affinity_ = ax::mojom::TextAffinity::kDownstream; return clone_with_downstream_affinity; } AXPositionInstance CloneWithUpstreamAffinity() const { if (!IsTextPosition()) { NOTREACHED() << "Only text positions have affinity."; return CreateNullPosition(); } AXPositionInstance clone_with_upstream_affinity = Clone(); clone_with_upstream_affinity->affinity_ = ax::mojom::TextAffinity::kUpstream; return clone_with_upstream_affinity; } // A serialization of a position as POD. Not for sharing on disk or sharing // across thread or process boundaries, just for passing a position to an // API that works with positions as opaque objects. struct SerializedPosition { AXPositionKind kind; AXNodeID anchor_id; int child_index; int text_offset; ax::mojom::TextAffinity affinity; char tree_id[33]; }; static_assert(std::is_trivially_copyable::value, "SerializedPosition must be POD"); SerializedPosition Serialize() { SerializedPosition result; result.kind = kind_; // A tree ID can be serialized as a 32-byte string. std::string tree_id_string = tree_id_.ToString(); DCHECK_LE(tree_id_string.size(), 32U); strncpy(result.tree_id, tree_id_string.c_str(), 32); result.tree_id[32] = 0; result.anchor_id = anchor_id_; result.child_index = child_index_; result.text_offset = text_offset_; result.affinity = affinity_; return result; } static AXPositionInstance Unserialize( const SerializedPosition& serialization) { AXPositionInstance new_position(new AXPositionType()); // Use initialize without validation because this is used by ATs that // used outdated information to generated a selection request. new_position->InitializeWithoutValidation( serialization.kind, ui::AXTreeID::FromString(serialization.tree_id), serialization.anchor_id, serialization.child_index, serialization.text_offset, serialization.affinity); return new_position; } std::string ToString() const { std::string str; switch (kind_) { case AXPositionKind::NULL_POSITION: return "NullPosition"; case AXPositionKind::TREE_POSITION: { std::string str_child_index; if (child_index_ == BEFORE_TEXT) { str_child_index = "before_text"; } else if (child_index_ == INVALID_INDEX) { str_child_index = "invalid"; } else { str_child_index = base::NumberToString(child_index_); } str = "TreePosition tree_id=" + tree_id_.ToString() + " anchor_id=" + base::NumberToString(anchor_id_) + " child_index=" + str_child_index; break; } case AXPositionKind::TEXT_POSITION: { std::string str_text_offset; if (text_offset_ == INVALID_OFFSET) { str_text_offset = "invalid"; } else { str_text_offset = base::NumberToString(text_offset_); } str = "TextPosition anchor_id=" + base::NumberToString(anchor_id_) + " text_offset=" + str_text_offset + " affinity=" + ui::ToString(static_cast(affinity_)); break; } } if (!IsTextPosition() || text_offset_ < 0 || text_offset_ > MaxTextOffset()) return str; const std::u16string& text = GetText(); DCHECK_GE(text_offset_, 0); const size_t max_text_offset = text.size(); DCHECK_LE(text_offset_, static_cast(max_text_offset)) << text; std::u16string annotated_text; if (text_offset_ == static_cast(max_text_offset)) { annotated_text = text + u"<>"; } else { // TODO(aleventhal) This extra casting is only necessary to satisfy a // compiler error that strangely occurs only when Initialize() contains // SnapToMaxTextOffsetIfBeyond(). size_t unsigned_text_offset = static_cast(text_offset_); annotated_text = text.substr(0, unsigned_text_offset) + u"<" + text[unsigned_text_offset] + u">" + text.substr(unsigned_text_offset + 1); } return str + " annotated_text=" + base::UTF16ToUTF8(annotated_text); } // Helper for logging the position, the AXTreeManager and the anchor node. std::string ToDebugString() const { std::ostringstream str; str << "* Position: " << ToString(); if (GetAnchor()) { str << "\n* Anchor node: " << *GetAnchor(); if (IsTreePosition()) { str << "\n* AnchorChildCount(): " << AnchorChildCount() << "\n* IsLeaf(): " << IsLeaf(); } else { str << "\n* TextOffset: " << text_offset() << "\n* MaxTextOffset: " << MaxTextOffset(); } } if (GetManager()) str << "\n* Tree: " << GetManager()->ax_tree()->data().ToString(); return str.str(); } AXPositionKind kind() const { return kind_; } AXTreeID tree_id() const { return tree_id_; } AXNodeID anchor_id() const { return anchor_id_; } AXTreeManager* GetManager() const { return AXTreeManager::FromID(tree_id()); } // Returns true if this position is within an "empty object", i.e. within a // node that should contribute no text to the accessibility tree's text // representation. For example, returns true if this position is within an // empty control, such as an empty text field or (on Windows) a collapsed // popup menu. On some platforms, such nodes need to be represented by an // "object replacement character". This character is inserted purely for // navigational purposes. This is because empty controls still need to act as // a word and character boundary on those platforms. static bool IsEmptyObject(const AXNode& node) { // A collapsed popup button that contains a menu list popup (i.e, the exact // subtree representation we get from a collapsed text field, because all // of the accessibility subtree inside the text field is hidden from // platform APIs. An example of how an ignored node can affect the // hypertext of an unignored ancestor is shown below: // ++kTextField "Hello" // ++++kGenericContainer ignored "Hello" // ++++++kStaticText "Hello" // ++++++++kInlineTextBox "Hello" // The generic container, even though it is ignored, should nevertheless // maintain the text of its static text child and not use an "object // replacement character". Otherwise, the value of the text field would // be wrong. // // Please note that there is one more method that controls whether an // "object replacement character" would be exposed. See // `AXPosition::IsInUnignoredEmptyObject()`. return !IsNullPosition() && !GetAnchor()->IsIgnored() && !GetAnchor()->IsText() && !GetAnchor()->IsChildOfLeaf(); } } // Determines if the anchor containing this position produces a hard line // break in the text representation, e.g. the anchor is a block level element // or a
. bool IsInLineBreakingObject() const { if (IsNullPosition()) return false; return GetAnchor()->GetBoolAttribute( ax::mojom::BoolAttribute::kIsLineBreakingObject); } ax::mojom::Role GetAnchorRole() const { if (IsNullPosition()) return ax::mojom::Role::kUnknown; return GetRole(GetAnchor()); } ax::mojom::Role GetRole(AXNode* node) const { return node->GetRole(); } AXTextAttributes GetTextAttributes() const { // Check either the current anchor or its parent for text attributes. AXTextAttributes current_anchor_text_attributes = !IsNullPosition() ? GetAnchor()->GetTextAttributes() : AXTextAttributes(); if (current_anchor_text_attributes.IsUnset()) { AXPositionInstance parent_position = AsTreePosition()->CreateParentPosition( ax::mojom::MoveDirection::kBackward); if (!parent_position->IsNullPosition()) return parent_position->GetAnchor()->GetTextAttributes(); } return current_anchor_text_attributes; } const std::vector& GetWordStartOffsets() const { if (IsNullPosition()) { static const base::NoDestructor> empty_word_starts; return *empty_word_starts; } DCHECK(GetAnchor()); // An embedded object replacement character is exposed in a node's text // representation when a control, such as a text field, is empty. Since the // control has no text, no word start offsets are present in the // `ax::mojom::IntListAttribute::kWordStarts` attribute, so we need to // special case them here. if (g_ax_embedded_object_behavior == AXEmbeddedObjectBehavior::kExposeCharacter && IsInUnignoredEmptyObject()) { // Using braces ensures that the vector will contain the given value, and // not create a vector of size 0. static const base::NoDestructor> embedded_word_starts{{0}}; return *embedded_word_starts; } return GetAnchor()->GetIntListAttribute( ax::mojom::IntListAttribute::kWordStarts); } const std::vector& GetWordEndOffsets() const { if (IsNullPosition()) { static const base::NoDestructor> empty_word_ends; return *empty_word_ends; } DCHECK(GetAnchor()); // An embedded object replacement character is exposed in a node's text // representation when a control, such as a text field, is empty. Since the // control has no text, no word end offsets are present in the // `ax::mojom::IntListAttribute::kWordEnds` attribute, so we need to special // case them here. // // Since the whole text exposed inside of an embedded object is of // length 1 (the embedded object replacement character), the word end offset // is positioned at 1. Because we want to treat embedded object replacement // characters as ordinary characters, it wouldn't be consistent to assume // they have no length and return 0 instead of 1. if (g_ax_embedded_object_behavior == AXEmbeddedObjectBehavior::kExposeCharacter && IsInUnignoredEmptyObject()) { // Using braces ensures that the vector will contain the given value, and // not create a vector of size 1. static const base::NoDestructor> embedded_word_ends{ {1}}; return *embedded_word_ends; } return GetAnchor()->GetIntListAttribute( ax::mojom::IntListAttribute::kWordEnds); } AXNodeID GetNextOnLineID() const { if (IsNullPosition()) return kInvalidAXNodeID; DCHECK(GetAnchor()); int next_on_line_id; if (GetAnchor()->GetIntAttribute(ax::mojom::IntAttribute::kNextOnLineId, &next_on_line_id)) { return static_cast(next_on_line_id); } return kInvalidAXNodeID; } AXNodeID GetPreviousOnLineID() const { if (IsNullPosition()) return kInvalidAXNodeID; DCHECK(GetAnchor()); int previous_on_line_id; if (GetAnchor()->GetIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, &previous_on_line_id)) { return static_cast(previous_on_line_id); } return kInvalidAXNodeID; } private: // Defines the relationship between positions during traversal. // For example, moving from a descendant to an ancestor, is a kAncestor move. enum class AXMoveType { kAncestor, kDescendant, kSibling, }; // Defines the direction of position movement, either next / previous in tree. enum class AXMoveDirection { kNextInTree, kPreviousInTree, }; // Type of predicate function called during anchor navigation. // When the predicate returns |true|, the navigation stops and returns a // null position object. using AbortMovePredicate = base::RepeatingCallback; // A text span is defined by a series of inline text boxes that make up a // single static text object. bool AtEndOfTextSpan() const { if (GetAnchorRole() != ax::mojom::Role::kInlineTextBox || !AtEndOfAnchor()) return false; // We are at the end of text span if |this| position has // role::kInlineTextBox, the parent of |this| has role::kStaticText, and the // anchor node of |this| is the last child of its parent's children. const bool is_last_child = AnchorIndexInParent() == (GetAnchorSiblingCount() - 1); DCHECK(GetAnchor()); return is_last_child && GetRole(GetAnchor()->GetParentCrossingTreeBoundary()) == ax::mojom::Role::kStaticText; } // Uses depth-first pre-order traversal. AXPositionInstance CreateNextAnchorPosition( const AbortMovePredicate& abort_predicate) const { if (IsNullPosition()) return Clone(); AXPositionInstance current_position = AsTreePosition(); DCHECK(!current_position->IsNullPosition()); if (!IsLeaf()) { const int child_index = current_position->child_index_; if (child_index < current_position->AnchorChildCount()) { AXPositionInstance child_position = current_position->CreateChildPositionAt(child_index); if (abort_predicate.Run(*current_position, *child_position, AXMoveType::kDescendant, AXMoveDirection::kNextInTree)) { return CreateNullPosition(); } return child_position; } } AXPositionInstance parent_position = current_position->CreateParentPosition(); // Get the next sibling if it exists, otherwise move up the AXTree to the // lowest next sibling of this position's ancestors. while (!parent_position->IsNullPosition()) { const int index_in_parent = current_position->AnchorIndexInParent(); if (index_in_parent + 1 < parent_position->AnchorChildCount()) { AXPositionInstance next_sibling = parent_position->CreateChildPositionAt(index_in_parent + 1); DCHECK(!next_sibling->IsNullPosition()); if (abort_predicate.Run(*current_position, *next_sibling, AXMoveType::kSibling, AXMoveDirection::kNextInTree)) { return CreateNullPosition(); } return next_sibling; } if (abort_predicate.Run(*current_position, *parent_position, AXMoveType::kAncestor, AXMoveDirection::kNextInTree)) { return CreateNullPosition(); } current_position = std::move(parent_position); parent_position = current_position->CreateParentPosition(); } return CreateNullPosition(); } // Uses depth-first pre-order traversal. AXPositionInstance CreatePreviousAnchorPosition( const AbortMovePredicate& abort_predicate) const { if (IsNullPosition()) return Clone(); AXPositionInstance current_position = AsTreePosition(); DCHECK(!current_position->IsNullPosition()); AXPositionInstance parent_position = current_position->CreateParentPosition(); if (parent_position->IsNullPosition()) return parent_position; // If there is no previous sibling, or the parent itself is a leaf, move up // to the parent. The parent can be a leaf if we start with a tree position // that is a descendant of a node that is an empty control represented by an // "object replacement character" (see `IsInUnignoredEmptyObject()`). const int index_in_parent = current_position->AnchorIndexInParent(); if (index_in_parent <= 0 || parent_position->IsLeaf()) { if (abort_predicate.Run(*current_position, *parent_position, AXMoveType::kAncestor, AXMoveDirection::kPreviousInTree)) { return CreateNullPosition(); } return parent_position; } // Get the previous sibling's deepest last child. AXPositionInstance rightmost_leaf = parent_position->CreateChildPositionAt(index_in_parent - 1); DCHECK(!rightmost_leaf->IsNullPosition()); if (abort_predicate.Run(*current_position, *rightmost_leaf, AXMoveType::kSibling, AXMoveDirection::kPreviousInTree)) { return CreateNullPosition(); } CHECK(!rightmost_leaf->IsNullPosition()); while (!rightmost_leaf->IsLeaf()) { parent_position = std::move(rightmost_leaf); rightmost_leaf = parent_position->CreateChildPositionAt( parent_position->AnchorChildCount() - 1); DCHECK(!rightmost_leaf->IsNullPosition()); if (abort_predicate.Run(*parent_position, *rightmost_leaf, AXMoveType::kDescendant, AXMoveDirection::kPreviousInTree)) { return CreateNullPosition(); } CHECK(!rightmost_leaf->IsNullPosition()); } return rightmost_leaf; } // Creates a text position using the next leaf node as its anchor. // Nearly all of the text in the accessibility tree is contained in leaf // nodes, so this method is mostly used to move through text nodes. AXPositionInstance CreateNextLeafTextPosition( const AbortMovePredicate& abort_predicate) const { // If this is an ancestor text position, resolve to its leaf text position. if (IsTextPosition() && !IsLeaf()) return AsLeafTextPosition(); AXPositionInstance next_leaf = CreateNextAnchorPosition(abort_predicate); while (!next_leaf->IsNullPosition() && !next_leaf->IsLeaf()) next_leaf = next_leaf->CreateNextAnchorPosition(abort_predicate); DCHECK(next_leaf); return next_leaf->AsLeafTextPosition(); } // Creates a text position using the previous leaf node as its anchor. // Nearly all of the text in the accessibility tree is contained in leaf // nodes, so this method is mostly used to move through text nodes. AXPositionInstance CreatePreviousLeafTextPosition( const AbortMovePredicate& abort_predicate) const { // If this is an ancestor text position, resolve to its leaf text position. if (IsTextPosition() && !IsLeaf()) return AsLeafTextPosition(); AXPositionInstance previous_leaf = CreatePreviousAnchorPosition(abort_predicate); while (!previous_leaf->IsNullPosition() && !previous_leaf->IsLeaf()) { previous_leaf = previous_leaf->CreatePreviousAnchorPosition(abort_predicate); } DCHECK(previous_leaf); return previous_leaf->AsLeafTextPosition(); } // Creates a tree position using the next leaf node as its anchor. // Nearly all of the text in the accessibility tree is contained in leaf // nodes, so this method is mostly used to move through text nodes. AXPositionInstance CreateNextLeafTreePosition( const AbortMovePredicate& abort_predicate) const { AXPositionInstance next_leaf = AsTreePosition()->CreateNextAnchorPosition(abort_predicate); while (!next_leaf->IsNullPosition() && !next_leaf->IsLeaf()) next_leaf = next_leaf->CreateNextAnchorPosition(abort_predicate); DCHECK(next_leaf); return next_leaf; } // Creates a tree position using the previous leaf node as its anchor. // Nearly all of the text in the accessibility tree is contained in leaf // nodes, so this method is mostly used to move through text nodes. AXPositionInstance CreatePreviousLeafTreePosition( const AbortMovePredicate& abort_predicate) const { AXPositionInstance previous_leaf = AsTreePosition()->CreatePreviousAnchorPosition(abort_predicate); while (!previous_leaf->IsNullPosition() && !previous_leaf->IsLeaf()) { previous_leaf = previous_leaf->CreatePreviousAnchorPosition(abort_predicate); } DCHECK(previous_leaf); return previous_leaf; } // // Static helpers for lambda usage. // static bool AtStartOfPagePredicate(const AXPositionInstance& position) { // If a page boundary is ignored, then it should not be exposed to assistive // software. return !position->IsIgnored() && position->AtStartOfPage(); } static bool AtEndOfPagePredicate(const AXPositionInstance& position) { // If a page boundary is ignored, then it should not be exposed to assistive // software. return !position->IsIgnored() && position->AtEndOfPage(); } static bool AtStartOfParagraphPredicate(const AXPositionInstance& position) { // Sometimes, nodes that are used to signify paragraph boundaries are // ignored, e.g. ". We make the design // decision to expose such boundaries to assistive software. Their // associated ignored nodes are still not exposed. This ensures that // navigation keys in text fields, such as Ctrl+Up/Down, will behave the // same way as related screen reader commands. return position->AtStartOfParagraph(); } static bool AtStartOfParagraphExcludingEmptyParagraphsPredicate( const AXPositionInstance& position) { // For UI Automation, empty lines after a paragraph should be merged into // the preceding paragraph. // // See // https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-uiautomationtextunits#paragraph const bool is_empty_paragraph = position->IsPointingToLineBreak() || (position->IsInLineBreakingObject() && (position->GetAnchor()->IsEmptyLeaf() || position->GetText().empty())); return !is_empty_paragraph && AtStartOfParagraphPredicate(position); } static bool AtEndOfParagraphPredicate(const AXPositionInstance& position) { // Sometimes, nodes that are used to signify paragraph boundaries are // ignored, e.g. ". We make the design // decision to expose such boundaries to assistive software. Their // associated ignored nodes are still not exposed. This ensures that // navigation keys in text fields, such as Ctrl+Up/Down, will behave the // same way as related screen reader commands. return position->AtEndOfParagraph(); } static bool AtStartOfLinePredicate(const AXPositionInstance& position) { // Sometimes, nodes that are used to signify line boundaries are ignored, // e.g.
which // is used to make a hard line break appear as a soft one. We make the // design decision to expose such boundaries to assistive software. Their // associated ignored nodes are still not exposed. return position->AtStartOfLine(); } static bool AtEndOfLinePredicate(const AXPositionInstance& position) { // Sometimes, nodes that are used to signify line boundaries are ignored, // e.g.
which // is used to make a hard line break appear as a soft one. We make the // design decision to expose such boundaries to assistive software. Their // associated ignored nodes are still not exposed. return position->AtEndOfLine(); } static bool AtStartOfSentencePredicate(const AXPositionInstance& position) { // Sentence boundaries should be at specific text offsets that are "visible" // to assistive software, hence not ignored. Ignored nodes are often used // for additional layout information, such as line and paragraph boundaries. // Their text is not currently processed. return !position->IsIgnored() && position->AtStartOfSentence(); } static bool AtEndOfSentencePredicate(const AXPositionInstance& position) { // Sentence boundaries should be at specific text offsets that are "visible" // to assistive software, hence not ignored. Ignored nodes are often used // for additional layout information, such as line and paragraph boundaries. // Their text is not currently processed. return !position->IsIgnored() && position->AtEndOfSentence(); } static bool AtStartOfFormatPredicate(const AXPositionInstance& position) { return position->AtStartOfFormat(); } static bool AtEndOfFormatPredicate(const AXPositionInstance& position) { return position->AtEndOfFormat(); } static bool AtStartOfWordPredicate(const AXPositionInstance& position) { // Word boundaries should be at specific text offsets that are "visible" to // assistive software, hence not ignored. Ignored nodes are often used for // additional layout information, such as line and paragraph boundaries. // Their text is not currently processed. return !position->IsIgnored() && position->AtStartOfWord(); } static bool AtEndOfWordPredicate(const AXPositionInstance& position) { // Word boundaries should be at specific text offsets that are "visible" to // assistive software, hence not ignored. Ignored nodes are often used for // additional layout information, such as line and paragraph boundaries. // Their text is not currently processed. return !position->IsIgnored() && position->AtEndOfWord(); } static bool DefaultAbortMovePredicate(const AXPosition& move_from, const AXPosition& move_to, const AXMoveType move_type, const AXMoveDirection direction) { // Default behavior is to never abort. return false; } // AbortMovePredicate function used to detect format boundaries. static bool AbortMoveAtFormatBoundary(const AXPosition& move_from, const AXPosition& move_to, const AXMoveType move_type, const AXMoveDirection direction) { if (move_from.IsNullPosition() || move_to.IsNullPosition() || move_from.IsInUnignoredEmptyObject() || move_to.IsInUnignoredEmptyObject()) { return true; } // Treat moving into or out of nodes with certain roles as a format break. ax::mojom::Role from_role = move_from.GetAnchorRole(); ax::mojom::Role to_role = move_to.GetAnchorRole(); if (from_role != to_role) { if (IsFormatBoundary(from_role) || IsFormatBoundary(to_role)) return true; } // Stop moving when text attributes differ. return move_from.AsLeafTreePosition()->GetTextAttributes() != move_to.AsLeafTreePosition()->GetTextAttributes(); } static bool MoveCrossesLineBreakingObject( const ax::mojom::TextBoundary paragraph_boundary, const AXPosition& move_from, const AXPosition& move_to, const AXMoveType move_type, const AXMoveDirection direction) { const AXPosition* proceeding_position = &move_from; const AXPosition* trailing_position = &move_to; switch (direction) { case AXMoveDirection::kNextInTree: break; case AXMoveDirection::kPreviousInTree: std::swap(proceeding_position, trailing_position); break; } switch (paragraph_boundary) { case ax::mojom::TextBoundary::kParagraphEnd: { const bool trailing_block = trailing_position->IsInLineBreakingObject(); const bool trailing_line_break = trailing_position->IsPointingToLineBreak(); return trailing_block || trailing_line_break; } case ax::mojom::TextBoundary::kParagraphStart: { // The trailing object does not need to be a block or a line break for // it to represent a start of a new paragraph. // // 1. Preceding block before "world" creates a paragraph start: //

hello

world
// 2. Preceding line break before "world" creates a paragraph start: //
Hello
world
const bool preceding_block = proceeding_position->IsInLineBreakingObject(); const bool preceding_line_break = proceeding_position->IsPointingToLineBreak(); return preceding_block || preceding_line_break; } default: NOTREACHED(); return false; } } // AbortMovePredicate function used to detect paragraph boundaries. static bool AbortMoveAtParagraphBoundary( const ax::mojom::TextBoundary paragraph_boundary, const AXPosition& move_from, const AXPosition& move_to, const AXMoveType move_type, const AXMoveDirection direction) { if (move_from.IsNullPosition() || move_to.IsNullPosition() || move_from.IsInUnignoredEmptyObject() || move_to.IsInUnignoredEmptyObject()) { // We deliberately put empty objects, such as empty text fields, in their // own paragraph for easier navigation. Otherwise, they could easily be // missed by screen reader users. return true; } return MoveCrossesLineBreakingObject(paragraph_boundary, move_from, move_to, move_type, direction); } // AbortMovePredicate function used to detect page boundaries. // // Depending on the type of content, it might be separated into a number of // pages. For example, a PDF document may expose multiple pages. static bool AbortMoveAtPageBoundary(const AXPosition& move_from, const AXPosition& move_to, const AXMoveType move_type, const AXMoveDirection direction) { if (move_from.IsNullPosition() || move_to.IsNullPosition()) return true; const bool move_from_break = move_from.GetAnchor()->GetBoolAttribute( ax::mojom::BoolAttribute::kIsPageBreakingObject); const bool move_to_break = move_to.GetAnchor()->GetBoolAttribute( ax::mojom::BoolAttribute::kIsPageBreakingObject); switch (move_type) { case AXMoveType::kAncestor: // For Ancestor moves, only abort when exiting a page break. // We don't care if the ancestor is a page break or not, since the // descendant is contained by it. return move_from_break; case AXMoveType::kDescendant: // For Descendant moves, only abort when entering a page break // descendant. We don't care if the ancestor is a page break or not, // since the descendant is contained by it. return move_to_break; case AXMoveType::kSibling: // For Sibling moves, abort if both of the siblings are a page break, // because that would mean exiting and/or entering a page break. return move_from_break && move_to_break; } } // AbortMovePredicate function used to detect crossing through the boundaries // of a window-like container, such as a webpage, a PDF, a dialog, the // browser's UI (AKA Views), or the whole desktop. static bool AbortMoveAtRootBoundary(const AXPosition& move_from, const AXPosition& move_to, const AXMoveType move_type, const AXMoveDirection direction) { // Positions are null when moving past the whole content, therefore the root // of a window-like container has certainly been crossed. if (move_from.IsNullPosition() || move_to.IsNullPosition()) return true; const ax::mojom::Role move_from_role = move_from.GetAnchorRole(); const ax::mojom::Role move_to_role = move_to.GetAnchorRole(); switch (move_type) { case AXMoveType::kAncestor: // For Ancestor moves, only abort when exiting a window-like container. // We don't care if the ancestor is the root of a window-like container // or not, since the descendant is contained by it. However, we do care // if the ancestor is an iframe because a webpage should be navigated as // a single document together with all its iframes, (out-of-process or // otherwise). return IsRootLike(move_from_role) && !IsIframe(move_to_role); case AXMoveType::kDescendant: // For Descendant moves, only abort when entering a window-like // container. We don't care if the ancestor is the root of a window-like // container or not, since the descendant is contained by it. However, // we do care if the ancestor is an iframe because a webpage should be // navigated as a single document together with all its iframes, // (out-of-process or otherwise). return IsRootLike(move_to_role) && !IsIframe(move_from_role); case AXMoveType::kSibling: // For Sibling moves, abort if both of the siblings are at the root of // window-like containers because that would mean exiting and/or // entering a new window-like container. Iframes should not be present // in this case because an iframe should never contain more than one // kRootWebArea as its immediate child. return IsRootLike(move_from_role) && IsRootLike(move_to_role); } } static bool AbortMoveAtStartOfInlineBlock(const AXPosition& move_from, const AXPosition& move_to, const AXMoveType move_type, const AXMoveDirection direction) { if (move_from.IsNullPosition() || move_to.IsNullPosition()) return true; // These will only be available if AXMode has kHTML set. const bool move_from_is_inline_block = move_from.GetAnchor()->GetStringAttribute( ax::mojom::StringAttribute::kDisplay) == "inline-block"; const bool move_to_is_inline_block = move_to.GetAnchor()->GetStringAttribute( ax::mojom::StringAttribute::kDisplay) == "inline-block"; switch (direction) { case AXMoveDirection::kNextInTree: // When moving forward, break if we enter an inline block. return move_to_is_inline_block && (move_type == AXMoveType::kDescendant || move_type == AXMoveType::kSibling); case AXMoveDirection::kPreviousInTree: // When moving backward, break if we exit an inline block. return move_from_is_inline_block && (move_type == AXMoveType::kAncestor || move_type == AXMoveType::kSibling); } NOTREACHED(); return false; } static const std::vector& GetSentenceStartOffsetsFunc( const AXPositionInstance& position) { if (position->IsNullPosition()) { static const base::NoDestructor> empty_sentence_starts; return *empty_sentence_starts; } DCHECK(position->GetAnchor()); return position->GetAnchor()->GetIntListAttribute( ax::mojom::IntListAttribute::kSentenceStarts); } static const std::vector& GetSentenceEndOffsetsFunc( const AXPositionInstance& position) { if (position->IsNullPosition()) { static const base::NoDestructor> empty_sentence_ends; return *empty_sentence_ends; } DCHECK(position->GetAnchor()); return position->GetAnchor()->GetIntListAttribute( ax::mojom::IntListAttribute::kSentenceEnds); } static const std::vector& GetWordStartOffsetsFunc( const AXPositionInstance& position) { return position->GetWordStartOffsets(); } static const std::vector& GetWordEndOffsetsFunc( const AXPositionInstance& position) { return position->GetWordEndOffsets(); } // Creates an ancestor equivalent position at the root node of this position's // accessibility tree, e.g. at the root of the current iframe (out-of-process // or not), PDF plugin, Views tree, dialog (native, ARIA or HTML), window, or // the whole desktop. // // For a similar method that does not stop at all iframe boundaries, see // `CreateRootAncestorPosition`. // // See `CreateParentPosition` for an explanation of the use of // |move_direction|. AXPositionInstance CreateAXTreeRootAncestorPosition( ax::mojom::MoveDirection move_direction) const { if (IsNullPosition()) return Clone(); AXPositionInstance root_position = Clone(); while (!IsRootLike(root_position->GetAnchorRole())) { AXPositionInstance parent_position = root_position->CreateParentPosition(move_direction); if (parent_position->IsNullPosition()) break; root_position = std::move(parent_position); } return root_position; } // Creates an ancestor equivalent position at the root node of all content, // e.g. at the root of the whole webpage, PDF plugin, Views tree, dialog // (native, ARIA or HTML), window, or the whole desktop. // // Note that this method will break out of an out-of-process iframe and return // a position at the root of the top-level document, but it will not break // into the Views tree if present. For a similar method that stops at all // iframe boundaries, see `CreateAXTreeRootAncestorPosition`. // // See `CreateParentPosition` for an explanation of the use of // |move_direction|. AXPositionInstance CreateRootAncestorPosition( ax::mojom::MoveDirection move_direction) const { AXPositionInstance root_position = CreateAXTreeRootAncestorPosition(move_direction); AXPositionInstance web_root_position = CreateNullPosition(); for (; !root_position->IsNullPosition(); root_position = root_position->CreateAXTreeRootAncestorPosition(move_direction)) { // An "ax::mojom::Role::kRootWebArea" could also be present at the root of // iframes or embedded objects, so we need to check that for that specific // role the position is also at the top of the forest of accessibility // trees making up the webpage. Note that the forest of accessibility // trees would include Views and on Chrome OS the whole desktop, so in the // case of a web root, checking if the parent position is the null // position will not work. if (root_position->GetAnchorRole() != ax::mojom::Role::kRootWebArea) { if (web_root_position->IsNullPosition()) return root_position; // Original position is not in web contents. // The previously saved web root is the shallowest in the forest of // accessibility trees. return web_root_position; } // Save this web root position and check if it is the shallowest in the // forest of accessibility trees. web_root_position = root_position->Clone(); root_position = root_position->CreateParentPosition(move_direction); } return web_root_position; } // Creates a text position that is in the same anchor as the current // position, but starting from the current text offset, adjusts to the next // or the previous boundary offset depending on the boundary direction. If // there is no next / previous offset, the current text offset is unchanged. AXPositionInstance CreatePositionAtNextOffsetBoundary( ax::mojom::MoveDirection move_direction, BoundaryTextOffsetsFunc get_offsets) const { if (IsNullPosition() || get_offsets.is_null()) return Clone(); AXPositionInstance text_position = AsTextPosition(); const std::vector& boundary_offsets = get_offsets.Run(text_position); if (boundary_offsets.empty()) return text_position; switch (move_direction) { case ax::mojom::MoveDirection::kNone: NOTREACHED(); return CreateNullPosition(); case ax::mojom::MoveDirection::kBackward: { auto offsets_iterator = std::lower_bound(boundary_offsets.begin(), boundary_offsets.end(), int32_t{text_position->text_offset_}); // If there is no previous offset, the current offset should be // unchanged. if (offsets_iterator > boundary_offsets.begin()) { // Since we already checked if "boundary_offsets" are non-empty, we // can safely move the iterator one position back, even if it's // currently at the vector's end. --offsets_iterator; text_position->text_offset_ = int(*offsets_iterator); text_position->affinity_ = ax::mojom::TextAffinity::kDownstream; } break; } case ax::mojom::MoveDirection::kForward: { const auto offsets_iterator = std::upper_bound(boundary_offsets.begin(), boundary_offsets.end(), int32_t{text_position->text_offset_}); // If there is no next offset, the current offset should be unchanged. if (offsets_iterator < boundary_offsets.end()) { text_position->text_offset_ = int(*offsets_iterator); text_position->affinity_ = ax::mojom::TextAffinity::kDownstream; } break; } } return text_position; } // Creates a text position that is in the same anchor as the current // position, but adjusts its text offset to be either at the first or last // offset boundary, based on the boundary direction. When moving forward, // the text position is adjusted to point to the first offset boundary, or // to the end of its anchor if there are no offset boundaries. When moving // backward, it is adjusted to point to the last offset boundary, or to the // start of its anchor if there are no offset boundaries. AXPositionInstance CreatePositionAtFirstOffsetBoundary( ax::mojom::MoveDirection move_direction, BoundaryTextOffsetsFunc get_offsets) const { if (IsNullPosition() || get_offsets.is_null()) return Clone(); AXPositionInstance text_position = AsTextPosition(); const std::vector& boundary_offsets = get_offsets.Run(text_position); switch (move_direction) { case ax::mojom::MoveDirection::kNone: NOTREACHED(); return CreateNullPosition(); case ax::mojom::MoveDirection::kBackward: if (boundary_offsets.empty()) { return text_position->CreatePositionAtStartOfAnchor(); } else { text_position->text_offset_ = int(boundary_offsets[boundary_offsets.size() - 1]); return text_position; } break; case ax::mojom::MoveDirection::kForward: if (boundary_offsets.empty()) { return text_position->CreatePositionAtEndOfAnchor(); } else { text_position->text_offset_ = int(boundary_offsets[0]); return text_position; } break; } } // Returns the next unignored leaf text position in the specified direction, // also ensuring that *AsLeafTextPosition() != // *CreateAdjacentLeafTextPosition() is true; returns a null position if no // adjacent position exists. // // This method is the first step for CreateBoundary[Start|End]Position to // guarantee that the resulting position when using a boundary behavior other // than `AXBoundaryBehavior::kStopAtAnchorBoundaryOrIfAlreadyAtBoundary` is // not equivalent to the initial position. That's why ignored positions are // also skipped. Otherwise, if a boundary is present on an ignored position, // the search for the next or previous boundary would stop prematurely. Note // that if there are multiple adjacent ignored positions and all of them // create a boundary, we'll skip them all on purpose. For example, adjacent // ignored paragraph boundaries could be created by using multiple aria-hidden // divs next to one another. These should not contribute more than one // paragraph boundary to the tree's text representation, otherwise this will // create user confusion. // // Note that using the `CompareTo` method with text positions does not take // into account position affinity or the order of their anchors in the tree: // two text positions are considered equivalent if their offsets in the text // representation of the entire AXTree are the same. As such, using // Create[Next|Previous]LeafTextPosition is not enough to create adjacent // positions, e.g. the end of an anchor and the start of the next one are // equivalent; furthermore, there could be nodes with no text between them, // all of them being equivalent too. // // IMPORTANT! This method basically moves the given position one character // forward/backward, but it could end up at the middle of a grapheme cluster, // so it shouldn't be used to move by ax::mojom::TextBoundary::kCharacter (for // such a purpose use Create[Next|Previous]CharacterPosition instead). AXPositionInstance CreateAdjacentLeafTextPosition( ax::mojom::MoveDirection move_direction) const { AXPositionInstance text_position = AsLeafTextPosition(); switch (move_direction) { case ax::mojom::MoveDirection::kNone: NOTREACHED(); return CreateNullPosition(); case ax::mojom::MoveDirection::kBackward: // If we are at a text offset greater than 0, we will simply decrease // the offset by one; otherwise, we will create a position at the end of // the previous unignored leaf node with non-empty text and decrease its // offset. // // Note that a position located at offset 0 of an empty text node is // considered both at the start and at the end of its anchor, so the // following loop skips over empty text leaf nodes, which is expected // since those positions are equivalent to both the previous non-empty // leaf node's end and the next non-empty leaf node's start. while (text_position->AtStartOfAnchor() || text_position->IsIgnored()) { text_position = text_position ->CreatePreviousLeafTextPosition( base::BindRepeating(&AbortMoveAtRootBoundary)) ->CreatePositionAtEndOfAnchor(); } if (!text_position->IsNullPosition()) --text_position->text_offset_; break; case ax::mojom::MoveDirection::kForward: // If we are at a text offset less than MaxTextOffset, we will simply // increase the offset by one; otherwise, we will create a position at // the start of the next unignored leaf node with non-empty text and // increase its offset. // // Same as the comment above: using AtEndOfAnchor is enough to skip // empty text nodes that are equivalent to the initial position. while (text_position->AtEndOfAnchor() || text_position->IsIgnored()) { text_position = text_position->CreateNextLeafTextPosition( base::BindRepeating(&AbortMoveAtRootBoundary)); } if (!text_position->IsNullPosition()) ++text_position->text_offset_; break; } DCHECK(text_position->IsValid()); return text_position; } AXPositionKind kind_; // TODO(crbug.com/1362839): use weak pointers for the AXTree, so that // AXPosition can be used without AXTreeManager support (and also faster than // the slow AXTreeID). AXTreeID tree_id_; AXNodeID anchor_id_; // For text positions, |child_index_| is initially set to |-1| and only // computed on demand. The same with tree positions and |text_offset_|. int child_index_; // "text_offset_" represents the number of UTF16 code units before this // position. It doesn't count grapheme clusters. int text_offset_; // Affinity is used to distinguish between two text positions that point to // the same text offset, but which happens to fall on a soft line break. A // soft line break doesn't insert any white space in the accessibility tree, // so without affinity there would be no way to determine whether a text // position is before or after the soft line break. An upstream affinity // means that the position is before the soft line break, whilst a // downstream affinity means that the position is after the soft line break. // // Please note that affinity could only be set to upstream for positions // that are anchored to non-leaf nodes. When on a leaf node, there could // never be an ambiguity as to which line a position points to because Blink // creates separate inline text boxes for each line of text. Therefore, a // leaf text position before the soft line break would be pointing to the // end of its anchor node, whilst a leaf text position after the soft line // break would be pointing to the start of the next node. ax::mojom::TextAffinity affinity_; // // Cached members that should be lazily created on first use. // // In the case of a leaf position, its text content (in UTF16 format). Used // for initializing a grapheme break iterator. mutable std::u16string name_; }; template const int AXPosition::BEFORE_TEXT; template const int AXPosition::INVALID_INDEX; template const int AXPosition::INVALID_OFFSET; template bool operator==(const AXPosition& first, const AXPosition& second) { const absl::optional compare_to_optional = first.CompareTo(second); return compare_to_optional.has_value() && compare_to_optional.value() == 0; } template bool operator!=(const AXPosition& first, const AXPosition& second) { const absl::optional compare_to_optional = first.CompareTo(second); // It makes sense to also return false if the positions are not comparable, // because by definition non-comparable positions are uniqual. Positions are // not comparable when one position is null and the other is not or if the // positions do not have any common ancestor. return !compare_to_optional.has_value() || compare_to_optional.value() != 0; } template bool operator<(const AXPosition& first, const AXPosition& second) { const absl::optional compare_to_optional = first.CompareTo(second); return compare_to_optional.has_value() && compare_to_optional.value() < 0; } template bool operator<=(const AXPosition& first, const AXPosition& second) { const absl::optional compare_to_optional = first.CompareTo(second); return compare_to_optional.has_value() && compare_to_optional.value() <= 0; } template bool operator>(const AXPosition& first, const AXPosition& second) { const absl::optional compare_to_optional = first.CompareTo(second); return compare_to_optional.has_value() && compare_to_optional.value() > 0; } template bool operator>=(const AXPosition& first, const AXPosition& second) { const absl::optional compare_to_optional = first.CompareTo(second); return compare_to_optional.has_value() && compare_to_optional.value() >= 0; } template void swap(AXPosition& first, AXPosition& second) { first.swap(second); } template std::ostream& operator<<( std::ostream& stream, const AXPosition& position) { return stream << position.ToString(); } } // namespace ui #endif // UI_ACCESSIBILITY_AX_POSITION_H_