Skip to content

Commit 455e6f7

Browse files
authored
feat!: Text range support (#145)
1 parent a4befe6 commit 455e6f7

File tree

11 files changed

+2432
-74
lines changed

11 files changed

+2432
-74
lines changed

common/src/lib.rs

Lines changed: 105 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ use serde_lib as serde;
2121
use serde_lib::{Deserialize, Serialize};
2222
use std::{
2323
num::{NonZeroU128, NonZeroU64},
24-
ops::Range,
2524
sync::Arc,
2625
};
2726

@@ -409,19 +408,6 @@ pub enum DropEffect {
409408
Popup,
410409
}
411410

412-
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
413-
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
414-
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
415-
#[cfg_attr(feature = "serde", serde(crate = "serde"))]
416-
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
417-
pub enum MarkerType {
418-
SpellingError,
419-
GrammarError,
420-
SearchMatch,
421-
ActiveSuggestion,
422-
Suggestion,
423-
}
424-
425411
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
426412
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
427413
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
@@ -606,19 +592,6 @@ impl From<NonZeroU64> for NodeId {
606592
}
607593
}
608594

609-
/// A marker spanning a range within text.
610-
#[derive(Clone, Debug, PartialEq, Eq)]
611-
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
612-
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
613-
#[cfg_attr(feature = "serde", serde(crate = "serde"))]
614-
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
615-
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
616-
pub struct TextMarker {
617-
pub marker_type: MarkerType,
618-
/// Indices are in UTF-8 code units.
619-
pub range: Range<usize>,
620-
}
621-
622595
/// Defines a custom action for a UI element.
623596
///
624597
/// For example, a list UI can allow a user to reorder items in the list by dragging the
@@ -646,18 +619,36 @@ fn is_empty<T>(slice: &[T]) -> bool {
646619
slice.is_empty()
647620
}
648621

649-
/// Offsets are in UTF-8 code units.
622+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
623+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
624+
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
625+
#[cfg_attr(feature = "serde", serde(crate = "serde"))]
626+
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
627+
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
628+
pub struct TextPosition {
629+
/// The node's role must be [`Role::InlineTextBox`].
630+
pub node: NodeId,
631+
/// The index of an item in [`Node::character_lengths`], or the length
632+
/// of that slice if the position is at the end of the line.
633+
pub character_index: usize,
634+
}
635+
650636
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
651637
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
652638
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
653639
#[cfg_attr(feature = "serde", serde(crate = "serde"))]
654640
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
655641
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
656642
pub struct TextSelection {
657-
anchor_node: NodeId,
658-
anchor_offset: usize,
659-
focus_node: NodeId,
660-
focus_offset: usize,
643+
/// The position where the selection started, and which does not change
644+
/// as the selection is expanded or contracted. If there is no selection
645+
/// but only a caret, this must be equal to [`focus`]. This is also known
646+
/// as a degenerate selection.
647+
pub anchor: TextPosition,
648+
/// The active end of the selection, which changes as the selection
649+
/// is expanded or contracted, or the position of the caret if there is
650+
/// no selection.
651+
pub focus: TextPosition,
661652
}
662653

663654
/// A single accessible object. A complete UI is represented as a tree of these.
@@ -925,25 +916,96 @@ pub struct Node {
925916
pub radio_group: Vec<NodeId>,
926917

927918
#[cfg_attr(feature = "serde", serde(default))]
928-
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))]
929-
pub markers: Box<[TextMarker]>,
919+
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
920+
pub is_spelling_error: bool,
921+
#[cfg_attr(feature = "serde", serde(default))]
922+
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
923+
pub is_grammar_error: bool,
924+
#[cfg_attr(feature = "serde", serde(default))]
925+
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
926+
pub is_search_match: bool,
927+
#[cfg_attr(feature = "serde", serde(default))]
928+
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
929+
pub is_suggestion: bool,
930930

