Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
d7358ac
New per-glyph and per-word indices
mwcampbell Oct 12, 2022
0a41c7c
Change and clarify the semantics of TextSelection
mwcampbell Oct 12, 2022
1e8cd85
Forgot to make TextSelection fields public
mwcampbell Oct 12, 2022
9e22677
Refactor TextSelection into two instances of a new TextPosition struct
mwcampbell Oct 13, 2022
074e3f2
Switch from 'glyph' back to 'character', and document the meaning of …
mwcampbell Oct 13, 2022
fd4c91c
Work so far on consumer text range API
mwcampbell Oct 17, 2022
afe4ff5
Stub Windows text range implementation
mwcampbell Oct 17, 2022
1165bfb
Refactor utility functions; forgot to commit the actual Windows text …
mwcampbell Oct 17, 2022
d38e332
More work on utility functions; implement Compare and CompareEndpoints
mwcampbell Oct 17, 2022
0ca03e8
Partial implementation of ExpandToEnclosingUnit
mwcampbell Oct 17, 2022
755ad30
Normalize endpoints for comparison
mwcampbell Oct 18, 2022
1cad12c
Refactor; implement MoveEndpointByRange
mwcampbell Oct 18, 2022
a655f47
More small refactors
mwcampbell Oct 18, 2022
544a2ff
Skeleton of MoveEndpointByUnit
mwcampbell Oct 18, 2022
5d8d37c
Break apart document_endpoints
mwcampbell Oct 18, 2022
ffa975e
More structure for MoveEndpointByUnit; fix a lifetime issue
mwcampbell Oct 18, 2022
923e6c0
Finish the generic logic of MoveEndpointByRange; add stub functions i…
mwcampbell Oct 18, 2022
f11a666
Hoist the document start/end check
mwcampbell Oct 18, 2022
36b35e5
Implement forward/backward by character
mwcampbell Oct 18, 2022
d9fb75c
Implement forward/backward by document
mwcampbell Oct 18, 2022
95977f4
Implement forward/backward by line
mwcampbell Oct 18, 2022
d1cc201
Implement GetEnclosingElement
mwcampbell Oct 18, 2022
1f97a61
Fix clippy warnings
mwcampbell Oct 18, 2022
8353137
Don't check hidden status of inline text boxes in the filter
mwcampbell Oct 18, 2022
3543e57
Partial implementation of GetAttributeValue
mwcampbell Oct 18, 2022
3572688
Refactor text markers; drop the 'active suggestion' marker type since…
mwcampbell Oct 19, 2022
714fa26
Refactor walk and text functions
mwcampbell Oct 19, 2022
8103199
Skeleton of new consumer attribute method
mwcampbell Oct 19, 2022
c48e3a5
Refactor walk to allow early result
mwcampbell Oct 19, 2022
110e3ce
Implement Range::attribute
mwcampbell Oct 19, 2022
253940c
Small refactor to Range::text
mwcampbell Oct 19, 2022
216b425
Find out which attribute NVDA wants first
mwcampbell Oct 19, 2022
3b125c8
Appease Rust 1.61
mwcampbell Oct 19, 2022
ff6ae5e
Basic fallback implementation of GetAttributeValue
mwcampbell Oct 19, 2022
6de129e
Get NVDA to read the current character and selection
mwcampbell Oct 19, 2022
59ae45b
Implement the Windows-specific 'expand' operation atop more generic p…
mwcampbell Oct 19, 2022
b047e83
Implement ITextRangeProvider::Move
mwcampbell Oct 19, 2022
8a09009
Rename normalize functions
mwcampbell Oct 19, 2022
50be667
Try automatically normalizing range endpoints, with a special case fo…
mwcampbell Oct 19, 2022
9872e3d
reformat
mwcampbell Oct 20, 2022
c42850d
emit text selection change events
mwcampbell Oct 20, 2022
777c5f0
Don't store TextPosition::character_index as u16; it's not worth it
mwcampbell Oct 20, 2022
ac32f48
Refactor character and word boundaries
mwcampbell Oct 21, 2022
8e0b2a0
Drop the println in GetAttributeValue for now
mwcampbell Oct 21, 2022
5b43c8c
Movement by word
mwcampbell Oct 21, 2022
92e5d7b
Return invalid operation error in AddToSelection and RemoveFromSelection
mwcampbell Oct 27, 2022
e979b20
Implement ScrollIntoView
mwcampbell Oct 27, 2022
b5beeb5
Implement Select
mwcampbell Oct 27, 2022
a39211d
Page is synonymous with document for now
mwcampbell Oct 27, 2022
4bb59db
Decide that the format unit is also synonymous with document for now,…
mwcampbell Oct 27, 2022
e1c2b86
Implement movement by paragraph
mwcampbell Oct 27, 2022
7136fa0
Decide that FindAttribute and FindText are out of scope for now
mwcampbell Oct 27, 2022
e13b22f
support one actual text attribute so far: read-only
mwcampbell Oct 27, 2022
a4e4c63
Don't crash in GetBoundingRectangles; still not implemented yet
mwcampbell Oct 27, 2022
59c33f7
Raise text change events
mwcampbell Oct 27, 2022
6b9e6f9
Require the last inline text box to end with a line break
mwcampbell Oct 28, 2022
96a44f1
Allow a couple of functions on a text range whose endpoints are no lo…
mwcampbell Oct 28, 2022
6047051
Special case for ExpandToEnclosingUnit on document
mwcampbell Oct 28, 2022
76c98a8
Clarify why character_pixel_lengths is optional
mwcampbell Oct 28, 2022
cc2acfe
Implement GetBoundingRectangles
mwcampbell Oct 28, 2022
ca30bfc
Don't require the last line to end with a newline. Fix the bug that c…
mwcampbell Oct 31, 2022
0224d23
typo
mwcampbell Oct 31, 2022
f380650
Make sure text change events are fired before selection change events
mwcampbell Nov 1, 2022
7ae2002
Allow endpoints of the same range to be compared even if the range is…
mwcampbell Nov 1, 2022
061e768
Rename character_pixel_lengths and adjust its documentation
mwcampbell Nov 7, 2022
d5d6ae9
Fix inconsistent internal naming
mwcampbell Nov 7, 2022
da7f8fb
Change the way we calculate bounding rects of text ranges
mwcampbell Nov 7, 2022
9e81cd9
Support the UIA CaretPosition attribute
mwcampbell Nov 7, 2022
6a6e521
Fix new clippy warning
mwcampbell Nov 7, 2022
3e04a64
Fix calculation of bounding rectangle for the caret at the end of a w…
mwcampbell Nov 7, 2022
a9141e1
Add a realistic test tree for multi-line text and write a few basic t…
mwcampbell Nov 7, 2022
3558a46
More tests; fix a bug in paragraph navigation
mwcampbell Nov 8, 2022
dc8eb6a
Implement hit-testing within text
mwcampbell Nov 11, 2022
0cd50bc
Traverse text boxes in reverse for the 'past end' check. Will be need…
mwcampbell Nov 11, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 105 additions & 43 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use serde_lib as serde;
use serde_lib::{Deserialize, Serialize};
use std::{
num::{NonZeroU128, NonZeroU64},
ops::Range,
sync::Arc,
};

