diff --git a/common/src/lib.rs b/common/src/lib.rs index 3b01874df..d7054b9c9 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -21,7 +21,6 @@ use serde_lib as serde; use serde_lib::{Deserialize, Serialize}; use std::{ num::{NonZeroU128, NonZeroU64}, - ops::Range, sync::Arc, }; @@ -409,19 +408,6 @@ pub enum DropEffect { Popup, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "schemars", derive(JsonSchema))] -#[cfg_attr(feature = "serde", serde(crate = "serde"))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub enum MarkerType { - SpellingError, - GrammarError, - SearchMatch, - ActiveSuggestion, - Suggestion, -} - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "schemars", derive(JsonSchema))] @@ -606,19 +592,6 @@ impl From for NodeId { } } -/// A marker spanning a range within text. -#[derive(Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "schemars", derive(JsonSchema))] -#[cfg_attr(feature = "serde", serde(crate = "serde"))] -#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub struct TextMarker { - pub marker_type: MarkerType, - /// Indices are in UTF-8 code units. - pub range: Range, -} - /// Defines a custom action for a UI element. /// /// For example, a list UI can allow a user to reorder items in the list by dragging the @@ -646,7 +619,20 @@ fn is_empty(slice: &[T]) -> bool { slice.is_empty() } -/// Offsets are in UTF-8 code units. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "schemars", derive(JsonSchema))] +#[cfg_attr(feature = "serde", serde(crate = "serde"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct TextPosition { + /// The node's role must be [`Role::InlineTextBox`]. + pub node: NodeId, + /// The index of an item in [`Node::character_lengths`], or the length + /// of that slice if the position is at the end of the line. + pub character_index: usize, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "schemars", derive(JsonSchema))] @@ -654,10 +640,15 @@ fn is_empty(slice: &[T]) -> bool { #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct TextSelection { - anchor_node: NodeId, - anchor_offset: usize, - focus_node: NodeId, - focus_offset: usize, + /// The position where the selection started, and which does not change + /// as the selection is expanded or contracted. If there is no selection + /// but only a caret, this must be equal to [`focus`]. This is also known + /// as a degenerate selection. + pub anchor: TextPosition, + /// The active end of the selection, which changes as the selection + /// is expanded or contracted, or the position of the caret if there is + /// no selection. + pub focus: TextPosition, } /// A single accessible object. A complete UI is represented as a tree of these. @@ -925,25 +916,96 @@ pub struct Node { pub radio_group: Vec, #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub markers: Box<[TextMarker]>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] + pub is_spelling_error: bool, + #[cfg_attr(feature = "serde", serde(default))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] + pub is_grammar_error: bool, + #[cfg_attr(feature = "serde", serde(default))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] + pub is_search_match: bool, + #[cfg_attr(feature = "serde", serde(default))] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] + pub is_suggestion: bool, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub text_direction: Option, - /// For inline text. This is the pixel position of the end of each - /// character within the bounding rectangle of this object, in the - /// direction given by [`Node::text_direction`]. For example, for left-to-right - /// text, the first offset is the right coordinate of the first - /// character within the object's bounds, the second offset - /// is the right coordinate of the second character, and so on. + + /// For inline text. The length (non-inclusive) of each character + /// in UTF-8 code units (bytes). The sum of these lengths must equal + /// the length of [`Node::value`], also in bytes. + /// + /// A character is defined as the smallest unit of text that + /// can be selected. This isn't necessarily a single Unicode + /// scalar value (code point). This is why AccessKit can't compute + /// the lengths of the characters from the text itself; this information + /// must be provided by the text editing implementation. + /// + /// If this node is the last text box in a line that ends with a hard + /// line break, that line break should be included at the end of this + /// node's value as either a CRLF or LF; in both cases, the line break + /// should be counted as a single character for the sake of this slice. + /// When the caret is at the end of such a line, the focus of the text + /// selection should be on the line break, not after it. #[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub character_offsets: Box<[f32]>, + pub character_lengths: Box<[u8]>, + /// For inline text. This is the position of each character within + /// the node's bounding box, in the direction given by + /// [`Node::text_direction`], in the coordinate space of this node. + /// + /// When present, the length of this slice should be the same as the length + /// of [`Node::character_lengths`], including for lines that end + /// with a hard line break. The position of such a line break should + /// be the position where an end-of-paragraph marker would be rendered. + /// + /// This field is optional. Without it, AccessKit can't support some + /// use cases, such as screen magnifiers that track the caret position + /// or screen readers that display a highlight cursor. However, + /// most text functionality still works without this information. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub character_positions: Option>, + /// For inline text. This is the advance width of each character, + /// in the direction given by [`Node::text_direction`], in the coordinate + /// space of this node. + /// + /// When present, the length of this slice should be the same as the length + /// of [`Node::character_lengths`], including for lines that end + /// with a hard line break. The width of such a line break should + /// be non-zero if selecting the line break by itself results in + /// a visible highlight (as in Microsoft Word), or zero if not + /// (as in Windows Notepad). + /// + /// This field is optional. Without it, AccessKit can't support some + /// use cases, such as screen magnifiers that track the caret position + /// or screen readers that display a highlight cursor. However, + /// most text functionality still works without this information. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub character_widths: Option>, - /// For inline text. The indices of each word, in UTF-8 code units. + /// For inline text. The length of each word in characters, as defined + /// in [`Node::character_lengths`]. The sum of these lengths must equal + /// the length of [`Node::character_lengths`]. + /// + /// The end of each word is the beginning of the next word; there are no + /// characters that are not considered part of a word. Trailing whitespace + /// is typically considered part of the word that precedes it, while + /// a line's leading whitespace is considered its own word. Whether + /// punctuation is considered a separate word or part of the preceding + /// word depends on the particular text editing implementation. + /// Some editors may have their own definition of a word; for example, + /// in an IDE, words may correspond to programming language tokens. + /// + /// Not all assistive technologies require information about word + /// boundaries, and not all platform accessibility APIs even expose + /// this information, but for assistive technologies that do use + /// this information, users will get unpredictable results if the word + /// boundaries exposed by the accessibility tree don't match + /// the editor's behavior. This is why AccessKit does not determine + /// word boundaries itself. #[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub words: Box<[Range]>, + pub word_lengths: Box<[u8]>, #[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] diff --git a/consumer/src/lib.rs b/consumer/src/lib.rs index ff99182e7..950bb25a5 100644 --- a/consumer/src/lib.rs +++ b/consumer/src/lib.rs @@ -12,6 +12,12 @@ pub use node::Node; pub(crate) mod iterators; pub use iterators::FilterResult; +pub(crate) mod text; +pub use text::{ + AttributeValue as TextAttributeValue, Position as TextPosition, Range as TextRange, + WeakRange as WeakTextRange, +}; + #[cfg(test)] mod tests { use accesskit::kurbo::{Affine, Rect, Vec2}; diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 810eadfc6..977d30aae 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -226,6 +226,19 @@ impl<'a> Node<'a> { * self.direct_transform() } + pub(crate) fn relative_transform(&self, stop_at: &Node) -> Affine { + let parent_transform = if let Some(parent) = self.parent() { + if parent.id() == stop_at.id() { + Affine::IDENTITY + } else { + parent.relative_transform(stop_at) + } + } else { + Affine::IDENTITY + }; + parent_transform * self.direct_transform() + } + /// Returns the node's transformed bounding box relative to the tree's /// container (e.g. window). pub fn bounding_box(&self) -> Option { @@ -235,13 +248,18 @@ impl<'a> Node<'a> { .map(|rect| self.transform().transform_rect_bbox(*rect)) } - /// Returns the deepest filtered node, either this node or a descendant, - /// at the given point in this node's coordinate space. - pub fn node_at_point( + pub(crate) fn bounding_box_in_coordinate_space(&self, other: &Node) -> Option { + self.data() + .bounds + .as_ref() + .map(|rect| self.relative_transform(other).transform_rect_bbox(*rect)) + } + + pub(crate) fn hit_test( &self, point: Point, filter: &impl Fn(&Node) -> FilterResult, - ) -> Option> { + ) -> Option<(Node<'a>, Point)> { let filter_result = filter(self); if filter_result == FilterResult::ExcludeSubtree { @@ -250,15 +268,15 @@ impl<'a> Node<'a> { for child in self.children().rev() { let point = child.direct_transform().inverse() * point; - if let Some(node) = child.node_at_point(point, filter) { - return Some(node); + if let Some(result) = child.hit_test(point, filter) { + return Some(result); } } if filter_result == FilterResult::Include { if let Some(rect) = &self.data().bounds { if rect.contains(point) { - return Some(*self); + return Some((*self, point)); } } } @@ -266,6 +284,16 @@ impl<'a> Node<'a> { None } + /// Returns the deepest filtered node, either this node or a descendant, + /// at the given point in this node's coordinate space. + pub fn node_at_point( + &self, + point: Point, + filter: &impl Fn(&Node) -> FilterResult, + ) -> Option> { + self.hit_test(point, filter).map(|(node, _)| node) + } + pub fn id(&self) -> NodeId { self.state.id } @@ -480,6 +508,22 @@ impl<'a> Node<'a> { self.data().selected } + pub fn index_path(&self) -> Vec { + self.relative_index_path(self.tree_state.root_id()) + } + + pub fn relative_index_path(&self, ancestor_id: NodeId) -> Vec { + let mut result = Vec::new(); + let mut current = *self; + while current.id() != ancestor_id { + let (parent, index) = current.parent_and_index().unwrap(); + result.push(index); + current = parent; + } + result.reverse(); + result + } + pub(crate) fn first_filtered_child( &self, filter: &impl Fn(&Node) -> FilterResult, diff --git a/consumer/src/text.rs b/consumer/src/text.rs new file mode 100644 index 000000000..e6f6ba758 --- /dev/null +++ b/consumer/src/text.rs @@ -0,0 +1,1478 @@ +// Copyright 2022 The AccessKit Authors. All rights reserved. +// Licensed under the Apache License, Version 2.0 (found in +// the LICENSE-APACHE file) or the MIT license (found in +// the LICENSE-MIT file), at your option. + +use accesskit::kurbo::{Point, Rect}; +use accesskit::{NodeId, Role, TextDirection, TextPosition as WeakPosition}; +use std::{cmp::Ordering, iter::FusedIterator}; + +use crate::{FilterResult, Node, TreeState}; + +#[derive(Clone, Copy)] +pub(crate) struct InnerPosition<'a> { + pub(crate) node: Node<'a>, + pub(crate) character_index: usize, +} + +impl<'a> InnerPosition<'a> { + fn upgrade(tree_state: &'a TreeState, weak: WeakPosition) -> Option { + let node = tree_state.node_by_id(weak.node)?; + if node.role() != Role::InlineTextBox { + return None; + } + let character_index = weak.character_index; + if character_index > node.data().character_lengths.len() { + return None; + } + Some(Self { + node, + character_index, + }) + } + + fn is_word_start(&self) -> bool { + let mut total_length = 0usize; + for length in self.node.data().word_lengths.iter() { + if total_length == self.character_index { + return true; + } + total_length += *length as usize; + } + false + } + + fn is_box_start(&self) -> bool { + self.character_index == 0 + } + + fn is_line_start(&self) -> bool { + self.is_box_start() && self.node.data().previous_on_line.is_none() + } + + fn is_box_end(&self) -> bool { + self.character_index == self.node.data().character_lengths.len() + } + + fn is_line_end(&self) -> bool { + self.is_box_end() && self.node.data().next_on_line.is_none() + } + + fn is_paragraph_end(&self) -> bool { + self.is_line_end() && self.node.value().unwrap().ends_with('\n') + } + + fn is_document_start(&self, root_node: &Node) -> bool { + self.is_box_start() + && self + .node + .preceding_inline_text_boxes(root_node) + .next() + .is_none() + } + + fn is_document_end(&self, root_node: &Node) -> bool { + self.is_box_end() + && self + .node + .following_inline_text_boxes(root_node) + .next() + .is_none() + } + + fn biased_to_start(&self, root_node: &Node) -> Self { + if self.is_box_end() { + if let Some(node) = self.node.following_inline_text_boxes(root_node).next() { + return Self { + node, + character_index: 0, + }; + } + } + *self + } + + fn biased_to_end(&self, root_node: &Node) -> Self { + if self.is_box_start() { + if let Some(node) = self.node.preceding_inline_text_boxes(root_node).next() { + return Self { + node, + character_index: node.data().character_lengths.len(), + }; + } + } + *self + } + + fn normalized(&self, root_node: &Node) -> Self { + if self.is_line_end() && !self.is_paragraph_end() { + *self + } else { + self.biased_to_start(root_node) + } + } + + fn comparable(&self, root_node: &Node) -> (Vec, usize) { + let normalized = self.biased_to_start(root_node); + ( + normalized.node.relative_index_path(root_node.id()), + normalized.character_index, + ) + } + + fn previous_word_start(&self) -> Self { + let mut total_length_before = 0usize; + for length in self.node.data().word_lengths.iter() { + let new_total_length = total_length_before + (*length as usize); + if new_total_length >= self.character_index { + break; + } + total_length_before = new_total_length; + } + Self { + node: self.node, + character_index: total_length_before, + } + } + + fn word_end(&self) -> Self { + let mut total_length = 0usize; + for length in self.node.data().word_lengths.iter() { + total_length += *length as usize; + if total_length > self.character_index { + break; + } + } + Self { + node: self.node, + character_index: total_length, + } + } + + fn line_start(&self) -> Self { + let mut node = self.node; + while let Some(id) = node.data().previous_on_line { + node = node.tree_state.node_by_id(id).unwrap(); + } + Self { + node, + character_index: 0, + } + } + + fn line_end(&self) -> Self { + let mut node = self.node; + while let Some(id) = node.data().next_on_line { + node = node.tree_state.node_by_id(id).unwrap(); + } + Self { + node, + character_index: node.data().character_lengths.len(), + } + } + + pub(crate) fn downgrade(&self) -> WeakPosition { + WeakPosition { + node: self.node.id(), + character_index: self.character_index, + } + } +} + +impl<'a> PartialEq for InnerPosition<'a> { + fn eq(&self, other: &Self) -> bool { + self.node.id() == other.node.id() && self.character_index == other.character_index + } +} + +impl<'a> Eq for InnerPosition<'a> {} + +#[derive(Clone, Copy)] +pub struct Position<'a> { + root_node: Node<'a>, + pub(crate) inner: InnerPosition<'a>, +} + +impl<'a> Position<'a> { + pub fn is_format_start(&self) -> bool { + // TODO: support variable text formatting (part of rich text) + self.is_document_start() + } + + pub fn is_word_start(&self) -> bool { + self.inner.is_word_start() + } + + pub fn is_line_start(&self) -> bool { + self.inner.is_line_start() + } + + pub fn is_line_end(&self) -> bool { + self.inner.is_line_end() + } + + pub fn is_paragraph_start(&self) -> bool { + self.is_document_start() + || (self.is_line_start() + && self.inner.biased_to_end(&self.root_node).is_paragraph_end()) + } + + pub fn is_page_start(&self) -> bool { + self.is_document_start() + } + + pub fn is_document_start(&self) -> bool { + self.inner.is_document_start(&self.root_node) + } + + pub fn is_document_end(&self) -> bool { + self.inner.is_document_end(&self.root_node) + } + + pub fn to_degenerate_range(&self) -> Range { + Range::new(self.root_node, self.inner, self.inner) + } + + pub fn forward_by_character(&self) -> Self { + let pos = self.inner.biased_to_start(&self.root_node); + Self { + root_node: self.root_node, + inner: InnerPosition { + node: pos.node, + character_index: pos.character_index + 1, + } + .normalized(&self.root_node), + } + } + + pub fn backward_by_character(&self) -> Self { + let pos = self.inner.biased_to_end(&self.root_node); + Self { + root_node: self.root_node, + inner: InnerPosition { + node: pos.node, + character_index: pos.character_index - 1, + } + .normalized(&self.root_node), + } + } + + pub fn forward_by_format(&self) -> Self { + // TODO: support variable text formatting (part of rich text) + self.forward_by_document() + } + + pub fn backward_by_format(&self) -> Self { + // TODO: support variable text formatting (part of rich text) + self.backward_by_document() + } + + pub fn forward_by_word(&self) -> Self { + let pos = self.inner.biased_to_start(&self.root_node); + Self { + root_node: self.root_node, + inner: pos.word_end().normalized(&self.root_node), + } + } + + pub fn backward_by_word(&self) -> Self { + let pos = self.inner.biased_to_end(&self.root_node); + Self { + root_node: self.root_node, + inner: pos.previous_word_start().normalized(&self.root_node), + } + } + + pub fn forward_by_line(&self) -> Self { + let pos = self.inner.biased_to_start(&self.root_node); + Self { + root_node: self.root_node, + inner: pos.line_end().normalized(&self.root_node), + } + } + + pub fn backward_by_line(&self) -> Self { + let pos = self.inner.biased_to_end(&self.root_node); + Self { + root_node: self.root_node, + inner: pos.line_start().normalized(&self.root_node), + } + } + + pub fn forward_by_paragraph(&self) -> Self { + let mut current = *self; + loop { + current = current.forward_by_line(); + if current.is_document_end() + || current + .inner + .biased_to_end(&self.root_node) + .is_paragraph_end() + { + break; + } + } + current + } + + pub fn backward_by_paragraph(&self) -> Self { + let mut current = *self; + loop { + current = current.backward_by_line(); + if current.is_paragraph_start() { + break; + } + } + current + } + + pub fn forward_by_page(&self) -> Self { + self.forward_by_document() + } + + pub fn backward_by_page(&self) -> Self { + self.backward_by_document() + } + + pub fn forward_by_document(&self) -> Self { + Self { + root_node: self.root_node, + inner: self.root_node.document_end(), + } + } + + pub fn backward_by_document(&self) -> Self { + Self { + root_node: self.root_node, + inner: self.root_node.document_start(), + } + } +} + +impl<'a> PartialEq for Position<'a> { + fn eq(&self, other: &Self) -> bool { + self.root_node.id() == other.root_node.id() && self.inner == other.inner + } +} + +impl<'a> Eq for Position<'a> {} + +impl<'a> PartialOrd for Position<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + if self.root_node.id() != other.root_node.id() { + return None; + } + let self_comparable = self.inner.comparable(&self.root_node); + let other_comparable = other.inner.comparable(&self.root_node); + Some(self_comparable.cmp(&other_comparable)) + } +} + +pub enum AttributeValue { + Single(T), + Mixed, +} + +#[derive(Clone, Copy)] +pub struct Range<'a> { + pub(crate) node: Node<'a>, + pub(crate) start: InnerPosition<'a>, + pub(crate) end: InnerPosition<'a>, +} + +impl<'a> Range<'a> { + fn new(node: Node<'a>, mut start: InnerPosition<'a>, mut end: InnerPosition<'a>) -> Self { + if start.comparable(&node) > end.comparable(&node) { + std::mem::swap(&mut start, &mut end); + } + Self { node, start, end } + } + + pub fn node(&self) -> &Node { + &self.node + } + + pub fn start(&self) -> Position<'a> { + Position { + root_node: self.node, + inner: self.start, + } + } + + pub fn end(&self) -> Position<'a> { + Position { + root_node: self.node, + inner: self.end, + } + } + + pub fn is_degenerate(&self) -> bool { + self.start.comparable(&self.node) == self.end.comparable(&self.node) + } + + fn walk(&self, mut f: F) -> Option + where + F: FnMut(&Node) -> Option, + { + // If the range is degenerate, we don't want to normalize it. + // This is important e.g. when getting the bounding rectangle + // of the caret range when the caret is at the end of a wrapped line. + let (start, end) = if self.is_degenerate() { + (self.start, self.start) + } else { + let start = self.start.biased_to_start(&self.node); + let end = self.end.biased_to_end(&self.node); + (start, end) + }; + if let Some(result) = f(&start.node) { + return Some(result); + } + if start.node.id() == end.node.id() { + return None; + } + for node in start.node.following_inline_text_boxes(&self.node) { + if let Some(result) = f(&node) { + return Some(result); + } + if node.id() == end.node.id() { + break; + } + } + None + } + + pub fn text(&self) -> String { + let mut result = String::new(); + self.walk::<_, ()>(|node| { + let character_lengths = &node.data().character_lengths; + let start_index = if node.id() == self.start.node.id() { + self.start.character_index + } else { + 0 + }; + let end_index = if node.id() == self.end.node.id() { + self.end.character_index + } else { + character_lengths.len() + }; + let value = node.value().unwrap(); + let s = if start_index == end_index { + "" + } else if start_index == 0 && end_index == character_lengths.len() { + value + } else { + let slice_start = character_lengths[..start_index] + .iter() + .copied() + .map(usize::from) + .sum::(); + let slice_end = slice_start + + character_lengths[start_index..end_index] + .iter() + .copied() + .map(usize::from) + .sum::(); + &value[slice_start..slice_end] + }; + result.push_str(s); + None + }); + result + } + + /// Returns the range's transformed bounding boxes relative to the tree's + /// container (e.g. window). + /// + /// If the return value is empty, it means that the source tree doesn't + /// provide enough information to calculate bounding boxes. Otherwise, + /// there will always be at least one box, even if it's zero-width, + /// as it is for a degenerate range. + pub fn bounding_boxes(&self) -> Vec { + let mut result = Vec::new(); + self.walk(|node| { + let mut rect = match &node.data().bounds { + Some(rect) => *rect, + None => { + return Some(Vec::new()); + } + }; + let positions = match &node.data().character_positions { + Some(positions) => positions, + None => { + return Some(Vec::new()); + } + }; + let widths = match &node.data().character_widths { + Some(widths) => widths, + None => { + return Some(Vec::new()); + } + }; + let direction = match node.data().text_direction { + Some(direction) => direction, + None => { + return Some(Vec::new()); + } + }; + let character_lengths = &node.data().character_lengths; + let start_index = if node.id() == self.start.node.id() { + self.start.character_index + } else { + 0 + }; + let end_index = if node.id() == self.end.node.id() { + self.end.character_index + } else { + character_lengths.len() + }; + if start_index != 0 || end_index != character_lengths.len() { + let pixel_start = if start_index < character_lengths.len() { + positions[start_index] + } else { + positions[start_index - 1] + widths[start_index - 1] + }; + let pixel_end = if end_index == start_index { + pixel_start + } else { + positions[end_index - 1] + widths[end_index - 1] + }; + let pixel_start = f64::from(pixel_start); + let pixel_end = f64::from(pixel_end); + match direction { + TextDirection::LeftToRight => { + let orig_left = rect.x0; + rect.x0 = orig_left + pixel_start; + rect.x1 = orig_left + pixel_end; + } + TextDirection::RightToLeft => { + let orig_right = rect.x1; + rect.x1 = orig_right - pixel_start; + rect.x0 = orig_right - pixel_end; + } + // Note: The following directions assume that the rectangle, + // in the node's coordinate space, is y-down. TBD: Will we + // ever encounter a case where this isn't true? + TextDirection::TopToBottom => { + let orig_top = rect.y0; + rect.y0 = orig_top + pixel_start; + rect.y1 = orig_top + pixel_end; + } + TextDirection::BottomToTop => { + let orig_bottom = rect.y1; + rect.y1 = orig_bottom - pixel_start; + rect.y0 = orig_bottom - pixel_end; + } + } + } + result.push(node.transform().transform_rect_bbox(rect)); + None + }) + .unwrap_or(result) + } + + pub fn attribute(&self, f: F) -> AttributeValue + where + F: Fn(&Node) -> T, + T: PartialEq, + { + let mut value = None; + self.walk(|node| { + let current = f(node); + if let Some(value) = &value { + if *value != current { + return Some(AttributeValue::Mixed); + } + } else { + value = Some(current); + } + None + }) + .unwrap_or_else(|| AttributeValue::Single(value.unwrap())) + } + + pub fn set_start(&mut self, pos: Position<'a>) { + assert_eq!(pos.root_node.id(), self.node.id()); + self.start = pos.inner; + // We use `>=` here because if the two endpoints are equivalent + // but with a different bias, we want to normalize the bias. + if self.start.comparable(&self.node) >= self.end.comparable(&self.node) { + self.end = self.start; + } + } + + pub fn set_end(&mut self, pos: Position<'a>) { + assert_eq!(pos.root_node.id(), self.node.id()); + self.end = pos.inner; + // We use `>=` here because if the two endpoints are equivalent + // but with a different bias, we want to normalize the bias. + if self.start.comparable(&self.node) >= self.end.comparable(&self.node) { + self.start = self.end; + } + } + + pub fn downgrade(&self) -> WeakRange { + WeakRange { + node_id: self.node.id(), + start: self.start.downgrade(), + end: self.end.downgrade(), + start_comparable: self.start.comparable(&self.node), + end_comparable: self.end.comparable(&self.node), + } + } +} + +impl<'a> PartialEq for Range<'a> { + fn eq(&self, other: &Self) -> bool { + self.node.id() == other.node.id() && self.start == other.start && self.end == other.end + } +} + +impl<'a> Eq for Range<'a> {} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WeakRange { + node_id: NodeId, + start: WeakPosition, + end: WeakPosition, + start_comparable: (Vec, usize), + end_comparable: (Vec, usize), +} + +impl WeakRange { + pub fn node_id(&self) -> NodeId { + self.node_id + } + + pub fn start_comparable(&self) -> &(Vec, usize) { + &self.start_comparable + } + + pub fn end_comparable(&self) -> &(Vec, usize) { + &self.end_comparable + } + + pub fn upgrade_node<'a>(&self, tree_state: &'a TreeState) -> Option> { + tree_state.node_by_id(self.node_id) + } + + pub fn upgrade<'a>(&self, tree_state: &'a TreeState) -> Option> { + let node = self.upgrade_node(tree_state)?; + let start = InnerPosition::upgrade(tree_state, self.start)?; + let end = InnerPosition::upgrade(tree_state, self.end)?; + Some(Range { node, start, end }) + } +} + +fn text_node_filter(root_id: NodeId, node: &Node) -> FilterResult { + if node.id() == root_id || node.role() == Role::InlineTextBox { + FilterResult::Include + } else { + FilterResult::ExcludeNode + } +} + +fn character_index_at_point(node: &Node, point: Point) -> usize { + // We know the node has a bounding rectangle because it was returned + // by a hit test. + let rect = node.data().bounds.as_ref().unwrap(); + let character_lengths = &node.data().character_lengths; + let positions = match &node.data().character_positions { + Some(positions) => positions, + None => { + return 0; + } + }; + let widths = match &node.data().character_widths { + Some(widths) => widths, + None => { + return 0; + } + }; + let direction = match node.data().text_direction { + Some(direction) => direction, + None => { + return 0; + } + }; + for (i, (position, width)) in positions.iter().zip(widths.iter()).enumerate().rev() { + let relative_pos = match direction { + TextDirection::LeftToRight => point.x - rect.x0, + TextDirection::RightToLeft => rect.x1 - point.x, + // Note: The following directions assume that the rectangle, + // in the node's coordinate space, is y-down. TBD: Will we + // ever encounter a case where this isn't true? + TextDirection::TopToBottom => point.y - rect.y0, + TextDirection::BottomToTop => rect.y1 - point.y, + }; + if relative_pos >= f64::from(*position) && relative_pos < f64::from(*position + *width) { + return i; + } + } + character_lengths.len() +} + +impl<'a> Node<'a> { + fn inline_text_boxes( + &self, + ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { + let id = self.id(); + self.filtered_children(move |node| text_node_filter(id, node)) + } + + fn following_inline_text_boxes( + &self, + root_node: &Node, + ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { + let id = root_node.id(); + self.following_filtered_siblings(move |node| text_node_filter(id, node)) + } + + fn preceding_inline_text_boxes( + &self, + root_node: &Node, + ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { + let id = root_node.id(); + self.preceding_filtered_siblings(move |node| text_node_filter(id, node)) + } + + pub fn supports_text_ranges(&self) -> bool { + let role = self.role(); + if role != Role::StaticText && role != Role::TextField && role != Role::Document { + return false; + } + self.inline_text_boxes().next().is_some() + } + + fn document_start(&self) -> InnerPosition<'a> { + let node = self.inline_text_boxes().next().unwrap(); + InnerPosition { + node, + character_index: 0, + } + } + + fn document_end(&self) -> InnerPosition<'a> { + let node = self.inline_text_boxes().next_back().unwrap(); + InnerPosition { + node, + character_index: node.data().character_lengths.len(), + } + } + + pub fn document_range(&self) -> Range { + let start = self.document_start(); + let end = self.document_end(); + Range::new(*self, start, end) + } + + pub fn has_text_selection(&self) -> bool { + self.data().text_selection.is_some() + } + + pub fn text_selection(&self) -> Option { + self.data().text_selection.map(|selection| { + let anchor = InnerPosition::upgrade(self.tree_state, selection.anchor).unwrap(); + let focus = InnerPosition::upgrade(self.tree_state, selection.focus).unwrap(); + Range::new(*self, anchor, focus) + }) + } + + /// Returns the nearest text position to the given point + /// in this node's coordinate space. + pub fn text_position_at_point(&self, point: Point) -> Position { + let id = self.id(); + if let Some((node, point)) = self.hit_test(point, &move |node| text_node_filter(id, node)) { + if node.role() == Role::InlineTextBox { + let pos = InnerPosition { + node, + character_index: character_index_at_point(&node, point), + }; + return Position { + root_node: *self, + inner: pos, + }; + } + } + + // The following tests can assume that the point is not within + // any inline text box. + + if let Some(node) = self.inline_text_boxes().next() { + if let Some(rect) = node.bounding_box_in_coordinate_space(self) { + let origin = rect.origin(); + if point.x < origin.x || point.y < origin.y { + return Position { + root_node: *self, + inner: self.document_start(), + }; + } + } + } + + for node in self.inline_text_boxes().rev() { + if let Some(rect) = node.bounding_box_in_coordinate_space(self) { + if let Some(direction) = node.data().text_direction { + let is_past_end = match direction { + TextDirection::LeftToRight => { + point.y >= rect.y0 && point.y < rect.y1 && point.x >= rect.x1 + } + TextDirection::RightToLeft => { + point.y >= rect.y0 && point.y < rect.y1 && point.x < rect.x0 + } + // Note: The following directions assume that the rectangle, + // in the root node's coordinate space, is y-down. TBD: Will we + // ever encounter a case where this isn't true? + TextDirection::TopToBottom => { + point.x >= rect.x0 && point.x < rect.x1 && point.y >= rect.y1 + } + TextDirection::BottomToTop => { + point.x >= rect.x0 && point.x < rect.x1 && point.y < rect.y0 + } + }; + if is_past_end { + return Position { + root_node: *self, + inner: InnerPosition { + node, + character_index: node.data().character_lengths.len(), + }, + }; + } + } + } + } + + Position { + root_node: *self, + inner: self.document_end(), + } + } +} + +#[cfg(test)] +mod tests { + use accesskit::kurbo::{Point, Rect}; + use accesskit::{NodeId, TextSelection}; + use std::{num::NonZeroU128, sync::Arc}; + + use crate::tests::NullActionHandler; + + const NODE_ID_1: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(1) }); + const NODE_ID_2: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(2) }); + const NODE_ID_3: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(3) }); + const NODE_ID_4: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(4) }); + const NODE_ID_5: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(5) }); + const NODE_ID_6: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(6) }); + const NODE_ID_7: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(7) }); + const NODE_ID_8: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(8) }); + + // This is based on an actual tree produced by egui. + fn main_multiline_tree(selection: Option) -> crate::Tree { + use accesskit::kurbo::Affine; + use accesskit::{Node, Role, TextDirection, Tree, TreeUpdate}; + + let update = TreeUpdate { + nodes: vec![ + ( + NODE_ID_1, + Arc::new(Node { + role: Role::Window, + transform: Some(Box::new(Affine::scale(1.5))), + children: vec![NODE_ID_2], + ..Default::default() + }), + ), + ( + NODE_ID_2, + Arc::new(Node { + role: Role::TextField, + bounds: Some(Rect { + x0: 8.0, + y0: 31.666664123535156, + x1: 296.0, + y1: 123.66666412353516, + }), + children: vec![ + NODE_ID_3, NODE_ID_4, NODE_ID_5, NODE_ID_6, NODE_ID_7, NODE_ID_8, + ], + focusable: true, + text_selection: selection, + ..Default::default() + }), + ), + ( + NODE_ID_3, + Arc::new(Node { + role: Role::InlineTextBox, + bounds: Some(Rect { + x0: 12.0, + y0: 33.666664123535156, + x1: 290.9189147949219, + y1: 48.33333206176758, + }), + value: Some("This paragraph is long enough to wrap ".into()), + text_direction: Some(TextDirection::LeftToRight), + character_lengths: vec![ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + ] + .into(), + character_positions: Some( + vec![ + 0.0, 7.3333335, 14.666667, 22.0, 29.333334, 36.666668, 44.0, + 51.333332, 58.666668, 66.0, 73.333336, 80.666664, 88.0, 95.333336, + 102.666664, 110.0, 117.333336, 124.666664, 132.0, 139.33333, + 146.66667, 154.0, 161.33333, 168.66667, 176.0, 183.33333, + 190.66667, 198.0, 205.33333, 212.66667, 220.0, 227.33333, + 234.66667, 242.0, 249.33333, 256.66666, 264.0, 271.33334, + ] + .into(), + ), + character_widths: Some( + vec![ + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, + 7.58557, 7.58557, 7.58557, + ] + .into(), + ), + word_lengths: vec![5, 10, 3, 5, 7, 3, 5].into(), + ..Default::default() + }), + ), + ( + NODE_ID_4, + Arc::new(Node { + role: Role::InlineTextBox, + bounds: Some(Rect { + x0: 12.0, + y0: 48.33333206176758, + x1: 129.5855712890625, + y1: 63.0, + }), + value: Some("to another line.\n".into()), + text_direction: Some(TextDirection::LeftToRight), + character_lengths: vec![1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + .into(), + character_positions: Some( + vec![ + 0.0, 7.3333435, 14.666687, 22.0, 29.333344, 36.666687, 44.0, + 51.333344, 58.666687, 66.0, 73.33334, 80.66669, 88.0, 95.33334, + 102.66669, 110.0, 117.58557, + ] + .into(), + ), + character_widths: Some( + vec![ + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, + 7.58557, 7.58557, 0.0, + ] + .into(), + ), + word_lengths: vec![3, 8, 6].into(), + ..Default::default() + }), + ), + ( + NODE_ID_5, + Arc::new(Node { + role: Role::InlineTextBox, + bounds: Some(Rect { + x0: 12.0, + y0: 63.0, + x1: 144.25222778320313, + y1: 77.66666412353516, + }), + value: Some("Another paragraph.\n".into()), + text_direction: Some(TextDirection::LeftToRight), + character_lengths: vec![ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + ] + .into(), + character_positions: Some( + vec![ + 0.0, 7.3333335, 14.666667, 22.0, 29.333334, 36.666668, 44.0, + 51.333332, 58.666668, 66.0, 73.333336, 80.666664, 88.0, 95.333336, + 102.666664, 110.0, 117.333336, 124.666664, 132.25223, + ] + .into(), + ), + character_widths: Some( + vec![ + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, + 7.58557, 7.58557, 7.58557, 7.58557, 0.0, + ] + .into(), + ), + word_lengths: vec![8, 11].into(), + ..Default::default() + }), + ), + ( + NODE_ID_6, + Arc::new(Node { + role: Role::InlineTextBox, + bounds: Some(Rect { + x0: 12.0, + y0: 77.66666412353516, + x1: 12.0, + y1: 92.33332824707031, + }), + value: Some("\n".into()), + text_direction: Some(TextDirection::LeftToRight), + character_lengths: vec![1].into(), + character_positions: Some(vec![0.0].into()), + character_widths: Some(vec![0.0].into()), + word_lengths: vec![1].into(), + ..Default::default() + }), + ), + ( + NODE_ID_7, + Arc::new(Node { + role: Role::InlineTextBox, + bounds: Some(Rect { + x0: 12.0, + y0: 92.33332824707031, + x1: 158.9188995361328, + y1: 107.0, + }), + value: Some("Last non-blank line.\n".into()), + text_direction: Some(TextDirection::LeftToRight), + character_lengths: vec![ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + ] + .into(), + character_positions: Some( + vec![ + 0.0, 7.3333335, 14.666667, 22.0, 29.333334, 36.666668, 44.0, + 51.333332, 58.666668, 66.0, 73.333336, 80.666664, 88.0, 95.333336, + 102.666664, 110.0, 117.333336, 124.666664, 132.0, 139.33333, + 146.9189, + ] + .into(), + ), + character_widths: Some( + vec![ + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, + 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 0.0, + ] + .into(), + ), + word_lengths: vec![5, 4, 6, 6].into(), + ..Default::default() + }), + ), + ( + NODE_ID_8, + Arc::new(Node { + role: Role::InlineTextBox, + bounds: Some(Rect { + x0: 12.0, + y0: 107.0, + x1: 12.0, + y1: 121.66666412353516, + }), + value: Some("".into()), + text_direction: Some(TextDirection::LeftToRight), + character_lengths: vec![].into(), + character_positions: Some(vec![].into()), + character_widths: Some(vec![].into()), + word_lengths: vec![0].into(), + ..Default::default() + }), + ), + ], + tree: Some(Tree::new(NODE_ID_1)), + focus: Some(NODE_ID_2), + }; + + crate::Tree::new(update, Box::new(NullActionHandler {})) + } + + fn multiline_end_selection() -> TextSelection { + use accesskit::TextPosition; + + TextSelection { + anchor: TextPosition { + node: NODE_ID_8, + character_index: 0, + }, + focus: TextPosition { + node: NODE_ID_8, + character_index: 0, + }, + } + } + + fn multiline_wrapped_line_end_selection() -> TextSelection { + use accesskit::TextPosition; + + TextSelection { + anchor: TextPosition { + node: NODE_ID_3, + character_index: 38, + }, + focus: TextPosition { + node: NODE_ID_3, + character_index: 38, + }, + } + } + + fn multiline_second_line_middle_selection() -> TextSelection { + use accesskit::TextPosition; + + TextSelection { + anchor: TextPosition { + node: NODE_ID_4, + character_index: 5, + }, + focus: TextPosition { + node: NODE_ID_4, + character_index: 5, + }, + } + } + + #[test] + fn supports_text_ranges() { + let tree = main_multiline_tree(None); + let state = tree.read(); + assert!(!state.node_by_id(NODE_ID_1).unwrap().supports_text_ranges()); + assert!(state.node_by_id(NODE_ID_2).unwrap().supports_text_ranges()); + } + + #[test] + fn multiline_document_range() { + let tree = main_multiline_tree(None); + let state = tree.read(); + let node = state.node_by_id(NODE_ID_2).unwrap(); + let range = node.document_range(); + let start = range.start(); + assert!(start.is_word_start()); + assert!(start.is_line_start()); + assert!(!start.is_line_end()); + assert!(start.is_paragraph_start()); + assert!(start.is_document_start()); + assert!(!start.is_document_end()); + let end = range.end(); + assert!(start < end); + assert!(end.is_word_start()); + assert!(end.is_line_start()); + assert!(end.is_line_end()); + assert!(end.is_paragraph_start()); + assert!(!end.is_document_start()); + assert!(end.is_document_end()); + assert_eq!(range.text(), "This paragraph is long enough to wrap to another line.\nAnother paragraph.\n\nLast non-blank line.\n"); + assert_eq!( + range.bounding_boxes(), + vec![ + Rect { + x0: 18.0, + y0: 50.499996185302734, + x1: 436.3783721923828, + y1: 72.49999809265137 + }, + Rect { + x0: 18.0, + y0: 72.49999809265137, + x1: 194.37835693359375, + y1: 94.5 + }, + Rect { + x0: 18.0, + y0: 94.5, + x1: 216.3783416748047, + y1: 116.49999618530273 + }, + Rect { + x0: 18.0, + y0: 116.49999618530273, + x1: 18.0, + y1: 138.49999237060547 + }, + Rect { + x0: 18.0, + y0: 138.49999237060547, + x1: 238.37834930419922, + y1: 160.5 + } + ] + ); + } + + #[test] + fn multiline_end_degenerate_range() { + let tree = main_multiline_tree(Some(multiline_end_selection())); + let state = tree.read(); + let node = state.node_by_id(NODE_ID_2).unwrap(); + let range = node.text_selection().unwrap(); + assert!(range.is_degenerate()); + let pos = range.start(); + assert!(pos.is_word_start()); + assert!(pos.is_line_start()); + assert!(pos.is_line_end()); + assert!(pos.is_paragraph_start()); + assert!(!pos.is_document_start()); + assert!(pos.is_document_end()); + assert_eq!(range.text(), ""); + assert_eq!( + range.bounding_boxes(), + vec![Rect { + x0: 18.0, + y0: 160.5, + x1: 18.0, + y1: 182.49999618530273, + }] + ); + } + + #[test] + fn multiline_wrapped_line_end_range() { + let tree = main_multiline_tree(Some(multiline_wrapped_line_end_selection())); + let state = tree.read(); + let node = state.node_by_id(NODE_ID_2).unwrap(); + let range = node.text_selection().unwrap(); + assert!(range.is_degenerate()); + let pos = range.start(); + assert!(!pos.is_word_start()); + assert!(!pos.is_line_start()); + assert!(pos.is_line_end()); + assert!(!pos.is_paragraph_start()); + assert!(!pos.is_document_start()); + assert!(!pos.is_document_end()); + assert_eq!(range.text(), ""); + assert_eq!( + range.bounding_boxes(), + vec![Rect { + x0: 436.3783721923828, + y0: 50.499996185302734, + x1: 436.3783721923828, + y1: 72.49999809265137 + }] + ); + let next_char_pos = pos.forward_by_character(); + let mut line_start_range = range; + line_start_range.set_end(next_char_pos); + assert!(!line_start_range.is_degenerate()); + assert_eq!(line_start_range.text(), "t"); + assert_eq!( + line_start_range.bounding_boxes(), + vec![Rect { + x0: 18.0, + y0: 72.49999809265137, + x1: 29.378354787826538, + y1: 94.5 + }] + ); + let prev_char_pos = pos.backward_by_character(); + let mut prev_char_range = range; + prev_char_range.set_start(prev_char_pos); + assert!(!prev_char_range.is_degenerate()); + assert_eq!(prev_char_range.text(), " "); + assert_eq!( + prev_char_range.bounding_boxes(), + vec![Rect { + x0: 425.00001525878906, + y0: 50.499996185302734, + x1: 436.3783721923828, + y1: 72.49999809265137 + }] + ); + } + + #[test] + fn multiline_find_line_ends_from_middle() { + let tree = main_multiline_tree(Some(multiline_second_line_middle_selection())); + let state = tree.read(); + let node = state.node_by_id(NODE_ID_2).unwrap(); + let mut range = node.text_selection().unwrap(); + assert!(range.is_degenerate()); + let pos = range.start(); + assert!(!pos.is_line_start()); + assert!(!pos.is_line_end()); + assert!(!pos.is_document_start()); + assert!(!pos.is_document_end()); + let line_start = pos.backward_by_line(); + range.set_start(line_start); + let line_end = line_start.forward_by_line(); + range.set_end(line_end); + assert!(!range.is_degenerate()); + assert_eq!(range.text(), "to another line.\n"); + assert_eq!( + range.bounding_boxes(), + vec![Rect { + x0: 18.0, + y0: 72.49999809265137, + x1: 194.37835693359375, + y1: 94.5 + },] + ); + } + + #[test] + fn multiline_find_paragraph_ends_from_middle() { + let tree = main_multiline_tree(Some(multiline_second_line_middle_selection())); + let state = tree.read(); + let node = state.node_by_id(NODE_ID_2).unwrap(); + let mut range = node.text_selection().unwrap(); + assert!(range.is_degenerate()); + let pos = range.start(); + assert!(!pos.is_paragraph_start()); + assert!(!pos.is_document_start()); + assert!(!pos.is_document_end()); + let paragraph_start = pos.backward_by_paragraph(); + range.set_start(paragraph_start); + let paragraph_end = paragraph_start.forward_by_paragraph(); + range.set_end(paragraph_end); + assert!(!range.is_degenerate()); + assert_eq!( + range.text(), + "This paragraph is long enough to wrap to another line.\n" + ); + assert_eq!( + range.bounding_boxes(), + vec![ + Rect { + x0: 18.0, + y0: 50.499996185302734, + x1: 436.3783721923828, + y1: 72.49999809265137 + }, + Rect { + x0: 18.0, + y0: 72.49999809265137, + x1: 194.37835693359375, + y1: 94.5 + }, + ] + ); + } + + #[test] + fn multiline_find_word_ends_from_middle() { + let tree = main_multiline_tree(Some(multiline_second_line_middle_selection())); + let state = tree.read(); + let node = state.node_by_id(NODE_ID_2).unwrap(); + let mut range = node.text_selection().unwrap(); + assert!(range.is_degenerate()); + let pos = range.start(); + assert!(!pos.is_word_start()); + assert!(!pos.is_document_start()); + assert!(!pos.is_document_end()); + let word_start = pos.backward_by_word(); + range.set_start(word_start); + let word_end = word_start.forward_by_word(); + range.set_end(word_end); + assert!(!range.is_degenerate()); + assert_eq!(range.text(), "another "); + assert_eq!( + range.bounding_boxes(), + vec![Rect { + x0: 51.0, + y0: 72.49999809265137, + x1: 139.3783721923828, + y1: 94.5 + }] + ); + } + + #[test] + fn text_position_at_point() { + let tree = main_multiline_tree(None); + let state = tree.read(); + let node = state.node_by_id(NODE_ID_2).unwrap(); + + { + let pos = node.text_position_at_point(Point::new(8.0, 31.666664123535156)); + assert!(pos.is_document_start()); + } + + { + let pos = node.text_position_at_point(Point::new(12.0, 33.666664123535156)); + assert!(pos.is_document_start()); + } + + { + let pos = node.text_position_at_point(Point::new(16.0, 40.0)); + assert!(pos.is_document_start()); + } + + { + let pos = node.text_position_at_point(Point::new(144.0, 40.0)); + assert!(!pos.is_document_start()); + assert!(!pos.is_document_end()); + assert!(!pos.is_line_end()); + let mut range = pos.to_degenerate_range(); + range.set_end(pos.forward_by_character()); + assert_eq!(range.text(), "l"); + } + + { + let pos = node.text_position_at_point(Point::new(150.0, 40.0)); + assert!(!pos.is_document_start()); + assert!(!pos.is_document_end()); + assert!(!pos.is_line_end()); + let mut range = pos.to_degenerate_range(); + range.set_end(pos.forward_by_character()); + assert_eq!(range.text(), "l"); + } + + { + let pos = node.text_position_at_point(Point::new(291.0, 40.0)); + assert!(!pos.is_document_start()); + assert!(!pos.is_document_end()); + assert!(pos.is_line_end()); + let mut range = pos.to_degenerate_range(); + range.set_start(pos.backward_by_word()); + assert_eq!(range.text(), "wrap "); + } + + { + let pos = node.text_position_at_point(Point::new(12.0, 50.0)); + assert!(!pos.is_document_start()); + assert!(pos.is_line_start()); + assert!(!pos.is_paragraph_start()); + let mut range = pos.to_degenerate_range(); + range.set_end(pos.forward_by_word()); + assert_eq!(range.text(), "to "); + } + + { + let pos = node.text_position_at_point(Point::new(130.0, 50.0)); + assert!(!pos.is_document_start()); + assert!(!pos.is_document_end()); + assert!(pos.is_line_end()); + let mut range = pos.to_degenerate_range(); + range.set_start(pos.backward_by_word()); + assert_eq!(range.text(), "line.\n"); + } + + { + let pos = node.text_position_at_point(Point::new(12.0, 80.0)); + assert!(!pos.is_document_start()); + assert!(!pos.is_document_end()); + assert!(pos.is_line_end()); + let mut range = pos.to_degenerate_range(); + range.set_start(pos.backward_by_line()); + assert_eq!(range.text(), "\n"); + } + + { + let pos = node.text_position_at_point(Point::new(12.0, 120.0)); + assert!(pos.is_document_end()); + } + + { + let pos = node.text_position_at_point(Point::new(250.0, 122.0)); + assert!(pos.is_document_end()); + } + } +} diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 4ca21bf50..6fea28c2d 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -4,8 +4,8 @@ // the LICENSE-MIT file), at your option. use accesskit::{ - Action, ActionData, ActionHandler, ActionRequest, Node as NodeData, NodeId, Tree as TreeData, - TreeUpdate, + Action, ActionData, ActionHandler, ActionRequest, Node as NodeData, NodeId, TextSelection, + Tree as TreeData, TreeUpdate, }; use parking_lot::{RwLock, RwLockWriteGuard}; use std::{ @@ -14,7 +14,10 @@ use std::{ sync::Arc, }; -use crate::Node; +use crate::{ + text::{Position as TextPosition, Range as TextRange}, + Node, +}; #[derive(Clone, Copy, PartialEq, Eq)] pub(crate) struct ParentAndIndex(pub(crate) NodeId, pub(crate) usize); @@ -340,6 +343,34 @@ impl Tree { data: Some(ActionData::NumericValue(value)), }) } + + pub fn scroll_into_view(&self, target: NodeId) { + self.action_handler.do_action(ActionRequest { + action: Action::ScrollIntoView, + target, + data: None, + }) + } + + pub fn scroll_text_position_into_view(&self, target: &TextPosition) { + self.action_handler.do_action(ActionRequest { + action: Action::ScrollIntoView, + target: target.inner.node.id(), + data: None, + }) + } + + pub fn select_text_range(&self, range: &TextRange) { + let selection = TextSelection { + anchor: range.start.downgrade(), + focus: range.end.downgrade(), + }; + self.action_handler.do_action(ActionRequest { + action: Action::SetTextSelection, + target: range.node.id(), + data: Some(ActionData::SetTextSelection(selection)), + }) + } } #[cfg(test)] diff --git a/platforms/windows/Cargo.toml b/platforms/windows/Cargo.toml index 2aefc7a99..46e260804 100644 --- a/platforms/windows/Cargo.toml +++ b/platforms/windows/Cargo.toml @@ -15,6 +15,7 @@ accesskit = { version = "0.6.1", path = "../../common" } accesskit_consumer = { version = "0.6.1", path = "../../consumer" } arrayvec = "0.7.1" lazy-init = "0.5.0" +parking_lot = "0.11.2" paste = "1.0" [dependencies.windows] @@ -34,6 +35,5 @@ features = [ [dev-dependencies] lazy_static = "1.4.0" -parking_lot = "0.11.2" scopeguard = "1.1.0" winit = "0.27.3" diff --git a/platforms/windows/src/adapter.rs b/platforms/windows/src/adapter.rs index 1e327c88e..93fd678dc 100644 --- a/platforms/windows/src/adapter.rs +++ b/platforms/windows/src/adapter.rs @@ -3,9 +3,9 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use std::sync::Arc; +use std::{collections::HashSet, sync::Arc}; -use accesskit::{ActionHandler, Live, TreeUpdate}; +use accesskit::{ActionHandler, Live, NodeId, Role, TreeUpdate}; use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler}; use lazy_init::LazyTransform; use windows::Win32::{ @@ -100,9 +100,40 @@ impl Adapter { tree: &'a Arc, hwnd: HWND, queue: Vec, + text_changed: HashSet, + } + impl Handler<'_> { + fn insert_text_change_if_needed(&mut self, node: &Node) { + if node.role() != Role::InlineTextBox { + return; + } + if let Some(node) = node.filtered_parent(&filter) { + if !node.supports_text_ranges() { + return; + } + let id = node.id(); + if self.text_changed.contains(&id) { + return; + } + let platform_node = PlatformNode::new(self.tree, node.id(), self.hwnd); + let element: IRawElementProviderSimple = platform_node.into(); + // Text change events must come before selection change + // events. It doesn't matter if text change events come + // before other events. + self.queue.insert( + 0, + QueuedEvent::Simple { + element, + event_id: UIA_Text_TextChangedEventId, + }, + ); + self.text_changed.insert(id); + } + } } impl TreeChangeHandler for Handler<'_> { fn node_added(&mut self, node: &Node) { + self.insert_text_change_if_needed(node); if filter(node) != FilterResult::Include { return; } @@ -116,6 +147,9 @@ impl Adapter { } } fn node_updated(&mut self, old_node: &Node, new_node: &Node) { + if old_node.value() != new_node.value() { + self.insert_text_change_if_needed(new_node); + } if filter(new_node) != FilterResult::Include { return; } @@ -146,13 +180,16 @@ impl Adapter { }); } } - fn node_removed(&mut self, _node: &Node) {} + fn node_removed(&mut self, node: &Node) { + self.insert_text_change_if_needed(node); + } // TODO: handle other events (#20) } let mut handler = Handler { tree, hwnd: self.hwnd, queue: Vec::new(), + text_changed: HashSet::new(), }; tree.update_and_process_changes(update, &mut handler); QueuedEvents(handler.queue) diff --git a/platforms/windows/src/lib.rs b/platforms/windows/src/lib.rs index 27582fb81..c907faf10 100644 --- a/platforms/windows/src/lib.rs +++ b/platforms/windows/src/lib.rs @@ -4,6 +4,7 @@ // the LICENSE-MIT file), at your option. mod node; +mod text; mod util; mod adapter; diff --git a/platforms/windows/src/node.rs b/platforms/windows/src/node.rs index e0a7dc252..56265e2c5 100644 --- a/platforms/windows/src/node.rs +++ b/platforms/windows/src/node.rs @@ -18,10 +18,10 @@ use paste::paste; use std::sync::{Arc, Weak}; use windows::{ core::*, - Win32::{Foundation::*, Graphics::Gdi::*, System::Com::*, UI::Accessibility::*}, + Win32::{Foundation::*, System::Com::*, UI::Accessibility::*}, }; -use crate::util::*; +use crate::{text::PlatformRange as PlatformTextRange, util::*}; fn runtime_id_from_node_id(id: NodeId) -> impl std::ops::Deref { let mut result = ArrayVec::() + 1 }>::new(); @@ -408,6 +408,10 @@ impl<'a> NodeWrapper<'a> { } } + fn is_text_pattern_supported(&self) -> bool { + self.node.supports_text_ranges() + } + pub(crate) fn enqueue_property_changes( &self, queue: &mut Vec, @@ -434,6 +438,15 @@ impl<'a> NodeWrapper<'a> { event_id: UIA_SelectionItem_ElementSelectedEventId, }); } + if self.is_text_pattern_supported() + && old.is_text_pattern_supported() + && self.node.text_selection() != old.node.text_selection() + { + queue.push(QueuedEvent::Simple { + element: element.clone(), + event_id: UIA_Text_TextSelectionChangedEventId, + }); + } } fn enqueue_property_change( @@ -468,10 +481,6 @@ impl<'a> NodeWrapper<'a> { } } -fn element_not_available() -> Error { - Error::new(HRESULT(UIA_E_ELEMENTNOTAVAILABLE as i32), "".into()) -} - #[implement( IRawElementProviderSimple, IRawElementProviderFragment, @@ -480,12 +489,13 @@ fn element_not_available() -> Error { IInvokeProvider, IValueProvider, IRangeValueProvider, - ISelectionItemProvider + ISelectionItemProvider, + ITextProvider )] pub(crate) struct PlatformNode { - tree: Weak, - node_id: NodeId, - hwnd: HWND, + pub(crate) tree: Weak, + pub(crate) node_id: NodeId, + pub(crate) hwnd: HWND, } impl PlatformNode { @@ -553,11 +563,7 @@ impl PlatformNode { } fn client_top_left(&self) -> Point { - let mut result = POINT::default(); - // If ClientToScreen fails, that means the window is gone. - // That's an unexpected condition, so we should fail loudly. - unsafe { ClientToScreen(self.hwnd, &mut result) }.unwrap(); - Point::new(result.x.into(), result.y.into()) + client_top_left(self.hwnd) } } @@ -839,5 +845,58 @@ patterns! { Err(Error::new(E_FAIL, "".into())) }) } + )), + (Text, is_text_pattern_supported, (), ( + fn GetSelection(&self) -> Result<*mut SAFEARRAY> { + self.resolve(|wrapper| { + if let Some(range) = wrapper.node.text_selection() { + let platform_range: ITextRangeProvider = PlatformTextRange::new(&self.tree, range, self.hwnd).into(); + let iunknown: IUnknown = platform_range.into(); + Ok(safe_array_from_com_slice(&[iunknown])) + } else { + Ok(std::ptr::null_mut()) + } + }) + }, + + fn GetVisibleRanges(&self) -> Result<*mut SAFEARRAY> { + // TBD: Do we need this? The Quorum GUI toolkit, which is our + // current point of comparison for text functionality, + // doesn't implement it. + Ok(std::ptr::null_mut()) + }, + + fn RangeFromChild(&self, _child: &Option) -> Result { + // We don't support embedded objects in text. + Err(not_implemented()) + }, + + fn RangeFromPoint(&self, point: &UiaPoint) -> Result { + self.resolve(|wrapper| { + let client_top_left = self.client_top_left(); + let point = Point::new(point.x - client_top_left.x, point.y - client_top_left.y); + let point = wrapper.node.transform().inverse() * point; + let pos = wrapper.node.text_position_at_point(point); + let range = pos.to_degenerate_range(); + Ok(PlatformTextRange::new(&self.tree, range, self.hwnd).into()) + }) + }, + + fn DocumentRange(&self) -> Result { + self.resolve(|wrapper| { + let range = wrapper.node.document_range(); + Ok(PlatformTextRange::new(&self.tree, range, self.hwnd).into()) + }) + }, + + fn SupportedTextSelection(&self) -> Result { + self.resolve(|wrapper| { + if wrapper.node.has_text_selection() { + Ok(SupportedTextSelection_Single) + } else { + Ok(SupportedTextSelection_None) + } + }) + } )) } diff --git a/platforms/windows/src/text.rs b/platforms/windows/src/text.rs new file mode 100644 index 000000000..ec0f736d0 --- /dev/null +++ b/platforms/windows/src/text.rs @@ -0,0 +1,576 @@ +// Copyright 2022 The AccessKit Authors. All rights reserved. +// Licensed under the Apache License, Version 2.0 (found in +// the LICENSE-APACHE file) or the MIT license (found in +// the LICENSE-MIT file), at your option. + +#![allow(non_upper_case_globals)] + +use accesskit_consumer::{ + Node, TextPosition as Position, TextRange as Range, Tree, TreeState, WeakTextRange as WeakRange, +}; +use parking_lot::RwLock; +use std::sync::{Arc, Weak}; +use windows::{ + core::*, + Win32::{Foundation::*, System::Com::*, UI::Accessibility::*}, +}; + +use crate::{node::PlatformNode, util::*}; + +fn upgrade_range<'a>(weak: &WeakRange, tree_state: &'a TreeState) -> Result> { + if let Some(range) = weak.upgrade(tree_state) { + Ok(range) + } else { + Err(element_not_available()) + } +} + +fn upgrade_range_node<'a>(weak: &WeakRange, tree_state: &'a TreeState) -> Result> { + if let Some(node) = weak.upgrade_node(tree_state) { + Ok(node) + } else { + Err(element_not_available()) + } +} + +fn weak_comparable_position_from_endpoint( + range: &WeakRange, + endpoint: TextPatternRangeEndpoint, +) -> Result<&(Vec, usize)> { + match endpoint { + TextPatternRangeEndpoint_Start => Ok(range.start_comparable()), + TextPatternRangeEndpoint_End => Ok(range.end_comparable()), + _ => Err(invalid_arg()), + } +} + +fn position_from_endpoint<'a>( + range: &Range<'a>, + endpoint: TextPatternRangeEndpoint, +) -> Result> { + match endpoint { + TextPatternRangeEndpoint_Start => Ok(range.start()), + TextPatternRangeEndpoint_End => Ok(range.end()), + _ => Err(invalid_arg()), + } +} + +fn set_endpoint_position<'a>( + range: &mut Range<'a>, + endpoint: TextPatternRangeEndpoint, + pos: Position<'a>, +) -> Result<()> { + match endpoint { + TextPatternRangeEndpoint_Start => { + range.set_start(pos); + } + TextPatternRangeEndpoint_End => { + range.set_end(pos); + } + _ => { + return Err(invalid_arg()); + } + } + Ok(()) +} + +fn back_to_unit_start(start: Position, unit: TextUnit) -> Result { + match unit { + TextUnit_Character => { + // If we get here, this position is at the start of a non-degenerate + // range, so it's always at the start of a character. + debug_assert!(!start.is_document_end()); + Ok(start) + } + TextUnit_Format => { + if start.is_format_start() { + Ok(start) + } else { + Ok(start.backward_by_format()) + } + } + TextUnit_Word => { + if start.is_word_start() { + Ok(start) + } else { + Ok(start.backward_by_word()) + } + } + TextUnit_Line => { + if start.is_line_start() { + Ok(start) + } else { + Ok(start.backward_by_line()) + } + } + TextUnit_Paragraph => { + if start.is_paragraph_start() { + Ok(start) + } else { + Ok(start.backward_by_paragraph()) + } + } + TextUnit_Page => { + if start.is_page_start() { + Ok(start) + } else { + Ok(start.backward_by_page()) + } + } + TextUnit_Document => { + if start.is_document_start() { + Ok(start) + } else { + Ok(start.backward_by_document()) + } + } + _ => Err(invalid_arg()), + } +} + +fn move_position_once(pos: Position, unit: TextUnit, forward: bool) -> Result { + match unit { + TextUnit_Character => { + if forward { + Ok(pos.forward_by_character()) + } else { + Ok(pos.backward_by_character()) + } + } + TextUnit_Format => { + if forward { + Ok(pos.forward_by_format()) + } else { + Ok(pos.backward_by_format()) + } + } + TextUnit_Word => { + if forward { + Ok(pos.forward_by_word()) + } else { + Ok(pos.backward_by_word()) + } + } + TextUnit_Line => { + if forward { + Ok(pos.forward_by_line()) + } else { + Ok(pos.backward_by_line()) + } + } + TextUnit_Paragraph => { + if forward { + Ok(pos.forward_by_paragraph()) + } else { + Ok(pos.backward_by_paragraph()) + } + } + TextUnit_Page => { + if forward { + Ok(pos.forward_by_page()) + } else { + Ok(pos.backward_by_page()) + } + } + TextUnit_Document => { + if forward { + Ok(pos.forward_by_document()) + } else { + Ok(pos.backward_by_document()) + } + } + _ => Err(invalid_arg()), + } +} + +fn move_position(mut pos: Position, unit: TextUnit, count: i32) -> Result<(Position, i32)> { + let forward = count > 0; + let count = count.abs(); + let mut moved = 0i32; + for _ in 0..count { + let at_end = if forward { + pos.is_document_end() + } else { + pos.is_document_start() + }; + if at_end { + break; + } + pos = move_position_once(pos, unit, forward)?; + moved += 1; + } + if !forward { + moved = -moved; + } + Ok((pos, moved)) +} + +#[implement(ITextRangeProvider)] +pub(crate) struct PlatformRange { + tree: Weak, + state: RwLock, + hwnd: HWND, +} + +impl PlatformRange { + pub(crate) fn new(tree: &Weak, range: Range, hwnd: HWND) -> Self { + Self { + tree: tree.clone(), + state: RwLock::new(range.downgrade()), + hwnd, + } + } + + fn upgrade_tree(&self) -> Result> { + if let Some(tree) = self.tree.upgrade() { + Ok(tree) + } else { + Err(element_not_available()) + } + } + + fn with_tree_state(&self, f: F) -> Result + where + F: FnOnce(&TreeState) -> Result, + { + let tree = self.upgrade_tree()?; + let state = tree.read(); + f(&state) + } + + fn upgrade_node<'a>(&self, tree_state: &'a TreeState) -> Result> { + let state = self.state.read(); + upgrade_range_node(&state, tree_state) + } + + fn with_node(&self, f: F) -> Result + where + F: FnOnce(Node) -> Result, + { + self.with_tree_state(|tree_state| { + let node = self.upgrade_node(tree_state)?; + f(node) + }) + } + + fn upgrade_for_read<'a>(&self, tree_state: &'a TreeState) -> Result> { + let state = self.state.read(); + upgrade_range(&state, tree_state) + } + + fn read(&self, f: F) -> Result + where + F: FnOnce(Range) -> Result, + { + self.with_tree_state(|tree_state| { + let range = self.upgrade_for_read(tree_state)?; + f(range) + }) + } + + fn write(&self, f: F) -> Result + where + F: FnOnce(&mut Range) -> Result, + { + self.with_tree_state(|tree_state| { + let mut state = self.state.write(); + let mut range = upgrade_range(&state, tree_state)?; + let result = f(&mut range); + *state = range.downgrade(); + result + }) + } + + fn action(&self, f: F) -> Result<()> + where + for<'a> F: FnOnce(&'a Tree, Range<'a>) -> Result<()>, + { + let tree = self.upgrade_tree()?; + let tree_state = tree.read(); + let range = self.upgrade_for_read(&tree_state)?; + f(&tree, range) + } + + fn require_same_tree(&self, other: &PlatformRange) -> Result<()> { + if self.tree.ptr_eq(&other.tree) { + Ok(()) + } else { + Err(invalid_arg()) + } + } +} + +impl Clone for PlatformRange { + fn clone(&self) -> Self { + PlatformRange { + tree: self.tree.clone(), + state: RwLock::new(self.state.read().clone()), + hwnd: self.hwnd, + } + } +} + +// Some text range methods take another text range interface pointer as a +// parameter. We need to cast these interface pointers to their underlying +// implementations. We assume that AccessKit is the only UIA provider +// within this process. This seems a safe assumption for most AccessKit users. + +impl ITextRangeProvider_Impl for PlatformRange { + fn Clone(&self) -> Result { + Ok(self.clone().into()) + } + + fn Compare(&self, other: &Option) -> Result { + let other = required_param(other)?.as_impl(); + Ok((self.tree.ptr_eq(&other.tree) && *self.state.read() == *other.state.read()).into()) + } + + fn CompareEndpoints( + &self, + endpoint: TextPatternRangeEndpoint, + other: &Option, + other_endpoint: TextPatternRangeEndpoint, + ) -> Result { + let other = required_param(other)?.as_impl(); + if std::ptr::eq(other as *const _, self as *const _) { + // Comparing endpoints within the same range can be done + // safely without upgrading the range. This allows ATs + // to determine whether an old range is degenerate even if + // that range is no longer valid. + let state = self.state.read(); + let other_state = other.state.read(); + let pos = weak_comparable_position_from_endpoint(&state, endpoint)?; + let other_pos = weak_comparable_position_from_endpoint(&other_state, other_endpoint)?; + let result = pos.cmp(other_pos); + return Ok(result as i32); + } + self.require_same_tree(other)?; + self.with_tree_state(|tree_state| { + let range = self.upgrade_for_read(tree_state)?; + let other_range = other.upgrade_for_read(tree_state)?; + if range.node().id() != other_range.node().id() { + return Err(invalid_arg()); + } + let pos = position_from_endpoint(&range, endpoint)?; + let other_pos = position_from_endpoint(&other_range, other_endpoint)?; + let result = pos.partial_cmp(&other_pos).unwrap(); + Ok(result as i32) + }) + } + + fn ExpandToEnclosingUnit(&self, unit: TextUnit) -> Result<()> { + if unit == TextUnit_Document { + // Handle document as a special case so we can get to a document + // range even if the current endpoints are now invalid. + // Based on observed behavior, Narrator needs this ability. + return self.with_tree_state(|tree_state| { + let mut state = self.state.write(); + let node = upgrade_range_node(&state, tree_state)?; + *state = node.document_range().downgrade(); + Ok(()) + }); + } + self.write(|range| { + let start = range.start(); + if unit == TextUnit_Character && start.is_document_end() { + // We know from experimentation that some Windows ATs + // expect ExpandToEnclosingUnit(TextUnit_Character) + // to do nothing if the range is degenerate at the end + // of the document. + return Ok(()); + } + let start = back_to_unit_start(start, unit)?; + range.set_start(start); + if !start.is_document_end() { + let end = move_position_once(start, unit, true)?; + range.set_end(end); + } + Ok(()) + }) + } + + fn FindAttribute( + &self, + _id: i32, + _value: &VARIANT, + _backward: BOOL, + ) -> Result { + // TODO: implement when we support variable formatting (part of rich text) + // Justification: JUCE doesn't implement this. + Err(Error::OK) + } + + fn FindText( + &self, + _text: &BSTR, + _backward: BOOL, + _ignore_case: BOOL, + ) -> Result { + // TODO: implement when there's a real-world use case that requires it + // Justification: Quorum doesn't implement this and is being used + // by blind students. + Err(Error::OK) + } + + fn GetAttributeValue(&self, id: i32) -> Result { + match id { + UIA_IsReadOnlyAttributeId => { + // TBD: do we ever want to support mixed read-only/editable text? + self.with_node(|node| { + let value = node.is_read_only(); + Ok(VariantFactory::from(value).into()) + }) + } + UIA_CaretPositionAttributeId => self.read(|range| { + let mut value = CaretPosition_Unknown; + if range.is_degenerate() { + let pos = range.start(); + if pos.is_line_start() { + value = CaretPosition_BeginningOfLine; + } else if pos.is_line_end() { + value = CaretPosition_EndOfLine; + } + } + Ok(VariantFactory::from(value).into()) + }), + // TODO: implement more attributes + _ => { + let value = unsafe { UiaGetReservedNotSupportedValue() }.unwrap(); + Ok(VariantFactory::from(value).into()) + } + } + } + + fn GetBoundingRectangles(&self) -> Result<*mut SAFEARRAY> { + self.read(|range| { + let rects = range.bounding_boxes(); + if rects.is_empty() { + return Ok(std::ptr::null_mut()); + } + let client_top_left = client_top_left(self.hwnd); + let mut result = Vec::::new(); + result.reserve(rects.len() * 4); + for rect in rects { + result.push(rect.x0 + client_top_left.x); + result.push(rect.y0 + client_top_left.y); + result.push(rect.width()); + result.push(rect.height()); + } + Ok(safe_array_from_f64_slice(&result)) + }) + } + + fn GetEnclosingElement(&self) -> Result { + self.with_node(|node| { + // Revisit this if we eventually support embedded objects. + Ok(PlatformNode { + tree: self.tree.clone(), + node_id: node.id(), + hwnd: self.hwnd, + } + .into()) + }) + } + + fn GetText(&self, _max_length: i32) -> Result { + // The Microsoft docs imply that the provider isn't _required_ + // to truncate text at the max length, so we just ignore it. + self.read(|range| Ok(range.text().into())) + } + + fn Move(&self, unit: TextUnit, count: i32) -> Result { + self.write(|range| { + let degenerate = range.is_degenerate(); + let start = range.start(); + let start = if degenerate { + start + } else { + back_to_unit_start(start, unit)? + }; + let (start, moved) = move_position(start, unit, count)?; + if moved != 0 { + range.set_start(start); + let end = if degenerate || start.is_document_end() { + start + } else { + move_position_once(start, unit, true)? + }; + range.set_end(end); + } + Ok(moved) + }) + } + + fn MoveEndpointByUnit( + &self, + endpoint: TextPatternRangeEndpoint, + unit: TextUnit, + count: i32, + ) -> Result { + self.write(|range| { + let pos = position_from_endpoint(range, endpoint)?; + let (pos, moved) = move_position(pos, unit, count)?; + set_endpoint_position(range, endpoint, pos)?; + Ok(moved) + }) + } + + fn MoveEndpointByRange( + &self, + endpoint: TextPatternRangeEndpoint, + other: &Option, + other_endpoint: TextPatternRangeEndpoint, + ) -> Result<()> { + let other = required_param(other)?.as_impl(); + self.require_same_tree(other)?; + // We have to obtain the tree state and ranges manually to avoid + // lifetime issues, and work with the two locks in a specific order + // to avoid deadlock. + self.with_tree_state(|tree_state| { + let other_range = other.upgrade_for_read(tree_state)?; + let mut state = self.state.write(); + let mut range = upgrade_range(&state, tree_state)?; + if range.node().id() != other_range.node().id() { + return Err(invalid_arg()); + } + let pos = position_from_endpoint(&other_range, other_endpoint)?; + set_endpoint_position(&mut range, endpoint, pos)?; + *state = range.downgrade(); + Ok(()) + }) + } + + fn Select(&self) -> Result<()> { + self.action(|tree, range| { + tree.select_text_range(&range); + Ok(()) + }) + } + + fn AddToSelection(&self) -> Result<()> { + // AccessKit doesn't support multiple text selections. + Err(invalid_operation()) + } + + fn RemoveFromSelection(&self) -> Result<()> { + // AccessKit doesn't support multiple text selections. + Err(invalid_operation()) + } + + fn ScrollIntoView(&self, align_to_top: BOOL) -> Result<()> { + self.action(|tree, range| { + let position = if align_to_top.into() { + range.start() + } else { + range.end() + }; + tree.scroll_text_position_into_view(&position); + Ok(()) + }) + } + + fn GetChildren(&self) -> Result<*mut SAFEARRAY> { + // We don't support embedded objects in text. + Ok(std::ptr::null_mut()) + } +} diff --git a/platforms/windows/src/util.rs b/platforms/windows/src/util.rs index c77833576..0eba7dee2 100644 --- a/platforms/windows/src/util.rs +++ b/platforms/windows/src/util.rs @@ -3,10 +3,13 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. +use accesskit::kurbo::Point; use std::{convert::TryInto, mem::ManuallyDrop}; use windows::{ core::*, Win32::{ + Foundation::*, + Graphics::Gdi::*, System::{Com::*, Ole::*}, UI::Accessibility::*, }, @@ -64,6 +67,17 @@ impl From for VariantFactory { } } +impl From for VariantFactory { + fn from(value: IUnknown) -> Self { + Self( + VT_UNKNOWN, + VARIANT_0_0_0 { + punkVal: ManuallyDrop::new(Some(value)), + }, + ) + } +} + impl From for VariantFactory { fn from(value: i32) -> Self { Self(VT_I4, VARIANT_0_0_0 { lVal: value }) @@ -88,6 +102,12 @@ impl From for VariantFactory { } } +impl From for VariantFactory { + fn from(value: CaretPosition) -> Self { + value.0.into() + } +} + const VARIANT_FALSE: i16 = 0i16; const VARIANT_TRUE: i16 = -1i16; @@ -108,7 +128,7 @@ impl> From> for VariantFactory { } } -fn safe_array_from_slice(vt: VARENUM, slice: &[T]) -> *mut SAFEARRAY { +fn safe_array_from_primitive_slice(vt: VARENUM, slice: &[T]) -> *mut SAFEARRAY { let sa = unsafe { SafeArrayCreateVector(VARENUM(vt.0 as u16), 0, slice.len().try_into().unwrap()) }; if sa.is_null() { @@ -122,7 +142,23 @@ fn safe_array_from_slice(vt: VARENUM, slice: &[T]) -> *mut SAFEARRAY { } pub(crate) fn safe_array_from_i32_slice(slice: &[i32]) -> *mut SAFEARRAY { - safe_array_from_slice(VT_I4, slice) + safe_array_from_primitive_slice(VT_I4, slice) +} + +pub(crate) fn safe_array_from_f64_slice(slice: &[f64]) -> *mut SAFEARRAY { + safe_array_from_primitive_slice(VT_R8, slice) +} + +pub(crate) fn safe_array_from_com_slice(slice: &[IUnknown]) -> *mut SAFEARRAY { + let sa = unsafe { SafeArrayCreateVector(VT_UNKNOWN, 0, slice.len().try_into().unwrap()) }; + if sa.is_null() { + panic!("SAFEARRAY allocation failed"); + } + for (i, item) in slice.iter().enumerate() { + let i: i32 = i.try_into().unwrap(); + unsafe { SafeArrayPutElement(&*sa, &i, std::mem::transmute_copy(item)) }.unwrap(); + } + sa } pub(crate) enum QueuedEvent { @@ -137,3 +173,31 @@ pub(crate) enum QueuedEvent { new_value: VARIANT, }, } + +pub(crate) fn not_implemented() -> Error { + Error::new(E_NOTIMPL, "".into()) +} + +pub(crate) fn invalid_arg() -> Error { + Error::new(E_INVALIDARG, "".into()) +} + +pub(crate) fn required_param(param: &Option) -> Result<&T> { + param.as_ref().map_or_else(|| Err(invalid_arg()), Ok) +} + +pub(crate) fn element_not_available() -> Error { + Error::new(HRESULT(UIA_E_ELEMENTNOTAVAILABLE as i32), "".into()) +} + +pub(crate) fn invalid_operation() -> Error { + Error::new(HRESULT(UIA_E_INVALIDOPERATION as i32), "".into()) +} + +pub(crate) fn client_top_left(hwnd: HWND) -> Point { + let mut result = POINT::default(); + // If ClientToScreen fails, that means the window is gone. + // That's an unexpected condition, so we should fail loudly. + unsafe { ClientToScreen(hwnd, &mut result) }.unwrap(); + Point::new(result.x.into(), result.y.into()) +}