931931
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
932932
pub text_direction: Option<TextDirection>,
933-
/// For inline text. This is the pixel position of the end of each
934-
/// character within the bounding rectangle of this object, in the
935-
/// direction given by [`Node::text_direction`]. For example, for left-to-right
936-
/// text, the first offset is the right coordinate of the first
937-
/// character within the object's bounds, the second offset
938-
/// is the right coordinate of the second character, and so on.
933+
934+
/// For inline text. The length (non-inclusive) of each character
935+
/// in UTF-8 code units (bytes). The sum of these lengths must equal
936+
/// the length of [`Node::value`], also in bytes.
937+
///
938+
/// A character is defined as the smallest unit of text that
939+
/// can be selected. This isn't necessarily a single Unicode
940+
/// scalar value (code point). This is why AccessKit can't compute
941+
/// the lengths of the characters from the text itself; this information
942+
/// must be provided by the text editing implementation.
943+
///
944+
/// If this node is the last text box in a line that ends with a hard
945+
/// line break, that line break should be included at the end of this
946+
/// node's value as either a CRLF or LF; in both cases, the line break
947+
/// should be counted as a single character for the sake of this slice.
948+
/// When the caret is at the end of such a line, the focus of the text
949+
/// selection should be on the line break, not after it.
939950
#[cfg_attr(feature = "serde", serde(default))]
940951
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))]
941-
pub character_offsets: Box<[f32]>,
952+
pub character_lengths: Box<[u8]>,
953+
/// For inline text. This is the position of each character within
954+
/// the node's bounding box, in the direction given by
955+
/// [`Node::text_direction`], in the coordinate space of this node.
956+
///
957+
/// When present, the length of this slice should be the same as the length
958+
/// of [`Node::character_lengths`], including for lines that end
959+
/// with a hard line break. The position of such a line break should
960+
/// be the position where an end-of-paragraph marker would be rendered.
961+
///
962+
/// This field is optional. Without it, AccessKit can't support some
963+
/// use cases, such as screen magnifiers that track the caret position
964+
/// or screen readers that display a highlight cursor. However,
965+
/// most text functionality still works without this information.
966+
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
967+
pub character_positions: Option<Box<[f32]>>,
968+
/// For inline text. This is the advance width of each character,
969+
/// in the direction given by [`Node::text_direction`], in the coordinate
970+
/// space of this node.
971+
///
972+
/// When present, the length of this slice should be the same as the length
973+
/// of [`Node::character_lengths`], including for lines that end
974+
/// with a hard line break. The width of such a line break should
975+
/// be non-zero if selecting the line break by itself results in
976+
/// a visible highlight (as in Microsoft Word), or zero if not
977+
/// (as in Windows Notepad).
978+
///
979+
/// This field is optional. Without it, AccessKit can't support some
980+
/// use cases, such as screen magnifiers that track the caret position
981+
/// or screen readers that display a highlight cursor. However,
982+
/// most text functionality still works without this information.
983+
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
984+
pub character_widths: Option<Box<[f32]>>,
942985

943-
/// For inline text. The indices of each word, in UTF-8 code units.
986+
/// For inline text. The length of each word in characters, as defined
987+
/// in [`Node::character_lengths`]. The sum of these lengths must equal
988+
/// the length of [`Node::character_lengths`].
989+
///
990+
/// The end of each word is the beginning of the next word; there are no
991+
/// characters that are not considered part of a word. Trailing whitespace
992+
/// is typically considered part of the word that precedes it, while
993+
/// a line's leading whitespace is considered its own word. Whether
994+
/// punctuation is considered a separate word or part of the preceding
995+
/// word depends on the particular text editing implementation.
996+
/// Some editors may have their own definition of a word; for example,
997+
/// in an IDE, words may correspond to programming language tokens.
998+
///
999+
/// Not all assistive technologies require information about word
1000+
/// boundaries, and not all platform accessibility APIs even expose
1001+
/// this information, but for assistive technologies that do use
1002+
/// this information, users will get unpredictable results if the word
1003+
/// boundaries exposed by the accessibility tree don't match
1004+
/// the editor's behavior. This is why AccessKit does not determine
1005+
/// word boundaries itself.
9441006
#[cfg_attr(feature = "serde", serde(default))]
9451007
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))]
946-
pub words: Box<[Range<usize>]>,
1008+
pub word_lengths: Box<[u8]>,
9471009

9481010
#[cfg_attr(feature = "serde", serde(default))]
9491011
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))]