Expand Down Expand Up @@ -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))]
Expand Down Expand Up @@ -606,19 +592,6 @@ impl From<NonZeroU64> 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<usize>,
}

/// 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
Expand Down Expand Up @@ -646,18 +619,36 @@ fn is_empty<T>(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))]
#[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 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.
Expand Down Expand Up @@ -925,25 +916,96 @@ pub struct Node {
pub radio_group: Vec<NodeId>,

#[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<TextDirection>,
/// 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<Box<[f32]>>,
/// 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<Box<[f32]>>,

/// 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<usize>]>,
pub word_lengths: Box<[u8]>,

#[cfg_attr(feature = "serde", serde(default))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))]
Expand Down
6 changes: 6 additions & 0 deletions consumer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
58 changes: 51 additions & 7 deletions consumer/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Rect> {
Expand All @@ -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<Rect> {
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<Node<'a>> {
) -> Option<(Node<'a>, Point)> {
let filter_result = filter(self);

if filter_result == FilterResult::ExcludeSubtree {
Expand All @@ -250,22 +268,32 @@ 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));
}
}
}

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<Node<'a>> {
self.hit_test(point, filter).map(|(node, _)| node)
}

pub fn id(&self) -> NodeId {
self.state.id
}
Expand Down Expand Up @@ -480,6 +508,22 @@ impl<'a> Node<'a> {
self.data().selected
}

pub fn index_path(&self) -> Vec<usize> {
self.relative_index_path(self.tree_state.root_id())
}

pub fn relative_index_path(&self, ancestor_id: NodeId) -> Vec<usize> {
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,
Expand Down
Loading