consumer/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ pub use node::Node;
1212
pub(crate) mod iterators;
1313
pub use iterators::FilterResult;
1414

15+
pub(crate) mod text;
16+
pub use text::{
17+
AttributeValue as TextAttributeValue, Position as TextPosition, Range as TextRange,
18+
WeakRange as WeakTextRange,
19+
};
20+
1521
#[cfg(test)]
1622
mod tests {
1723
use accesskit::kurbo::{Affine, Rect, Vec2};

consumer/src/node.rs

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,19 @@ impl<'a> Node<'a> {
226226
* self.direct_transform()
227227
}
228228

229+
pub(crate) fn relative_transform(&self, stop_at: &Node) -> Affine {
230+
let parent_transform = if let Some(parent) = self.parent() {
231+
if parent.id() == stop_at.id() {
232+
Affine::IDENTITY
233+
} else {
234+
parent.relative_transform(stop_at)
235+
}
236+
} else {
237+
Affine::IDENTITY
238+
};
239+
parent_transform * self.direct_transform()
240+
}
241+
229242
/// Returns the node's transformed bounding box relative to the tree's
230243
/// container (e.g. window).
231244
pub fn bounding_box(&self) -> Option<Rect> {
@@ -235,13 +248,18 @@ impl<'a> Node<'a> {
235248
.map(|rect| self.transform().transform_rect_bbox(*rect))
236249
}
237250

238-
/// Returns the deepest filtered node, either this node or a descendant,
239-
/// at the given point in this node's coordinate space.
240-
pub fn node_at_point(
251+
pub(crate) fn bounding_box_in_coordinate_space(&self, other: &Node) -> Option<Rect> {
252+
self.data()
253+
.bounds
254+
.as_ref()
255+
.map(|rect| self.relative_transform(other).transform_rect_bbox(*rect))
256+
}
257+
258+
pub(crate) fn hit_test(
241259
&self,
242260
point: Point,
243261
filter: &impl Fn(&Node) -> FilterResult,
244-
) -> Option<Node<'a>> {
262+
) -> Option<(Node<'a>, Point)> {
245263
let filter_result = filter(self);
246264

247265
if filter_result == FilterResult::ExcludeSubtree {
@@ -250,22 +268,32 @@ impl<'a> Node<'a> {
250268

251269
for child in self.children().rev() {
252270
let point = child.direct_transform().inverse() * point;
253-
if let Some(node) = child.node_at_point(point, filter) {
254-
return Some(node);
271+
if let Some(result) = child.hit_test(point, filter) {
272+
return Some(result);
255273
}
256274
}
257275

258276
if filter_result == FilterResult::Include {
259277
if let Some(rect) = &self.data().bounds {
260278
if rect.contains(point) {
261-
return Some(*self);
279+
return Some((*self, point));
262280
}
263281
}
264282
}
265283

266284
None
267285
}
268286

287+
/// Returns the deepest filtered node, either this node or a descendant,
288+
/// at the given point in this node's coordinate space.
289+
pub fn node_at_point(
290+
&self,
291+
point: Point,
292+
filter: &impl Fn(&Node) -> FilterResult,
293+
) -> Option<Node<'a>> {
294+
self.hit_test(point, filter).map(|(node, _)| node)
295+
}
296+
269297
pub fn id(&self) -> NodeId {
270298
self.state.id
271299
}
@@ -480,6 +508,22 @@ impl<'a> Node<'a> {
480508
self.data().selected
481509
}
482510

511+
pub fn index_path(&self) -> Vec<usize> {
512+
self.relative_index_path(self.tree_state.root_id())
513+
}
514+
515+
pub fn relative_index_path(&self, ancestor_id: NodeId) -> Vec<usize> {
516+
let mut result = Vec::new();
517+
let mut current = *self;
518+
while current.id() != ancestor_id {
519+
let (parent, index) = current.parent_and_index().unwrap();
520+
result.push(index);
521+
current = parent;
522+
}
523+
result.reverse();
524+
result
525+
}
526+
483527
pub(crate) fn first_filtered_child(
484528
&self,
485529
filter: &impl Fn(&Node) -> FilterResult,

0 commit comments

Comments
 (0)