diff --git a/Cargo.lock b/Cargo.lock index 040acd4b8..d019cb24f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ab_glyph" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5568a4aa5ba8adf5175c5c460b030e27d8893412976cc37bef0e4fbc16cfbba" +checksum = "fe21446ad43aa56417a767f3e2f3d7c4ca522904de1dd640529a76e9c5c3b33c" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -22,8 +22,7 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" name = "accesskit" version = "0.8.1" dependencies = [ - "enumset", - "kurbo", + "enumn", "schemars", "serde", ] @@ -450,41 +449,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" -[[package]] -name = "darling" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc" -dependencies = [ - "darling_core", - "quote", - "syn", -] - [[package]] name = "derivative" version = "2.2.0" @@ -575,22 +539,11 @@ dependencies = [ ] [[package]] -name = "enumset" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6216d2c19a6fb5f29d1ada1dc7bc4367a8cbf0fa4af5cf12e07b5bbdde6b5b2c" -dependencies = [ - "enumset_derive", - "serde", -] - -[[package]] -name = "enumset_derive" -version = "0.5.5" +name = "enumn" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6451128aa6655d880755345d085494cf7561a6bee7c8dc821e5d77e6d267ecd4" +checksum = "e88bcb3a067a6555d577aba299e75eff9942da276e6506fc6274327daa026132" dependencies = [ - "darling", "proc-macro2", "quote", "syn", @@ -621,12 +574,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foreign-types" version = "0.3.2" @@ -722,12 +669,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "instant" version = "0.1.11" @@ -770,17 +711,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kurbo" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449" -dependencies = [ - "arrayvec", - "schemars", - "serde", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -1262,7 +1192,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271ac0c667b8229adf70f0f957697c96fafd7486ab7481e15dc5e45e3e6a4368" dependencies = [ "dyn-clone", - "enumset", "schemars_derive", "serde", "serde_json", @@ -1446,12 +1375,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9df65f20698aeed245efdde3628a6b559ea1239bbb871af1b6e3b58c413b2bd1" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "syn" version = "1.0.107" diff --git a/common/Cargo.toml b/common/Cargo.toml index c8eaddd30..76540de6d 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -14,11 +14,10 @@ edition = "2021" features = ["schemars", "serde"] [dependencies] -enumset = "1.0.8" -kurbo = "0.8.3" -schemars_lib = { package = "schemars", version = "0.8.7", features = ["enumset"], optional = true } -serde_lib = { package = "serde", version = "1.0", features = ["derive", "rc"], optional = true } +enumn = { version = "0.1.6", optional = true } +schemars = { version = "0.8.7", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } [features] -schemars = ["serde", "schemars_lib", "kurbo/schemars"] -serde = ["serde_lib", "enumset/serde", "kurbo/serde"] +serde = ["dep:serde", "enumn"] +schemars = ["dep:schemars", "serde"] diff --git a/common/src/geometry.rs b/common/src/geometry.rs new file mode 100644 index 000000000..07a09b15a --- /dev/null +++ b/common/src/geometry.rs @@ -0,0 +1,875 @@ +// Copyright 2023 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. + +// Derived from kurbo. +// Copyright 2018 The kurbo Authors. +// 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 std::{ + fmt, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, +}; + +/// A 2D affine transform. Derived from [kurbo](https://github.com/linebender/kurbo). +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Affine([f64; 6]); + +impl Affine { + /// The identity transform. + pub const IDENTITY: Affine = Affine::scale(1.0); + + /// A transform that is flipped on the y-axis. Useful for converting between + /// y-up and y-down spaces. + pub const FLIP_Y: Affine = Affine::new([1.0, 0., 0., -1.0, 0., 0.]); + + /// A transform that is flipped on the x-axis. + pub const FLIP_X: Affine = Affine::new([-1.0, 0., 0., 1.0, 0., 0.]); + + /// Construct an affine transform from coefficients. + /// + /// If the coefficients are `(a, b, c, d, e, f)`, then the resulting + /// transformation represents this augmented matrix: + /// + /// ```text + /// | a c e | + /// | b d f | + /// | 0 0 1 | + /// ``` + /// + /// Note that this convention is transposed from PostScript and + /// Direct2D, but is consistent with the + /// [Wikipedia](https://en.wikipedia.org/wiki/Affine_transformation) + /// formulation of affine transformation as augmented matrix. The + /// idea is that `(A * B) * v == A * (B * v)`, where `*` is the + /// [`Mul`](std::ops::Mul) trait. + #[inline] + pub const fn new(c: [f64; 6]) -> Affine { + Affine(c) + } + + /// An affine transform representing uniform scaling. + #[inline] + pub const fn scale(s: f64) -> Affine { + Affine([s, 0.0, 0.0, s, 0.0, 0.0]) + } + + /// An affine transform representing non-uniform scaling + /// with different scale values for x and y + #[inline] + pub const fn scale_non_uniform(s_x: f64, s_y: f64) -> Affine { + Affine([s_x, 0.0, 0.0, s_y, 0.0, 0.0]) + } + + /// An affine transform representing rotation. + /// + /// The convention for rotation is that a positive angle rotates a + /// positive X direction into positive Y. Thus, in a Y-down coordinate + /// system (as is common for graphics), it is a clockwise rotation, and + /// in Y-up (traditional for math), it is anti-clockwise. + /// + /// The angle, `th`, is expressed in radians. + #[inline] + pub fn rotate(th: f64) -> Affine { + let (s, c) = th.sin_cos(); + Affine([c, s, -s, c, 0.0, 0.0]) + } + + /// An affine transform representing translation. + #[inline] + pub fn translate>(p: V) -> Affine { + let p = p.into(); + Affine([1.0, 0.0, 0.0, 1.0, p.x, p.y]) + } + + /// Creates an affine transformation that takes the unit square to the given rectangle. + /// + /// Useful when you want to draw into the unit square but have your output fill any rectangle. + /// In this case push the `Affine` onto the transform stack. + pub fn map_unit_square(rect: Rect) -> Affine { + Affine([rect.width(), 0., 0., rect.height(), rect.x0, rect.y0]) + } + + /// Get the coefficients of the transform. + #[inline] + pub fn as_coeffs(self) -> [f64; 6] { + self.0 + } + + /// Compute the determinant of this transform. + pub fn determinant(self) -> f64 { + self.0[0] * self.0[3] - self.0[1] * self.0[2] + } + + /// Compute the inverse transform. + /// + /// Produces NaN values when the determinant is zero. + pub fn inverse(self) -> Affine { + let inv_det = self.determinant().recip(); + Affine([ + inv_det * self.0[3], + -inv_det * self.0[1], + -inv_det * self.0[2], + inv_det * self.0[0], + inv_det * (self.0[2] * self.0[5] - self.0[3] * self.0[4]), + inv_det * (self.0[1] * self.0[4] - self.0[0] * self.0[5]), + ]) + } + + /// Compute the bounding box of a transformed rectangle. + /// + /// Returns the minimal `Rect` that encloses the given `Rect` after affine transformation. + /// If the transform is axis-aligned, then this bounding box is "tight", in other words the + /// returned `Rect` is the transformed rectangle. + /// + /// The returned rectangle always has non-negative width and height. + pub fn transform_rect_bbox(self, rect: Rect) -> Rect { + let p00 = self * Point::new(rect.x0, rect.y0); + let p01 = self * Point::new(rect.x0, rect.y1); + let p10 = self * Point::new(rect.x1, rect.y0); + let p11 = self * Point::new(rect.x1, rect.y1); + Rect::from_points(p00, p01).union(Rect::from_points(p10, p11)) + } + + /// Is this map finite? + #[inline] + pub fn is_finite(&self) -> bool { + self.0[0].is_finite() + && self.0[1].is_finite() + && self.0[2].is_finite() + && self.0[3].is_finite() + && self.0[4].is_finite() + && self.0[5].is_finite() + } + + /// Is this map NaN? + #[inline] + pub fn is_nan(&self) -> bool { + self.0[0].is_nan() + || self.0[1].is_nan() + || self.0[2].is_nan() + || self.0[3].is_nan() + || self.0[4].is_nan() + || self.0[5].is_nan() + } +} + +impl Default for Affine { + #[inline] + fn default() -> Affine { + Affine::IDENTITY + } +} + +impl Mul for Affine { + type Output = Point; + + #[inline] + fn mul(self, other: Point) -> Point { + Point::new( + self.0[0] * other.x + self.0[2] * other.y + self.0[4], + self.0[1] * other.x + self.0[3] * other.y + self.0[5], + ) + } +} + +impl Mul for Affine { + type Output = Affine; + + #[inline] + fn mul(self, other: Affine) -> Affine { + Affine([ + self.0[0] * other.0[0] + self.0[2] * other.0[1], + self.0[1] * other.0[0] + self.0[3] * other.0[1], + self.0[0] * other.0[2] + self.0[2] * other.0[3], + self.0[1] * other.0[2] + self.0[3] * other.0[3], + self.0[0] * other.0[4] + self.0[2] * other.0[5] + self.0[4], + self.0[1] * other.0[4] + self.0[3] * other.0[5] + self.0[5], + ]) + } +} + +impl MulAssign for Affine { + #[inline] + fn mul_assign(&mut self, other: Affine) { + *self = self.mul(other); + } +} + +impl Mul for f64 { + type Output = Affine; + + #[inline] + fn mul(self, other: Affine) -> Affine { + Affine([ + self * other.0[0], + self * other.0[1], + self * other.0[2], + self * other.0[3], + self * other.0[4], + self * other.0[5], + ]) + } +} + +/// A 2D point. Derived from [kurbo](https://github.com/linebender/kurbo). +#[derive(Clone, Copy, Default, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Point { + /// The x coordinate. + pub x: f64, + /// The y coordinate. + pub y: f64, +} + +impl Point { + /// The point (0, 0). + pub const ZERO: Point = Point::new(0., 0.); + + /// The point at the origin; (0, 0). + pub const ORIGIN: Point = Point::new(0., 0.); + + /// Create a new `Point` with the provided `x` and `y` coordinates. + #[inline] + pub const fn new(x: f64, y: f64) -> Self { + Point { x, y } + } + + /// Convert this point into a `Vec2`. + #[inline] + pub const fn to_vec2(self) -> Vec2 { + Vec2::new(self.x, self.y) + } +} + +impl From<(f64, f64)> for Point { + #[inline] + fn from(v: (f64, f64)) -> Point { + Point { x: v.0, y: v.1 } + } +} + +impl From for (f64, f64) { + #[inline] + fn from(v: Point) -> (f64, f64) { + (v.x, v.y) + } +} + +impl Add for Point { + type Output = Point; + + #[inline] + fn add(self, other: Vec2) -> Self { + Point::new(self.x + other.x, self.y + other.y) + } +} + +impl AddAssign for Point { + #[inline] + fn add_assign(&mut self, other: Vec2) { + *self = Point::new(self.x + other.x, self.y + other.y) + } +} + +impl Sub for Point { + type Output = Point; + + #[inline] + fn sub(self, other: Vec2) -> Self { + Point::new(self.x - other.x, self.y - other.y) + } +} + +impl SubAssign for Point { + #[inline] + fn sub_assign(&mut self, other: Vec2) { + *self = Point::new(self.x - other.x, self.y - other.y) + } +} + +impl Add<(f64, f64)> for Point { + type Output = Point; + + #[inline] + fn add(self, (x, y): (f64, f64)) -> Self { + Point::new(self.x + x, self.y + y) + } +} + +impl AddAssign<(f64, f64)> for Point { + #[inline] + fn add_assign(&mut self, (x, y): (f64, f64)) { + *self = Point::new(self.x + x, self.y + y) + } +} + +impl Sub<(f64, f64)> for Point { + type Output = Point; + + #[inline] + fn sub(self, (x, y): (f64, f64)) -> Self { + Point::new(self.x - x, self.y - y) + } +} + +impl SubAssign<(f64, f64)> for Point { + #[inline] + fn sub_assign(&mut self, (x, y): (f64, f64)) { + *self = Point::new(self.x - x, self.y - y) + } +} + +impl Sub for Point { + type Output = Vec2; + + #[inline] + fn sub(self, other: Point) -> Vec2 { + Vec2::new(self.x - other.x, self.y - other.y) + } +} + +impl fmt::Debug for Point { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({:?}, {:?})", self.x, self.y) + } +} + +/// A rectangle. Derived from [kurbo](https://github.com/linebender/kurbo). +#[derive(Clone, Copy, Default, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Rect { + /// The minimum x coordinate (left edge). + pub x0: f64, + /// The minimum y coordinate (top edge in y-down spaces). + pub y0: f64, + /// The maximum x coordinate (right edge). + pub x1: f64, + /// The maximum y coordinate (bottom edge in y-down spaces). + pub y1: f64, +} + +impl From<(Point, Point)> for Rect { + fn from(points: (Point, Point)) -> Rect { + Rect::from_points(points.0, points.1) + } +} + +impl From<(Point, Size)> for Rect { + fn from(params: (Point, Size)) -> Rect { + Rect::from_origin_size(params.0, params.1) + } +} + +impl Add for Rect { + type Output = Rect; + + #[inline] + fn add(self, v: Vec2) -> Rect { + Rect::new(self.x0 + v.x, self.y0 + v.y, self.x1 + v.x, self.y1 + v.y) + } +} + +impl Sub for Rect { + type Output = Rect; + + #[inline] + fn sub(self, v: Vec2) -> Rect { + Rect::new(self.x0 - v.x, self.y0 - v.y, self.x1 - v.x, self.y1 - v.y) + } +} + +impl fmt::Debug for Rect { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if f.alternate() { + write!( + f, + "Rect {{ origin: {:?}, size: {:?} }}", + self.origin(), + self.size() + ) + } else { + write!( + f, + "Rect {{ x0: {:?}, y0: {:?}, x1: {:?}, y1: {:?} }}", + self.x0, self.y0, self.x1, self.y1 + ) + } + } +} + +impl Rect { + /// The empty rectangle at the origin. + pub const ZERO: Rect = Rect::new(0., 0., 0., 0.); + + /// A new rectangle from minimum and maximum coordinates. + #[inline] + pub const fn new(x0: f64, y0: f64, x1: f64, y1: f64) -> Rect { + Rect { x0, y0, x1, y1 } + } + + /// A new rectangle from two points. + /// + /// The result will have non-negative width and height. + #[inline] + pub fn from_points(p0: impl Into, p1: impl Into) -> Rect { + let p0 = p0.into(); + let p1 = p1.into(); + Rect::new(p0.x, p0.y, p1.x, p1.y).abs() + } + + /// A new rectangle from origin and size. + /// + /// The result will have non-negative width and height. + #[inline] + pub fn from_origin_size(origin: impl Into, size: impl Into) -> Rect { + let origin = origin.into(); + Rect::from_points(origin, origin + size.into().to_vec2()) + } + + /// Create a new `Rect` with the same size as `self` and a new origin. + #[inline] + pub fn with_origin(self, origin: impl Into) -> Rect { + Rect::from_origin_size(origin, self.size()) + } + + /// Create a new `Rect` with the same origin as `self` and a new size. + #[inline] + pub fn with_size(self, size: impl Into) -> Rect { + Rect::from_origin_size(self.origin(), size) + } + + /// The width of the rectangle. + /// + /// Note: nothing forbids negative width. + #[inline] + pub fn width(&self) -> f64 { + self.x1 - self.x0 + } + + /// The height of the rectangle. + /// + /// Note: nothing forbids negative height. + #[inline] + pub fn height(&self) -> f64 { + self.y1 - self.y0 + } + + /// Returns the minimum value for the x-coordinate of the rectangle. + #[inline] + pub fn min_x(&self) -> f64 { + self.x0.min(self.x1) + } + + /// Returns the maximum value for the x-coordinate of the rectangle. + #[inline] + pub fn max_x(&self) -> f64 { + self.x0.max(self.x1) + } + + /// Returns the minimum value for the y-coordinate of the rectangle. + #[inline] + pub fn min_y(&self) -> f64 { + self.y0.min(self.y1) + } + + /// Returns the maximum value for the y-coordinate of the rectangle. + #[inline] + pub fn max_y(&self) -> f64 { + self.y0.max(self.y1) + } + + /// The origin of the rectangle. + /// + /// This is the top left corner in a y-down space and with + /// non-negative width and height. + #[inline] + pub fn origin(&self) -> Point { + Point::new(self.x0, self.y0) + } + + /// The size of the rectangle. + #[inline] + pub fn size(&self) -> Size { + Size::new(self.width(), self.height()) + } + + /// Take absolute value of width and height. + /// + /// The resulting rect has the same extents as the original, but is + /// guaranteed to have non-negative width and height. + #[inline] + pub fn abs(&self) -> Rect { + let Rect { x0, y0, x1, y1 } = *self; + Rect::new(x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)) + } + + /// The area of the rectangle. + #[inline] + pub fn area(&self) -> f64 { + self.width() * self.height() + } + + /// Whether this rectangle has zero area. + /// + /// Note: a rectangle with negative area is not considered empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.area() == 0.0 + } + + /// Returns `true` if `point` lies within `self`. + #[inline] + pub fn contains(&self, point: Point) -> bool { + point.x >= self.x0 && point.x < self.x1 && point.y >= self.y0 && point.y < self.y1 + } + + /// The smallest rectangle enclosing two rectangles. + /// + /// Results are valid only if width and height are non-negative. + #[inline] + pub fn union(&self, other: Rect) -> Rect { + Rect::new( + self.x0.min(other.x0), + self.y0.min(other.y0), + self.x1.max(other.x1), + self.y1.max(other.y1), + ) + } + + /// Compute the union with one point. + /// + /// This method includes the perimeter of zero-area rectangles. + /// Thus, a succession of `union_pt` operations on a series of + /// points yields their enclosing rectangle. + /// + /// Results are valid only if width and height are non-negative. + pub fn union_pt(&self, pt: Point) -> Rect { + Rect::new( + self.x0.min(pt.x), + self.y0.min(pt.y), + self.x1.max(pt.x), + self.y1.max(pt.y), + ) + } + + /// The intersection of two rectangles. + /// + /// The result is zero-area if either input has negative width or + /// height. The result always has non-negative width and height. + #[inline] + pub fn intersect(&self, other: Rect) -> Rect { + let x0 = self.x0.max(other.x0); + let y0 = self.y0.max(other.y0); + let x1 = self.x1.min(other.x1); + let y1 = self.y1.min(other.y1); + Rect::new(x0, y0, x1.max(x0), y1.max(y0)) + } +} + +/// A 2D size. Derived from [kurbo](https://github.com/linebender/kurbo). +#[derive(Clone, Copy, Default, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Size { + /// The width. + pub width: f64, + /// The height. + pub height: f64, +} + +impl Size { + /// A size with zero width or height. + pub const ZERO: Size = Size::new(0., 0.); + + /// Create a new `Size` with the provided `width` and `height`. + #[inline] + pub const fn new(width: f64, height: f64) -> Self { + Size { width, height } + } + + /// Convert this size into a [`Vec2`], with `width` mapped to `x` and `height` + /// mapped to `y`. + #[inline] + pub const fn to_vec2(self) -> Vec2 { + Vec2::new(self.width, self.height) + } +} + +impl fmt::Debug for Size { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}W×{:?}H", self.width, self.height) + } +} + +impl MulAssign for Size { + #[inline] + fn mul_assign(&mut self, other: f64) { + *self = Size { + width: self.width * other, + height: self.height * other, + }; + } +} + +impl Mul for f64 { + type Output = Size; + + #[inline] + fn mul(self, other: Size) -> Size { + other * self + } +} + +impl Mul for Size { + type Output = Size; + + #[inline] + fn mul(self, other: f64) -> Size { + Size { + width: self.width * other, + height: self.height * other, + } + } +} + +impl DivAssign for Size { + #[inline] + fn div_assign(&mut self, other: f64) { + *self = Size { + width: self.width / other, + height: self.height / other, + }; + } +} + +impl Div for Size { + type Output = Size; + + #[inline] + fn div(self, other: f64) -> Size { + Size { + width: self.width / other, + height: self.height / other, + } + } +} + +impl Add for Size { + type Output = Size; + #[inline] + fn add(self, other: Size) -> Size { + Size { + width: self.width + other.width, + height: self.height + other.height, + } + } +} + +impl AddAssign for Size { + #[inline] + fn add_assign(&mut self, other: Size) { + *self = *self + other; + } +} + +impl Sub for Size { + type Output = Size; + #[inline] + fn sub(self, other: Size) -> Size { + Size { + width: self.width - other.width, + height: self.height - other.height, + } + } +} + +impl SubAssign for Size { + #[inline] + fn sub_assign(&mut self, other: Size) { + *self = *self - other; + } +} + +impl From<(f64, f64)> for Size { + #[inline] + fn from(v: (f64, f64)) -> Size { + Size { + width: v.0, + height: v.1, + } + } +} + +impl From for (f64, f64) { + #[inline] + fn from(v: Size) -> (f64, f64) { + (v.width, v.height) + } +} + +/// A 2D vector. Derived from [kurbo](https://github.com/linebender/kurbo). +/// +/// This is intended primarily for a vector in the mathematical sense, +/// but it can be interpreted as a translation, and converted to and +/// from a point (vector relative to the origin) and size. +#[derive(Clone, Copy, Default, Debug, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Vec2 { + /// The x-coordinate. + pub x: f64, + /// The y-coordinate. + pub y: f64, +} + +impl Vec2 { + /// The vector (0, 0). + pub const ZERO: Vec2 = Vec2::new(0., 0.); + + /// Create a new vector. + #[inline] + pub const fn new(x: f64, y: f64) -> Vec2 { + Vec2 { x, y } + } + + /// Convert this vector into a `Point`. + #[inline] + pub const fn to_point(self) -> Point { + Point::new(self.x, self.y) + } + + /// Convert this vector into a `Size`. + #[inline] + pub const fn to_size(self) -> Size { + Size::new(self.x, self.y) + } +} + +impl From<(f64, f64)> for Vec2 { + #[inline] + fn from(v: (f64, f64)) -> Vec2 { + Vec2 { x: v.0, y: v.1 } + } +} + +impl From for (f64, f64) { + #[inline] + fn from(v: Vec2) -> (f64, f64) { + (v.x, v.y) + } +} + +impl Add for Vec2 { + type Output = Vec2; + + #[inline] + fn add(self, other: Vec2) -> Vec2 { + Vec2 { + x: self.x + other.x, + y: self.y + other.y, + } + } +} + +impl AddAssign for Vec2 { + #[inline] + fn add_assign(&mut self, other: Vec2) { + *self = Vec2 { + x: self.x + other.x, + y: self.y + other.y, + } + } +} + +impl Sub for Vec2 { + type Output = Vec2; + + #[inline] + fn sub(self, other: Vec2) -> Vec2 { + Vec2 { + x: self.x - other.x, + y: self.y - other.y, + } + } +} + +impl SubAssign for Vec2 { + #[inline] + fn sub_assign(&mut self, other: Vec2) { + *self = Vec2 { + x: self.x - other.x, + y: self.y - other.y, + } + } +} + +impl Mul for Vec2 { + type Output = Vec2; + + #[inline] + fn mul(self, other: f64) -> Vec2 { + Vec2 { + x: self.x * other, + y: self.y * other, + } + } +} + +impl MulAssign for Vec2 { + #[inline] + fn mul_assign(&mut self, other: f64) { + *self = Vec2 { + x: self.x * other, + y: self.y * other, + }; + } +} + +impl Mul for f64 { + type Output = Vec2; + + #[inline] + fn mul(self, other: Vec2) -> Vec2 { + other * self + } +} + +impl Div for Vec2 { + type Output = Vec2; + + /// Note: division by a scalar is implemented by multiplying by the reciprocal. + /// + /// This is more efficient but has different roundoff behavior than division. + #[inline] + #[allow(clippy::suspicious_arithmetic_impl)] + fn div(self, other: f64) -> Vec2 { + self * other.recip() + } +} + +impl DivAssign for Vec2 { + #[inline] + fn div_assign(&mut self, other: f64) { + self.mul_assign(other.recip()); + } +} + +impl Neg for Vec2 { + type Output = Vec2; + + #[inline] + fn neg(self) -> Vec2 { + Vec2 { + x: -self.x, + y: -self.y, + } + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 1a9da492b..a9b92eabb 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -8,21 +8,29 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE.chromium file. -use enumset::{EnumSet, EnumSetType}; -pub use kurbo; -use kurbo::{Affine, Point, Rect}; #[cfg(feature = "schemars")] -use schemars_lib as schemars; -#[cfg(feature = "schemars")] -use schemars_lib::JsonSchema; -#[cfg(feature = "serde")] -use serde_lib as serde; +use schemars::{ + gen::SchemaGenerator, + schema::{ArrayValidation, InstanceType, ObjectValidation, Schema, SchemaObject}, + JsonSchema, Map as SchemaMap, +}; #[cfg(feature = "serde")] -use serde_lib::{Deserialize, Serialize}; +use serde::{ + de::{Deserializer, IgnoredAny, MapAccess, SeqAccess, Visitor}, + ser::{SerializeMap, SerializeSeq, Serializer}, + Deserialize, Serialize, +}; use std::{ + collections::BTreeSet, num::{NonZeroU128, NonZeroU64}, + ops::DerefMut, sync::Arc, }; +#[cfg(feature = "serde")] +use std::{fmt, mem::size_of_val}; + +mod geometry; +pub use geometry::{Affine, Point, Rect, Size, Vec2}; /// The type of an accessibility node. /// @@ -34,10 +42,9 @@ use std::{ /// is ordered roughly by expected usage frequency (with the notable exception /// of [`Role::Unknown`]). This is more efficient in serialization formats /// where integers use a variable-length encoding. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, 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 Role { Unknown, @@ -245,6 +252,7 @@ pub enum Role { } impl Default for Role { + #[inline] fn default() -> Self { Self::Unknown } @@ -254,12 +262,11 @@ impl Default for Role { /// /// In contrast to [`DefaultActionVerb`], these describe what happens to the /// object, e.g. "focus". -#[derive(EnumSetType, Debug)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize, enumn::N))] #[cfg_attr(feature = "schemars", derive(JsonSchema))] -#[cfg_attr(feature = "serde", serde(crate = "serde"))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -#[cfg_attr(feature = "serde", enumset(serialize_as_list))] +#[repr(u8)] pub enum Action { /// Do the default action for an object, typically this means "click". Default, @@ -334,10 +341,100 @@ pub enum Action { ShowContextMenu, } +impl Action { + fn mask(self) -> u32 { + 1 << (self as u8) + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +struct Actions(u32); + +#[cfg(feature = "serde")] +impl Serialize for Actions { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(None)?; + for i in 0..((size_of_val(&self.0) as u8) * 8) { + if let Some(action) = Action::n(i) { + if (self.0 & action.mask()) != 0 { + seq.serialize_element(&action)?; + } + } + } + seq.end() + } +} + +#[cfg(feature = "serde")] +struct ActionsVisitor; + +#[cfg(feature = "serde")] +impl<'de> Visitor<'de> for ActionsVisitor { + type Value = Actions; + + #[inline] + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("action set") + } + + fn visit_seq(self, mut seq: V) -> Result + where + V: SeqAccess<'de>, + { + let mut actions = Actions::default(); + while let Some(action) = seq.next_element::()? { + actions.0 |= action.mask(); + } + Ok(actions) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Actions { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_seq(ActionsVisitor) + } +} + +#[cfg(feature = "schemars")] +impl JsonSchema for Actions { + #[inline] + fn schema_name() -> String { + "Actions".into() + } + + #[inline] + fn is_referenceable() -> bool { + false + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + SchemaObject { + instance_type: Some(InstanceType::Array.into()), + array: Some( + ArrayValidation { + unique_items: Some(true), + items: Some(gen.subschema_for::().into()), + ..Default::default() + } + .into(), + ), + ..Default::default() + } + .into() + } +} + #[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 Orientation { /// E.g. most toolbars and separators. @@ -349,7 +446,6 @@ pub enum Orientation { #[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 NameFrom { /// E.g. [`aria-label`]. @@ -373,7 +469,6 @@ pub enum NameFrom { #[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 DescriptionFrom { AriaDescription, @@ -388,30 +483,9 @@ pub enum DescriptionFrom { Title, } -/// Function that can be performed when a dragged object is released -/// on a drop target. -/// -/// Note: [`aria-dropeffect`] is deprecated in WAI-ARIA 1.1. -/// -/// [`aria-dropeffect`]: https://www.w3.org/TR/wai-aria-1.1/#aria-dropeffect -#[derive(EnumSetType, Debug)] -#[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"))] -#[cfg_attr(feature = "serde", enumset(serialize_as_list))] -pub enum DropEffect { - Copy, - Execute, - Link, - Move, - 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 TextDirection { LeftToRight, @@ -424,21 +498,19 @@ pub enum TextDirection { /// [`aria-invalid`] attribute. /// /// [`aria-invalid`]: https://www.w3.org/TR/wai-aria-1.1/#aria-invalid -#[derive(Clone, Debug, PartialEq, Eq)] +#[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(rename_all = "camelCase"))] -pub enum InvalidState { - False, +pub enum Invalid { True, - Other(Box), + Grammar, + Spelling, } #[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 CheckedState { False, @@ -455,7 +527,6 @@ pub enum CheckedState { #[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 DefaultActionVerb { Click, @@ -476,7 +547,6 @@ pub enum DefaultActionVerb { #[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 SortDirection { Unsorted, @@ -488,7 +558,6 @@ pub enum SortDirection { #[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 AriaCurrent { False, @@ -503,7 +572,6 @@ pub enum AriaCurrent { #[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 Live { Off, @@ -514,7 +582,6 @@ pub enum Live { #[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 HasPopup { True, @@ -528,7 +595,6 @@ pub enum HasPopup { #[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 ListStyle { Circle, @@ -543,7 +609,6 @@ pub enum ListStyle { #[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 TextAlign { Left, @@ -555,7 +620,6 @@ pub enum TextAlign { #[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 VerticalOffset { Subscript, @@ -565,7 +629,6 @@ pub enum VerticalOffset { #[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 TextDecoration { Solid, @@ -583,10 +646,10 @@ pub type NodeIdContent = NonZeroU128; #[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"))] pub struct NodeId(pub NodeIdContent); impl From for NodeId { + #[inline] fn from(inner: NonZeroU64) -> Self { Self(inner.into()) } @@ -599,7 +662,6 @@ impl From for NodeId { #[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 CustomAction { @@ -607,22 +669,9 @@ pub struct CustomAction { pub description: Box, } -// Helper for skipping false values in serialization. -#[cfg(feature = "serde")] -fn is_false(b: &bool) -> bool { - !b -} - -// Helper for skipping empty slices in serialization. -#[cfg(feature = "serde")] -fn is_empty(slice: &[T]) -> bool { - slice.is_empty() -} - #[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 { @@ -636,7 +685,6 @@ pub struct TextPosition { #[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 { @@ -651,283 +699,932 @@ pub struct TextSelection { pub focus: TextPosition, } -/// A single accessible object. A complete UI is represented as a tree of these. -#[derive(Clone, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize, enumn::N))] #[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"))] +#[repr(u8)] +enum Flag { + AutofillAvailable, + Default, + Editable, + Hovered, + Hidden, + Linked, + Multiline, + Multiselectable, + Protected, + Required, + Visited, + Busy, + LiveAtomic, + Modal, + Scrollable, + SelectedFromFocus, + TouchPassThrough, + ReadOnly, + Disabled, + Bold, + Italic, + CanvasHasFallback, + ClipsChildren, + IsLineBreakingObject, + IsPageBreakingObject, + IsSpellingError, + IsGrammarError, + IsSearchMatch, + IsSuggestion, + IsNonatomicTextFieldRoot, +} + +impl Flag { + fn mask(self) -> u32 { + 1 << (self as u8) + } +} + +// The following is based on the technique described here: +// https://viruta.org/reducing-memory-consumption-in-librsvg-2.html + +#[derive(Clone, Debug, PartialEq)] +enum PropertyValue { + None, + NodeIdVec(Vec), + NodeId(NodeId), + String(Box), + F64(f64), + Usize(usize), + Color(u32), + TextDecoration(TextDecoration), + LengthSlice(Box<[u8]>), + CoordSlice(Box<[f32]>), + Bool(bool), + NameFrom(NameFrom), + DescriptionFrom(DescriptionFrom), + Invalid(Invalid), + CheckedState(CheckedState), + Live(Live), + DefaultActionVerb(DefaultActionVerb), + TextDirection(TextDirection), + Orientation(Orientation), + SortDirection(SortDirection), + AriaCurrent(AriaCurrent), + HasPopup(HasPopup), + ListStyle(ListStyle), + TextAlign(TextAlign), + VerticalOffset(VerticalOffset), + Affine(Box), + Rect(Rect), + TextSelection(Box), + CustomActionVec(Vec), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize, enumn::N))] +#[cfg_attr(feature = "schemars", derive(JsonSchema))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[repr(u8)] +enum PropertyId { + // NodeIdVec + Children, + IndirectChildren, + Controls, + Details, + DescribedBy, + FlowTo, + LabelledBy, + RadioGroup, + + // NodeId + ActiveDescendant, + ErrorMessage, + InPageLinkTarget, + MemberOf, + NextOnLine, + PreviousOnLine, + PopupFor, + TableHeader, + TableRowHeader, + TableColumnHeader, + NextFocus, + PreviousFocus, + + // String + Name, + Description, + Value, + AccessKey, + AutoComplete, + CheckedStateDescription, + ClassName, + CssDisplay, + FontFamily, + HtmlTag, + InnerHtml, + InputType, + KeyShortcuts, + Language, + LiveRelevant, + Placeholder, + AriaRole, + RoleDescription, + Tooltip, + Url, + + // f64 + ScrollX, + ScrollXMin, + ScrollXMax, + ScrollY, + ScrollYMin, + ScrollYMax, + NumericValue, + MinNumericValue, + MaxNumericValue, + NumericValueStep, + NumericValueJump, + FontSize, + FontWeight, + TextIndent, + + // usize + TableRowCount, + TableColumnCount, + TableRowIndex, + TableColumnIndex, + TableCellColumnIndex, + TableCellColumnSpan, + TableCellRowIndex, + TableCellRowSpan, + HierarchicalLevel, + SizeOfSet, + PositionInSet, + + // Color + ColorValue, + BackgroundColor, + ForegroundColor, + + // TextDecoration + Overline, + Strikethrough, + Underline, + + // LengthSlice + CharacterLengths, + WordLengths, + + // CoordSlice + CharacterPositions, + CharacterWidths, + + // bool + Expanded, + Selected, + + // Unique enums + NameFrom, + DescriptionFrom, + Invalid, + CheckedState, + Live, + DefaultActionVerb, + TextDirection, + Orientation, + SortDirection, + AriaCurrent, + HasPopup, + ListStyle, + TextAlign, + VerticalOffset, + + // Other + Transform, + Bounds, + TextSelection, + CustomActions, + + // This MUST be last. + Unset, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +struct PropertyIndices([u8; PropertyId::Unset as usize]); + +impl Default for PropertyIndices { + fn default() -> Self { + Self([PropertyId::Unset as u8; PropertyId::Unset as usize]) + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct NodeClass { + role: Role, + actions: Actions, + indices: PropertyIndices, +} + +/// Allows nodes that have the same role, actions, and set of defined properties +/// to share metadata. Each node has a class which is created by [`NodeBuilder`], +/// and when [`NodeBuilder::build`] is called, the node's class is added +/// to the provided instance of this struct if an identical class isn't +/// in that set already. Once a class is added to a class set, it currently +/// remains in that set for the life of that set, whether or not any nodes +/// are still using the class. +/// +/// It's not an error for different nodes in the same tree, or even subsequent +/// versions of the same node, to be built from different class sets; +/// it's merely suboptimal. +/// +/// Note: This struct's `Default` implementation doesn't provide access to +/// a shared set, as one might assume; it creates a new set. For a shared set, +/// use [`NodeClassSet::lock_global`]. +#[derive(Clone, Default)] +#[repr(transparent)] +pub struct NodeClassSet(BTreeSet>); + +impl NodeClassSet { + #[inline] + pub fn new() -> Self { + Default::default() + } + + /// Accesses a shared class set guarded by a mutex. + pub fn lock_global() -> impl DerefMut { + use std::{ + ops::Deref, + sync::{Mutex, MutexGuard}, + }; + + // We don't want to add a dependency like once_cell just like this, and + // once_cell would add a second level of synchronization anyway. We could + // use const initialization of BTreeSet, but that wasn't stabilized until + // Rust 1.66. So we just use Option. + static INSTANCE: Mutex> = Mutex::new(None); + + struct Guard<'a>(MutexGuard<'a, Option>); + + impl<'a> Deref for Guard<'a> { + type Target = NodeClassSet; + + #[inline] + fn deref(&self) -> &Self::Target { + self.0.as_ref().unwrap() + } + } + + impl<'a> DerefMut for Guard<'a> { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.as_mut().unwrap() + } + } + + let mut instance = INSTANCE.lock().unwrap(); + instance.get_or_insert_with(Default::default); + Guard(instance) + } +} + +/// A single accessible object. A complete UI is represented as a tree of these. +/// +/// For brevity, and to make more of the documentation usable in bindings +/// to other languages, documentation of getter methods is written as if +/// documenting fields in a struct, and such methods are referred to +/// as properties. +#[derive(Clone, Debug, PartialEq)] pub struct Node { - pub role: Role, - /// An affine transform to apply to any coordinates within this node - /// and its descendants, including the [`bounds`] field of this node. - /// The combined transforms of this node and its ancestors define - /// the coordinate space of this node. This field should be `None` - /// if it would be set to the identity transform, which should be - /// the case for most nodes. - /// - /// AccessKit expects the final transformed coordinates to be relative - /// to the origin of the tree's container (e.g. window), in physical - /// pixels, with the y coordinate being top-down. - /// - /// [`bounds`]: Node::bounds - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub transform: Option>, - /// The bounding box of this node, in the node's coordinate space. - /// This field does not affect the coordinate space of either this node - /// or its descendants; only the [`transform`] field affects that. - /// This, along with the recommendation that most nodes should have `None` - /// in their [`transform`] field, implies that the `bounds` field - /// of most nodes should be in the coordinate space of the nearest ancestor - /// with a non-`None` [`transform`] field, or if there is no such ancestor, - /// the tree's container (e.g. window). - /// - /// [`transform`]: Node::transform - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub bounds: Option, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub children: Vec, - - /// Unordered set of actions supported by this node. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "EnumSet::is_empty"))] - pub actions: EnumSet, - - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub name: Option>, - /// What information was used to compute the object's name. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub name_from: Option, + class: Arc, + flags: u32, + props: Arc<[PropertyValue]>, +} - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub description: Option>, - /// What information was used to compute the object's description. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub description_from: Option, +/// Builds a [`Node`]. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct NodeBuilder { + class: NodeClass, + flags: u32, + props: Vec, +} - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub value: Option>, +impl NodeClass { + fn get_property<'a>(&self, props: &'a [PropertyValue], id: PropertyId) -> &'a PropertyValue { + let index = self.indices.0[id as usize]; + if index == PropertyId::Unset as u8 { + &PropertyValue::None + } else { + &props[index as usize] + } + } +} - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub autofill_available: bool, - /// Whether this node is expanded, collapsed, or neither. - /// - /// Setting this to `false` means the node is collapsed; omitting it means this state - /// isn't applicable. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub expanded: Option, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub default: bool, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub editable: bool, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub focusable: bool, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub orientation: Option, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub hovered: bool, - /// Exclude this node and its descendants from the tree presented to - /// assistive technologies, and from hit testing. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub hidden: bool, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub linked: bool, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub multiline: bool, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub multiselectable: bool, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub protected: bool, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub required: bool, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub visited: bool, - - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub busy: bool, +fn unexpected_property_type() -> ! { + panic!(); +} - /// The object functions as a text field which exposes its descendants. - /// - /// Use cases include the root of a content-editable region, an ARIA - /// textbox which isn't currently editable and which has interactive - /// descendants, and a `` element that has "design-mode" set to "on". - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub nonatomic_text_field_root: bool, +impl NodeBuilder { + fn get_property_mut(&mut self, id: PropertyId, default: PropertyValue) -> &mut PropertyValue { + let index = self.class.indices.0[id as usize] as usize; + if index == PropertyId::Unset as usize { + self.props.push(default); + let index = self.props.len() - 1; + self.class.indices.0[id as usize] = index as u8; + &mut self.props[index] + } else { + if matches!(self.props[index], PropertyValue::None) { + self.props[index] = default; + } + &mut self.props[index] + } + } - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub live_atomic: bool, + fn set_property(&mut self, id: PropertyId, value: PropertyValue) { + let index = self.class.indices.0[id as usize]; + if index == PropertyId::Unset as u8 { + self.props.push(value); + self.class.indices.0[id as usize] = (self.props.len() - 1) as u8; + } else { + self.props[index as usize] = value; + } + } - /// If a dialog box is marked as explicitly modal. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub modal: bool, + fn clear_property(&mut self, id: PropertyId) { + let index = self.class.indices.0[id as usize]; + if index != PropertyId::Unset as u8 { + self.props[index as usize] = PropertyValue::None; + } + } +} - /// Set on a canvas element if it has fallback content. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub canvas_has_fallback: bool, +macro_rules! flag_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + impl Node { + $($(#[$doc])* + #[inline] + pub fn $getter(&self) -> bool { + (self.flags & (Flag::$id).mask()) != 0 + })* + } + impl NodeBuilder { + $($(#[$doc])* + #[inline] + pub fn $getter(&self) -> bool { + (self.flags & (Flag::$id).mask()) != 0 + } + #[inline] + pub fn $setter(&mut self) { + self.flags |= (Flag::$id).mask(); + } + #[inline] + pub fn $clearer(&mut self) { + self.flags &= !((Flag::$id).mask()); + })* + } + } +} - /// Indicates this node is user-scrollable, e.g. `overflow: scroll|auto`, as - /// opposed to only programmatically scrollable, like `overflow: hidden`, or - /// not scrollable at all, e.g. `overflow: visible`. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub scrollable: bool, +macro_rules! option_ref_type_getters { + ($(($method:ident, $type:ty, $variant:ident)),+) => { + impl NodeClass { + $(fn $method<'a>(&self, props: &'a [PropertyValue], id: PropertyId) -> Option<&'a $type> { + match self.get_property(props, id) { + PropertyValue::None => None, + PropertyValue::$variant(value) => Some(value), + _ => unexpected_property_type(), + } + })* + } + } +} - /// A hint to clients that the node is clickable. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub clickable: bool, +macro_rules! slice_type_getters { + ($(($method:ident, $type:ty, $variant:ident)),+) => { + impl NodeClass { + $(fn $method<'a>(&self, props: &'a [PropertyValue], id: PropertyId) -> &'a [$type] { + match self.get_property(props, id) { + PropertyValue::None => &[], + PropertyValue::$variant(value) => value, + _ => unexpected_property_type(), + } + })* + } + } +} - /// Indicates that this node clips its children, i.e. may have - /// `overflow: hidden` or clip children by default. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub clips_children: bool, - - /// Indicates that this node is not selectable because the style has - /// `user-select: none`. Note that there may be other reasons why a node is - /// not selectable - for example, bullets in a list. However, this attribute - /// is only set on `user-select: none`. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub not_user_selectable_style: bool, +macro_rules! copy_type_getters { + ($(($method:ident, $type:ty, $variant:ident)),+) => { + impl NodeClass { + $(fn $method(&self, props: &[PropertyValue], id: PropertyId) -> Option<$type> { + match self.get_property(props, id) { + PropertyValue::None => None, + PropertyValue::$variant(value) => Some(*value), + _ => unexpected_property_type(), + } + })* + } + } +} - /// Indicates whether this node is selected or unselected. - /// - /// The absence of this flag (as opposed to a `false` setting) - /// means that the concept of "selected" doesn't apply. - /// When deciding whether to set the flag to false or omit it, - /// consider whether it would be appropriate for a screen reader - /// to announce "not selected". The ambiguity of this flag - /// in platform accessibility APIs has made extraneous - /// "not selected" announcements a common annoyance. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub selected: Option, - /// Indicates whether this node is selected due to selection follows focus. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub selected_from_focus: bool, +macro_rules! box_type_setters { + ($(($method:ident, $type:ty, $variant:ident)),+) => { + impl NodeBuilder { + $(fn $method(&mut self, id: PropertyId, value: impl Into>) { + self.set_property(id, PropertyValue::$variant(value.into())); + })* + } + } +} - /// Indicates whether this node can be grabbed for drag-and-drop operation. - /// - /// Setting this flag to `false` rather than omitting it means that - /// this node is not currently grabbed but it can be. - /// - /// Note: [`aria-grabbed`] is deprecated in WAI-ARIA 1.1. - /// - /// [`aria-grabbed`]: https://www.w3.org/TR/wai-aria-1.1/#aria-grabbed - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub grabbed: Option, - /// Note: [`aria-dropeffect`] is deprecated in WAI-ARIA 1.1. - /// - /// [`aria-dropeffect`]: https://www.w3.org/TR/wai-aria-1.1/#aria-dropeffect - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "EnumSet::is_empty"))] - pub drop_effects: EnumSet, +macro_rules! copy_type_setters { + ($(($method:ident, $type:ty, $variant:ident)),+) => { + impl NodeBuilder { + $(fn $method(&mut self, id: PropertyId, value: $type) { + self.set_property(id, PropertyValue::$variant(value)); + })* + } + } +} - /// Indicates whether this node causes a hard line-break - /// (e.g. block level elements, or `
`). - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub is_line_breaking_object: bool, - /// Indicates whether this node causes a page break. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub is_page_breaking_object: bool, +macro_rules! vec_type_methods { + ($(($type:ty, $variant:ident, $getter:ident, $setter:ident, $pusher:ident)),+) => { + $(slice_type_getters! { + ($getter, $type, $variant) + })* + impl NodeBuilder { + $(fn $setter(&mut self, id: PropertyId, value: impl Into>) { + self.set_property(id, PropertyValue::$variant(value.into())); + } + fn $pusher(&mut self, id: PropertyId, item: $type) { + match self.get_property_mut(id, PropertyValue::$variant(Vec::new())) { + PropertyValue::$variant(v) => { + v.push(item); + } + _ => unexpected_property_type(), + } + })* + } + } +} + +macro_rules! property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $type_getter:ident, $getter_result:ty, $setter:ident, $type_setter:ident, $setter_param:ty, $clearer:ident)),+) => { + impl Node { + $($(#[$doc])* + #[inline] + pub fn $getter(&self) -> $getter_result { + self.class.$type_getter(&self.props, PropertyId::$id) + })* + } + impl NodeBuilder { + $($(#[$doc])* + #[inline] + pub fn $getter(&self) -> $getter_result { + self.class.$type_getter(&self.props, PropertyId::$id) + } + #[inline] + pub fn $setter(&mut self, value: $setter_param) { + self.$type_setter(PropertyId::$id, value); + } + #[inline] + pub fn $clearer(&mut self) { + self.clear_property(PropertyId::$id); + })* + } + } +} + +macro_rules! vec_property_methods { + ($($(#[$doc:meta])* ($id:ident, $item_type:ty, $getter:ident, $type_getter:ident, $setter:ident, $type_setter:ident, $pusher:ident, $type_pusher:ident, $clearer:ident)),+) => { + $(property_methods! { + $(#[$doc])* + ($id, $getter, $type_getter, &[$item_type], $setter, $type_setter, impl Into>, $clearer) + } + impl NodeBuilder { + #[inline] + pub fn $pusher(&mut self, item: $item_type) { + self.$type_pusher(PropertyId::$id, item); + } + })* + } +} + +macro_rules! node_id_vec_property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $pusher:ident, $clearer:ident)),+) => { + $(vec_property_methods! { + $(#[$doc])* + ($id, NodeId, $getter, get_node_id_vec, $setter, set_node_id_vec, $pusher, push_to_node_id_vec, $clearer) + })* + } +} + +macro_rules! node_id_property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + $(property_methods! { + $(#[$doc])* + ($id, $getter, get_node_id_property, Option, $setter, set_node_id_property, NodeId, $clearer) + })* + } +} + +macro_rules! string_property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + $(property_methods! { + $(#[$doc])* + ($id, $getter, get_string_property, Option<&str>, $setter, set_string_property, impl Into>, $clearer) + })* + } +} + +macro_rules! f64_property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + $(property_methods! { + $(#[$doc])* + ($id, $getter, get_f64_property, Option, $setter, set_f64_property, f64, $clearer) + })* + } +} - /// True if the node has any ARIA attributes set. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub has_aria_attribute: bool, +macro_rules! usize_property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + $(property_methods! { + $(#[$doc])* + ($id, $getter, get_usize_property, Option, $setter, set_usize_property, usize, $clearer) + })* + } +} + +macro_rules! color_property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + $(property_methods! { + $(#[$doc])* + ($id, $getter, get_color_property, Option, $setter, set_color_property, u32, $clearer) + })* + } +} + +macro_rules! text_decoration_property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + $(property_methods! { + $(#[$doc])* + ($id, $getter, get_text_decoration_property, Option, $setter, set_text_decoration_property, TextDecoration, $clearer) + })* + } +} + +macro_rules! length_slice_property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + $(property_methods! { + $(#[$doc])* + ($id, $getter, get_length_slice_property, &[u8], $setter, set_length_slice_property, impl Into>, $clearer) + })* + } +} + +macro_rules! coord_slice_property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + $(property_methods! { + $(#[$doc])* + ($id, $getter, get_coord_slice_property, Option<&[f32]>, $setter, set_coord_slice_property, impl Into>, $clearer) + })* + } +} +macro_rules! bool_property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + $(property_methods! { + $(#[$doc])* + ($id, $getter, get_bool_property, Option, $setter, set_bool_property, bool, $clearer) + })* + } +} + +macro_rules! unique_enum_property_methods { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + impl Node { + $($(#[$doc])* + #[inline] + pub fn $getter(&self) -> Option<$id> { + match self.class.get_property(&self.props, PropertyId::$id) { + PropertyValue::None => None, + PropertyValue::$id(value) => Some(*value), + _ => unexpected_property_type(), + } + })* + } + impl NodeBuilder { + $($(#[$doc])* + #[inline] + pub fn $getter(&self) -> Option<$id> { + match self.class.get_property(&self.props, PropertyId::$id) { + PropertyValue::None => None, + PropertyValue::$id(value) => Some(*value), + _ => unexpected_property_type(), + } + } + #[inline] + pub fn $setter(&mut self, value: $id) { + self.set_property(PropertyId::$id, PropertyValue::$id(value)); + } + #[inline] + pub fn $clearer(&mut self) { + self.clear_property(PropertyId::$id); + })* + } + } +} + +impl NodeBuilder { + #[inline] + pub fn new(role: Role) -> Self { + Self { + class: NodeClass { + role, + ..Default::default() + }, + ..Default::default() + } + } + + pub fn build(self, classes: &mut NodeClassSet) -> Node { + let class = if let Some(class) = classes.0.get(&self.class) { + Arc::clone(class) + } else { + let class = Arc::new(self.class); + classes.0.insert(Arc::clone(&class)); + class + }; + Node { + class, + flags: self.flags, + props: self.props.into(), + } + } +} + +impl Node { + #[inline] + pub fn role(&self) -> Role { + self.class.role + } +} + +impl NodeBuilder { + #[inline] + pub fn role(&self) -> Role { + self.class.role + } + #[inline] + pub fn set_role(&mut self, value: Role) { + self.class.role = value; + } +} + +impl Node { + #[inline] + pub fn supports_action(&self, action: Action) -> bool { + (self.class.actions.0 & action.mask()) != 0 + } +} + +impl NodeBuilder { + #[inline] + pub fn supports_action(&self, action: Action) -> bool { + (self.class.actions.0 & action.mask()) != 0 + } + #[inline] + pub fn add_action(&mut self, action: Action) { + self.class.actions.0 |= action.mask(); + } + #[inline] + pub fn remove_action(&mut self, action: Action) { + self.class.actions.0 &= !(action.mask()); + } + #[inline] + pub fn clear_actions(&mut self) { + self.class.actions.0 = 0; + } +} + +flag_methods! { + (AutofillAvailable, is_autofill_available, set_autofill_available, clear_autofill_available), + (Default, is_default, set_default, clear_default), + (Editable, is_editable, set_editable, clear_editable), + (Hovered, is_hovered, set_hovered, clear_hovered), + /// Exclude this node and its descendants from the tree presented to + /// assistive technologies, and from hit testing. + (Hidden, is_hidden, set_hidden, clear_hidden), + (Linked, is_linked, set_linked, clear_linked), + (Multiline, is_multiline, set_multiline, clear_multiline), + (Multiselectable, is_multiselectable, set_multiselectable, clear_multiselectable), + (Protected, is_protected, set_protected, clear_protected), + (Required, is_required, set_required, clear_required), + (Visited, is_visited, set_visited, clear_visited), + (Busy, is_busy, set_busy, clear_busy), + (LiveAtomic, is_live_atomic, set_live_atomic, clear_live_atomic), + /// If a dialog box is marked as explicitly modal. + (Modal, is_modal, set_modal, clear_modal), + /// Indicates this node is user-scrollable, e.g. `overflow: scroll|auto`, as + /// opposed to only programmatically scrollable, like `overflow: hidden`, or + /// not scrollable at all, e.g. `overflow: visible`. + (Scrollable, is_scrollable, set_scrollable, clear_scrollable), + /// Indicates whether this node is selected due to selection follows focus. + (SelectedFromFocus, is_selected_from_focus, set_selected_from_focus, clear_selected_from_focus), /// This element allows touches to be passed through when a screen reader /// is in touch exploration mode, e.g. a virtual keyboard normally /// behaves this way. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub touch_pass_through: bool, + (TouchPassThrough, is_touch_pass_through, set_touch_pass_through, clear_touch_pass_through), + /// Use for a textbox that allows focus/selection but not input. + (ReadOnly, is_read_only, set_read_only, clear_read_only), + /// Use for a control or group of controls that disallows input. + (Disabled, is_disabled, set_disabled, clear_disabled), + (Bold, is_bold, set_bold, clear_bold), + (Italic, is_italic, set_italic, clear_italic), + /// Set on a canvas element if it has fallback content. + (CanvasHasFallback, canvas_has_fallback, set_canvas_has_fallback, clear_canvas_has_fallback), + /// Indicates that this node clips its children, i.e. may have + /// `overflow: hidden` or clip children by default. + (ClipsChildren, clips_children, set_clips_children, clear_clips_children), + /// Indicates whether this node causes a hard line-break + /// (e.g. block level elements, or `
`). + (IsLineBreakingObject, is_line_breaking_object, set_is_line_breaking_object, clear_is_line_breaking_object), + /// Indicates whether this node causes a page break. + (IsPageBreakingObject, is_page_breaking_object, set_is_page_breaking_object, clear_is_page_breaking_object), + (IsSpellingError, is_spelling_error, set_is_spelling_error, clear_is_spelling_error), + (IsGrammarError, is_grammar_error, set_is_grammar_error, clear_is_grammar_error), + (IsSearchMatch, is_search_match, set_is_search_match, clear_is_search_match), + (IsSuggestion, is_suggestion, set_is_suggestion, clear_is_suggestion), + /// The object functions as a text field which exposes its descendants. + /// + /// Use cases include the root of a content-editable region, an ARIA + /// textbox which isn't currently editable and which has interactive + /// descendants, and a `` element that has "design-mode" set to "on". + (IsNonatomicTextFieldRoot, is_nonatomic_text_field_root, set_is_nonatomic_text_field_root, clear_is_nonatomic_text_field_root) +} + +option_ref_type_getters! { + (get_affine_property, Affine, Affine), + (get_string_property, str, String), + (get_coord_slice_property, [f32], CoordSlice), + (get_text_selection_property, TextSelection, TextSelection) +} +slice_type_getters! { + (get_length_slice_property, u8, LengthSlice) +} + +copy_type_getters! { + (get_rect_property, Rect, Rect), + (get_node_id_property, NodeId, NodeId), + (get_f64_property, f64, F64), + (get_usize_property, usize, Usize), + (get_color_property, u32, Color), + (get_text_decoration_property, TextDecoration, TextDecoration), + (get_bool_property, bool, Bool) +} + +box_type_setters! { + (set_affine_property, Affine, Affine), + (set_string_property, str, String), + (set_length_slice_property, [u8], LengthSlice), + (set_coord_slice_property, [f32], CoordSlice), + (set_text_selection_property, TextSelection, TextSelection) +} + +copy_type_setters! { + (set_rect_property, Rect, Rect), + (set_node_id_property, NodeId, NodeId), + (set_f64_property, f64, F64), + (set_usize_property, usize, Usize), + (set_color_property, u32, Color), + (set_text_decoration_property, TextDecoration, TextDecoration), + (set_bool_property, bool, Bool) +} + +vec_type_methods! { + (NodeId, NodeIdVec, get_node_id_vec, set_node_id_vec, push_to_node_id_vec), + (CustomAction, CustomActionVec, get_custom_action_vec, set_custom_action_vec, push_to_custom_action_vec) +} + +node_id_vec_property_methods! { + (Children, children, set_children, push_child, clear_children), /// Ids of nodes that are children of this node logically, but are /// not children of this node in the tree structure. As an example, /// a table cell is a child of a row, and an 'indirect' child of a /// column. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub indirect_children: Vec, - - // Relationships between this node and other nodes. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub active_descendant: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub error_message: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub in_page_link_target: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub member_of: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub next_on_line: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub previous_on_line: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub popup_for: Option, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub controls: Vec, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub details: Vec, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub described_by: Vec, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub flow_to: Vec, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub labelled_by: Vec, + (IndirectChildren, indirect_children, set_indirect_children, push_indirect_child, clear_indirect_children), + (Controls, controls, set_controls, push_controlled, clear_controls), + (Details, details, set_details, push_detail, clear_details), + (DescribedBy, described_by, set_described_by, push_described_by, clear_described_by), + (FlowTo, flow_to, set_flow_to, push_flow_to, clear_flow_to), + (LabelledBy, labelled_by, set_labelled_by, push_labelled_by, clear_labelled_by), /// On radio buttons this should be set to a list of all of the buttons /// in the same group as this one, including this radio button itself. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub radio_group: Vec, - - #[cfg_attr(feature = "serde", serde(default))] - #[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, + (RadioGroup, radio_group, set_radio_group, push_to_radio_group, clear_radio_group) +} + +node_id_property_methods! { + (ActiveDescendant, active_descendant, set_active_descendant, clear_active_descendant), + (ErrorMessage, error_message, set_error_message, clear_error_message), + (InPageLinkTarget, in_page_link_target, set_in_page_link_target, clear_in_page_link_target), + (MemberOf, member_of, set_member_of, clear_member_of), + (NextOnLine, next_on_line, set_next_on_line, clear_next_on_line), + (PreviousOnLine, previous_on_line, set_previous_on_line, clear_previous_on_line), + (PopupFor, popup_for, set_popup_for, clear_popup_for), + (TableHeader, table_header, set_table_header, clear_table_header), + (TableRowHeader, table_row_header, set_table_row_header, clear_table_row_header), + (TableColumnHeader, table_column_header, set_table_column_header, clear_table_column_header), + (NextFocus, next_focus, set_next_focus, clear_next_focus), + (PreviousFocus, previous_focus, set_previous_focus, clear_previous_focus) +} + +string_property_methods! { + (Name, name, set_name, clear_name), + (Description, description, set_description, clear_description), + (Value, value, set_value, clear_value), + (AccessKey, access_key, set_access_key, clear_access_key), + (AutoComplete, auto_complete, set_auto_complete, clear_auto_complete), + (CheckedStateDescription, checked_state_description, set_checked_state_description, clear_checked_state_description), + (ClassName, class_name, set_class_name, clear_class_name), + (CssDisplay, css_display, set_css_display, clear_css_display), + /// Only present when different from parent. + (FontFamily, font_family, set_font_family, clear_font_family), + (HtmlTag, html_tag, set_html_tag, clear_html_tag), + /// Inner HTML of an element. Only used for a top-level math element, + /// to support third-party math accessibility products that parse MathML. + (InnerHtml, inner_html, set_inner_html, clear_inner_html), + (InputType, input_type, set_input_type, clear_input_type), + (KeyShortcuts, key_shortcuts, set_key_shortcuts, clear_key_shortcuts), + /// Only present when different from parent. + (Language, language, set_language, clear_language), + (LiveRelevant, live_relevant, set_live_relevant, clear_live_relevant), + /// Only if not already exposed in [`name`] ([`NameFrom::Placeholder`]). + /// + /// [`name`]: Node::name + (Placeholder, placeholder, set_placeholder, clear_placeholder), + (AriaRole, aria_role, set_aria_role, clear_aria_role), + (RoleDescription, role_description, set_role_description, clear_role_description), + /// Only if not already exposed in [`name`] ([`NameFrom::Title`]). + /// + /// [`name`]: Node::name + (Tooltip, tooltip, set_tooltip, clear_tooltip), + (Url, url, set_url, clear_url) +} + +f64_property_methods! { + (ScrollX, scroll_x, set_scroll_x, clear_scroll_x), + (ScrollXMin, scroll_x_min, set_scroll_x_min, clear_scroll_x_min), + (ScrollXMax, scroll_x_max, set_scroll_x_max, clear_scroll_x_max), + (ScrollY, scroll_y, set_scroll_y, clear_scroll_y), + (ScrollYMin, scroll_y_min, set_scroll_y_min, clear_scroll_y_min), + (ScrollYMax, scroll_y_max, set_scroll_y_max, clear_scroll_y_max), + (NumericValue, numeric_value, set_numeric_value, clear_numeric_value), + (MinNumericValue, min_numeric_value, set_min_numeric_value, clear_min_numeric_value), + (MaxNumericValue, max_numeric_value, set_max_numeric_value, clear_max_numeric_value), + (NumericValueStep, numeric_value_step, set_numeric_value_step, clear_numeric_value_step), + (NumericValueJump, numeric_value_jump, set_numeric_value_jump, clear_numeric_value_jump), + /// Font size is in pixels. + (FontSize, font_size, set_font_size, clear_font_size), + /// Font weight can take on any arbitrary numeric value. Increments of 100 in + /// range `[0, 900]` represent keywords such as light, normal, bold, etc. + (FontWeight, font_weight, set_font_weight, clear_font_weight), + /// The indentation of the text, in mm. + (TextIndent, text_indent, set_text_indent, clear_text_indent) +} + +usize_property_methods! { + (TableRowCount, table_row_count, set_table_row_count, clear_table_row_count), + (TableColumnCount, table_column_count, set_table_column_count, clear_table_column_count), + (TableRowIndex, table_row_index, set_table_row_index, clear_table_row_index), + (TableColumnIndex, table_column_index, set_table_column_index, clear_table_column_index), + (TableCellColumnIndex, table_cell_column_index, set_table_cell_column_index, clear_table_cell_column_index), + (TableCellColumnSpan, table_cell_column_span, set_table_cell_column_span, clear_table_cell_column_span), + (TableCellRowIndex, table_cell_row_index, set_table_cell_row_index, clear_table_cell_row_index), + (TableCellRowSpan, table_cell_row_span, set_table_cell_row_span, clear_table_cell_row_span), + (HierarchicalLevel, hierarchical_level, set_hierarchical_level, clear_hierarchical_level), + (SizeOfSet, size_of_set, set_size_of_set, clear_size_of_set), + (PositionInSet, position_in_set, set_position_in_set, clear_position_in_set) +} + +color_property_methods! { + /// For [`Role::ColorWell`], specifies the selected color in RGBA. + (ColorValue, color_value, set_color_value, clear_color_value), + /// Background color in RGBA. + (BackgroundColor, background_color, set_background_color, clear_background_color), + /// Foreground color in RGBA. + (ForegroundColor, foreground_color, set_foreground_color, clear_foreground_color) +} + +text_decoration_property_methods! { + (Overline, overline, set_overline, clear_overline), + (Strikethrough, strikethrough, set_strikethrough, clear_strikethrough), + (Underline, underline, set_underline, clear_underline) +} +length_slice_property_methods! { /// 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. + /// the length of [`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 @@ -941,285 +1638,640 @@ pub struct Node { /// 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_lengths: Box<[u8]>, + /// + /// [`value`]: Node::value + (CharacterLengths, character_lengths, set_character_lengths, clear_character_lengths), + + /// For inline text. The length of each word in characters, as defined + /// in [`character_lengths`]. The sum of these lengths must equal + /// the length of [`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. + /// + /// [`character_lengths`]: Node::character_lengths + (WordLengths, word_lengths, set_word_lengths, clear_word_lengths) +} + +coord_slice_property_methods! { /// 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. + /// [`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 + /// of [`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 + /// This property 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>, + /// + /// [`text_direction`]: Node::text_direction + /// [`character_lengths`]: Node::character_lengths + (CharacterPositions, character_positions, set_character_positions, clear_character_positions), + /// For inline text. This is the advance width of each character, - /// in the direction given by [`Node::text_direction`], in the coordinate + /// in the direction given by [`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 + /// of [`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 + /// This property 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>, + /// + /// [`text_direction`]: Node::text_direction + /// [`character_lengths`]: Node::character_lengths + (CharacterWidths, character_widths, set_character_widths, clear_character_widths) +} - /// 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`]. +bool_property_methods! { + /// Whether this node is expanded, collapsed, or neither. /// - /// 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. + /// Setting this to `false` means the node is collapsed; omitting it means this state + /// isn't applicable. + (Expanded, is_expanded, set_expanded, clear_expanded), + + /// Indicates whether this node is selected or unselected. /// - /// 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 word_lengths: Box<[u8]>, + /// The absence of this flag (as opposed to a `false` setting) + /// means that the concept of "selected" doesn't apply. + /// When deciding whether to set the flag to false or omit it, + /// consider whether it would be appropriate for a screen reader + /// to announce "not selected". The ambiguity of this flag + /// in platform accessibility APIs has made extraneous + /// "not selected" announcements a common annoyance. + (Selected, is_selected, set_selected, clear_selected) +} - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_empty"))] - pub custom_actions: Box<[CustomAction]>, +unique_enum_property_methods! { + /// What information was used to compute the object's name. + (NameFrom, name_from, set_name_from, clear_name_from), + /// What information was used to compute the object's description. + (DescriptionFrom, description_from, set_description_from, clear_description_from), + (Invalid, invalid, set_invalid, clear_invalid), + (CheckedState, checked_state, set_checked_state, clear_checked_state), + (Live, live, set_live, clear_live), + (DefaultActionVerb, default_action_verb, set_default_action_verb, clear_default_action_verb), + (TextDirection, text_direction, set_text_direction, clear_text_direction), + (Orientation, orientation, set_orientation, clear_orientation), + (SortDirection, sort_direction, set_sort_direction, clear_sort_direction), + (AriaCurrent, aria_current, set_aria_current, clear_aria_current), + (HasPopup, has_popup, set_has_popup, clear_has_popup), + /// The list style type. Only available on list items. + (ListStyle, list_style, set_list_style, clear_list_style), + (TextAlign, text_align, set_text_align, clear_text_align), + (VerticalOffset, vertical_offset, set_vertical_offset, clear_vertical_offset) +} - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub access_key: Option>, +property_methods! { + /// An affine transform to apply to any coordinates within this node + /// and its descendants, including the [`bounds`] property of this node. + /// The combined transforms of this node and its ancestors define + /// the coordinate space of this node. /// This should be `None` if + /// it would be set to the identity transform, which should be the case + /// for most nodes. + /// + /// AccessKit expects the final transformed coordinates to be relative + /// to the origin of the tree's container (e.g. window), in physical + /// pixels, with the y coordinate being top-down. + /// + /// [`bounds`]: Node::bounds + (Transform, transform, get_affine_property, Option<&Affine>, set_transform, set_affine_property, impl Into>, clear_transform), - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub invalid_state: Option, + /// The bounding box of this node, in the node's coordinate space. + /// This property does not affect the coordinate space of either this node + /// or its descendants; only the [`transform`] property affects that. + /// This, along with the recommendation that most nodes should have + /// a [`transform`] of `None`, implies that the `bounds` property + /// of most nodes should be in the coordinate space of the nearest ancestor + /// with a non-`None` [`transform`], or if there is no such ancestor, + /// the tree's container (e.g. window). + /// + /// [`transform`]: Node::transform + (Bounds, bounds, get_rect_property, Option, set_bounds, set_rect_property, Rect, clear_bounds), - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub auto_complete: Option>, + (TextSelection, text_selection, get_text_selection_property, Option<&TextSelection>, set_text_selection, set_text_selection_property, impl Into>, clear_text_selection) +} - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub checked_state: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub checked_state_description: Option>, +vec_property_methods! { + (CustomActions, CustomAction, custom_actions, get_custom_action_vec, set_custom_actions, set_custom_action_vec, push_custom_action, push_to_custom_action_vec, clear_custom_actions) +} - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub class_name: Option>, +#[cfg(feature = "serde")] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +enum ClassFieldId { + Role, + Actions, +} - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub css_display: Option>, +#[cfg(feature = "serde")] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(untagged)] +enum DeserializeKey { + ClassField(ClassFieldId), + Flag(Flag), + Property(PropertyId), + Unknown(String), +} - /// Only present when different from parent. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub font_family: Option>, +#[cfg(feature = "serde")] +macro_rules! serialize_class_fields { + ($self:ident, $map:ident, { $(($name:ident, $id:ident)),+ }) => { + $($map.serialize_entry(&ClassFieldId::$id, &$self.class.$name)?;)* + } +} + +#[cfg(feature = "serde")] +macro_rules! serialize_property { + ($self:ident, $map:ident, $index:ident, $id:ident, { $($variant:ident),+ }) => { + match &$self.props[$index as usize] { + PropertyValue::None => (), + $(PropertyValue::$variant(value) => { + $map.serialize_entry(&$id, &Some(value))?; + })* + } + } +} - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub html_tag: Option>, +#[cfg(feature = "serde")] +macro_rules! deserialize_class_field { + ($builder:ident, $map:ident, $key:ident, { $(($name:ident, $id:ident)),+ }) => { + match $key { + $(ClassFieldId::$id => { + $builder.class.$name = $map.next_value()?; + })* + } + } +} - /// Inner HTML of an element. Only used for a top-level math element, - /// to support third-party math accessibility products that parse MathML. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub inner_html: Option>, +#[cfg(feature = "serde")] +macro_rules! deserialize_property { + ($builder:ident, $map:ident, $key:ident, { $($type:ident { $($id:ident),+ }),+ }) => { + match $key { + $($(PropertyId::$id => { + if let Some(value) = $map.next_value()? { + $builder.set_property(PropertyId::$id, PropertyValue::$type(value)); + } else { + $builder.clear_property(PropertyId::$id); + } + })*)* + PropertyId::Unset => { + let _ = $map.next_value::()?; + } + } + } +} - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub input_type: Option>, +#[cfg(feature = "serde")] +impl Serialize for Node { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(None)?; + serialize_class_fields!(self, map, { + (role, Role), + (actions, Actions) + }); + for i in 0..((size_of_val(&self.flags) as u8) * 8) { + if let Some(flag) = Flag::n(i) { + if (self.flags & flag.mask()) != 0 { + map.serialize_entry(&flag, &true)?; + } + } + } + for (id, index) in self.class.indices.0.iter().copied().enumerate() { + if index == PropertyId::Unset as u8 { + continue; + } + let id = PropertyId::n(id as _).unwrap(); + serialize_property!(self, map, index, id, { + NodeIdVec, + NodeId, + String, + F64, + Usize, + Color, + TextDecoration, + LengthSlice, + CoordSlice, + Bool, + NameFrom, + DescriptionFrom, + Invalid, + CheckedState, + Live, + DefaultActionVerb, + TextDirection, + Orientation, + SortDirection, + AriaCurrent, + HasPopup, + ListStyle, + TextAlign, + VerticalOffset, + Affine, + Rect, + TextSelection, + CustomActionVec + }); + } + map.end() + } +} - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub key_shortcuts: Option>, +#[cfg(feature = "serde")] +struct NodeVisitor; - /// Only present when different from parent. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub language: Option>, - - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub live_relevant: Option>, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub live: Option, - - /// Only if not already exposed in [`Node::name`] ([`NameFrom::Placeholder`]). - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub placeholder: Option>, - - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub aria_role: Option>, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub role_description: Option>, - - /// Only if not already exposed in [`Node::name`] ([`NameFrom::Title`]). - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub tooltip: Option>, - - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub url: Option>, - - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub default_action_verb: Option, - - // Scrollable container attributes. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub scroll_x: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub scroll_x_min: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub scroll_x_max: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub scroll_y: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub scroll_y_min: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub scroll_y_max: Option, - - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub text_selection: Option, - - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub aria_column_count: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub aria_cell_column_index: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub aria_cell_column_span: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub aria_row_count: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub aria_cell_row_index: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub aria_cell_row_span: Option, - - // Table attributes. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub table_row_count: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub table_column_count: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub table_header: Option, - - // Table row attributes. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub table_row_index: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub table_row_header: Option, - - // Table column attributes. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub table_column_index: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub table_column_header: Option, - - // Table cell attributes. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub table_cell_column_index: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub table_cell_column_span: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub table_cell_row_index: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub table_cell_row_span: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub sort_direction: Option, - - /// Tree control attributes. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub hierarchical_level: Option, +#[cfg(feature = "serde")] +impl<'de> Visitor<'de> for NodeVisitor { + type Value = Node; - /// Use for a textbox that allows focus/selection but not input. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub read_only: bool, - /// Use for a control or group of controls that disallows input. - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub disabled: bool, + #[inline] + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct Node") + } - // Position or Number of items in current set of listitems or treeitems - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub set_size: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub pos_in_set: Option, + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut builder = NodeBuilder::default(); + while let Some(key) = map.next_key()? { + match key { + DeserializeKey::ClassField(id) => { + deserialize_class_field!(builder, map, id, { + (role, Role), + (actions, Actions) + }); + } + DeserializeKey::Flag(flag) => { + if map.next_value()? { + builder.flags |= flag.mask(); + } else { + builder.flags &= !(flag.mask()); + } + } + DeserializeKey::Property(id) => { + deserialize_property!(builder, map, id, { + NodeIdVec { + Children, + IndirectChildren, + Controls, + Details, + DescribedBy, + FlowTo, + LabelledBy, + RadioGroup + }, + NodeId { + ActiveDescendant, + ErrorMessage, + InPageLinkTarget, + MemberOf, + NextOnLine, + PreviousOnLine, + PopupFor, + TableHeader, + TableRowHeader, + TableColumnHeader, + NextFocus, + PreviousFocus + }, + String { + Name, + Description, + Value, + AccessKey, + AutoComplete, + CheckedStateDescription, + ClassName, + CssDisplay, + FontFamily, + HtmlTag, + InnerHtml, + InputType, + KeyShortcuts, + Language, + LiveRelevant, + Placeholder, + AriaRole, + RoleDescription, + Tooltip, + Url + }, + F64 { + ScrollX, + ScrollXMin, + ScrollXMax, + ScrollY, + ScrollYMin, + ScrollYMax, + NumericValue, + MinNumericValue, + MaxNumericValue, + NumericValueStep, + NumericValueJump, + FontSize, + FontWeight, + TextIndent + }, + Usize { + TableRowCount, + TableColumnCount, + TableRowIndex, + TableColumnIndex, + TableCellColumnIndex, + TableCellColumnSpan, + TableCellRowIndex, + TableCellRowSpan, + HierarchicalLevel, + SizeOfSet, + PositionInSet + }, + Color { + ColorValue, + BackgroundColor, + ForegroundColor + }, + TextDecoration { + Overline, + Strikethrough, + Underline + }, + LengthSlice { + CharacterLengths, + WordLengths + }, + CoordSlice { + CharacterPositions, + CharacterWidths + }, + Bool { + Expanded, + Selected + }, + NameFrom { NameFrom }, + DescriptionFrom { DescriptionFrom }, + Invalid { Invalid }, + CheckedState { CheckedState }, + Live { Live }, + DefaultActionVerb { DefaultActionVerb }, + TextDirection { TextDirection }, + Orientation { Orientation }, + SortDirection { SortDirection }, + AriaCurrent { AriaCurrent }, + HasPopup { HasPopup }, + ListStyle { ListStyle }, + TextAlign { TextAlign }, + VerticalOffset { VerticalOffset }, + Affine { Transform }, + Rect { Bounds }, + TextSelection { TextSelection }, + CustomActionVec { CustomActions } + }); + } + DeserializeKey::Unknown(_) => { + let _ = map.next_value::()?; + } + } + } - /// For [`Role::ColorWell`], specifies the selected color in RGBA. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub color_value: Option, + Ok(builder.build(&mut NodeClassSet::lock_global())) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Node { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(NodeVisitor) + } +} - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub aria_current: Option, +#[cfg(feature = "schemars")] +macro_rules! add_schema_property { + ($gen:ident, $properties:ident, $enum_value:expr, $type:ty) => {{ + let name = format!("{:?}", $enum_value); + let name = name[..1].to_ascii_lowercase() + &name[1..]; + let subschema = $gen.subschema_for::<$type>(); + $properties.insert(name, subschema); + }}; +} - /// Background color in RGBA. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub background_color: Option, - /// Foreground color in RGBA. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub foreground_color: Option, +#[cfg(feature = "schemars")] +macro_rules! add_flags_to_schema { + ($gen:ident, $properties:ident, { $($variant:ident),+ }) => { + $(add_schema_property!($gen, $properties, Flag::$variant, bool);)* + } +} - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub has_popup: Option, +#[cfg(feature = "schemars")] +macro_rules! add_properties_to_schema { + ($gen:ident, $properties:ident, { $($type:ty { $($id:ident),+ }),+ }) => { + $($(add_schema_property!($gen, $properties, PropertyId::$id, Option<$type>);)*)* + } +} - /// The list style type. Only available on list items. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub list_style: Option, - - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub text_align: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub vertical_offset: Option, - - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub bold: bool, - #[cfg_attr(feature = "serde", serde(default))] - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - pub italic: bool, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub overline: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub strikethrough: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub underline: Option, - - // Focus traversal order. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub previous_focus: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub next_focus: Option, - - // Numeric value attributes. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub numeric_value: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub min_numeric_value: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub max_numeric_value: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub numeric_value_step: Option, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub numeric_value_jump: Option, - - // Text attributes. - /// Font size is in pixels. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub font_size: Option, - /// Font weight can take on any arbitrary numeric value. Increments of 100 in - /// range `[0, 900]` represent keywords such as light, normal, bold, etc. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub font_weight: Option, - /// The text indent of the text, in mm. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub text_indent: Option, +#[cfg(feature = "schemars")] +impl JsonSchema for Node { + #[inline] + fn schema_name() -> String { + "Node".into() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + let mut properties = SchemaMap::::new(); + add_schema_property!(gen, properties, ClassFieldId::Role, Role); + add_schema_property!(gen, properties, ClassFieldId::Actions, Actions); + add_flags_to_schema!(gen, properties, { + AutofillAvailable, + Default, + Editable, + Hovered, + Hidden, + Linked, + Multiline, + Multiselectable, + Protected, + Required, + Visited, + Busy, + LiveAtomic, + Modal, + Scrollable, + SelectedFromFocus, + TouchPassThrough, + ReadOnly, + Disabled, + Bold, + Italic, + CanvasHasFallback, + ClipsChildren, + IsLineBreakingObject, + IsPageBreakingObject, + IsSpellingError, + IsGrammarError, + IsSearchMatch, + IsSuggestion, + IsNonatomicTextFieldRoot + }); + add_properties_to_schema!(gen, properties, { + Vec { + Children, + IndirectChildren, + Controls, + Details, + DescribedBy, + FlowTo, + LabelledBy, + RadioGroup + }, + NodeId { + ActiveDescendant, + ErrorMessage, + InPageLinkTarget, + MemberOf, + NextOnLine, + PreviousOnLine, + PopupFor, + TableHeader, + TableRowHeader, + TableColumnHeader, + NextFocus, + PreviousFocus + }, + Box { + Name, + Description, + Value, + AccessKey, + AutoComplete, + CheckedStateDescription, + ClassName, + CssDisplay, + FontFamily, + HtmlTag, + InnerHtml, + InputType, + KeyShortcuts, + Language, + LiveRelevant, + Placeholder, + AriaRole, + RoleDescription, + Tooltip, + Url + }, + f64 { + ScrollX, + ScrollXMin, + ScrollXMax, + ScrollY, + ScrollYMin, + ScrollYMax, + NumericValue, + MinNumericValue, + MaxNumericValue, + NumericValueStep, + NumericValueJump, + FontSize, + FontWeight, + TextIndent + }, + usize { + TableRowCount, + TableColumnCount, + TableRowIndex, + TableColumnIndex, + TableCellColumnIndex, + TableCellColumnSpan, + TableCellRowIndex, + TableCellRowSpan, + HierarchicalLevel, + SizeOfSet, + PositionInSet + }, + u32 { + ColorValue, + BackgroundColor, + ForegroundColor + }, + TextDecoration { + Overline, + Strikethrough, + Underline + }, + Box<[u8]> { + CharacterLengths, + WordLengths + }, + Box<[f32]> { + CharacterPositions, + CharacterWidths + }, + bool { + Expanded, + Selected + }, + NameFrom { NameFrom }, + DescriptionFrom { DescriptionFrom }, + Invalid { Invalid }, + CheckedState { CheckedState }, + Live { Live }, + DefaultActionVerb { DefaultActionVerb }, + TextDirection { TextDirection }, + Orientation { Orientation }, + SortDirection { SortDirection }, + AriaCurrent { AriaCurrent }, + HasPopup { HasPopup }, + ListStyle { ListStyle }, + TextAlign { TextAlign }, + VerticalOffset { VerticalOffset }, + Affine { Transform }, + Rect { Bounds }, + TextSelection { TextSelection }, + Vec { CustomActions } + }); + SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some( + ObjectValidation { + properties, + ..Default::default() + } + .into(), + ), + ..Default::default() + } + .into() + } } /// The data associated with an accessibility tree that's global to the @@ -1227,7 +2279,6 @@ pub struct Node { #[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 Tree { @@ -1236,11 +2287,11 @@ pub struct Tree { /// The node that's used as the root scroller, if any. On some platforms /// like Android we need to ignore accessibility scroll offsets for /// that node and get them from the viewport instead. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub root_scroller: Option, } impl Tree { + #[inline] pub fn new(root: NodeId) -> Tree { Tree { root, @@ -1263,7 +2314,6 @@ impl Tree { #[derive(Clone, Debug, Default, PartialEq)] #[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 TreeUpdate { @@ -1287,13 +2337,12 @@ pub struct TreeUpdate { /// an updated version of the parent node with the child's ID removed /// from [`Node::children`]. Neither the child nor any of its descendants /// may be included in this list. - pub nodes: Vec<(NodeId, Arc)>, + pub nodes: Vec<(NodeId, Node)>, /// Rarely updated information about the tree as a whole. This may be omitted /// if it has not changed since the previous update, but providing the same /// information again is also allowed. This is required when initializing /// a tree. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub tree: Option, /// The node with keyboard focus within this tree, if any. @@ -1307,7 +2356,6 @@ pub struct TreeUpdate { /// render widgets (e.g. to draw or not draw a focus rectangle), /// so this focus tracking should not be duplicated between the toolkit /// and the AccessKit platform adapters. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub focus: Option, } @@ -1320,7 +2368,6 @@ impl TreeUpdate> From for TreeUpdate { #[derive(Clone, Debug, PartialEq)] #[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 ActionData { CustomAction(i32), @@ -1341,13 +2388,11 @@ pub enum ActionData { #[derive(Clone, Debug, PartialEq)] #[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 ActionRequest { pub action: Action, pub target: NodeId, - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub data: Option, } diff --git a/consumer/src/iterators.rs b/consumer/src/iterators.rs index c4b3d27e1..7d01b7bdd 100644 --- a/consumer/src/iterators.rs +++ b/consumer/src/iterators.rs @@ -29,7 +29,7 @@ impl<'a> FollowingSiblings<'a> { let parent_and_index = node.parent_and_index(); let (back_position, front_position, done) = if let Some((ref parent, index)) = parent_and_index { - let back_position = parent.data().children.len() - 1; + let back_position = parent.data().children().len() - 1; let front_position = index + 1; ( back_position, @@ -60,7 +60,7 @@ impl<'a> Iterator for FollowingSiblings<'a> { .parent .as_ref()? .data() - .children + .children() .get(self.front_position)?; self.front_position += 1; Some(*child) @@ -86,7 +86,7 @@ impl<'a> DoubleEndedIterator for FollowingSiblings<'a> { .parent .as_ref()? .data() - .children + .children() .get(self.back_position)?; self.back_position -= 1; Some(*child) @@ -138,7 +138,7 @@ impl<'a> Iterator for PrecedingSiblings<'a> { .parent .as_ref()? .data() - .children + .children() .get(self.front_position)?; if !self.done { self.front_position -= 1; @@ -166,7 +166,7 @@ impl<'a> DoubleEndedIterator for PrecedingSiblings<'a> { .parent .as_ref()? .data() - .children + .children() .get(self.back_position)?; self.back_position += 1; Some(*child) diff --git a/consumer/src/lib.rs b/consumer/src/lib.rs index 2881b534c..6edd1b0a6 100644 --- a/consumer/src/lib.rs +++ b/consumer/src/lib.rs @@ -20,10 +20,11 @@ pub use text::{ #[cfg(test)] mod tests { - use accesskit::kurbo::{Affine, Rect, Vec2}; - use accesskit::{ActionHandler, ActionRequest, Node, NodeId, Role, Tree, TreeUpdate}; + use accesskit::{ + ActionHandler, ActionRequest, Affine, NodeBuilder, NodeClassSet, NodeId, Rect, Role, Tree, + TreeUpdate, Vec2, + }; use std::num::NonZeroU128; - use std::sync::Arc; use crate::FilterResult; @@ -50,93 +51,90 @@ mod tests { } pub fn test_tree() -> crate::tree::Tree { - let root = Arc::new(Node { - role: Role::RootWebArea, - children: vec![ + let mut classes = NodeClassSet::new(); + let root = { + let mut builder = NodeBuilder::new(Role::RootWebArea); + builder.set_children(vec![ PARAGRAPH_0_ID, PARAGRAPH_1_IGNORED_ID, PARAGRAPH_2_ID, PARAGRAPH_3_IGNORED_ID, - ], - ..Default::default() - }); - let paragraph_0 = Arc::new(Node { - role: Role::Paragraph, - children: vec![STATIC_TEXT_0_0_IGNORED_ID], - ..Default::default() - }); - let static_text_0_0_ignored = Arc::new(Node { - role: Role::StaticText, - name: Some("static_text_0_0_ignored".into()), - ..Default::default() - }); - let paragraph_1_ignored = Arc::new(Node { - role: Role::Paragraph, - transform: Some(Box::new(Affine::translate(Vec2::new(10.0, 40.0)))), - bounds: Some(Rect { + ]); + builder.build(&mut classes) + }; + let paragraph_0 = { + let mut builder = NodeBuilder::new(Role::Paragraph); + builder.set_children(vec![STATIC_TEXT_0_0_IGNORED_ID]); + builder.build(&mut classes) + }; + let static_text_0_0_ignored = { + let mut builder = NodeBuilder::new(Role::StaticText); + builder.set_name("static_text_0_0_ignored"); + builder.build(&mut classes) + }; + let paragraph_1_ignored = { + let mut builder = NodeBuilder::new(Role::Paragraph); + builder.set_transform(Affine::translate(Vec2::new(10.0, 40.0))); + builder.set_bounds(Rect { x0: 0.0, y0: 0.0, x1: 800.0, y1: 40.0, - }), - children: vec![STATIC_TEXT_1_0_ID], - ..Default::default() - }); - let static_text_1_0 = Arc::new(Node { - role: Role::StaticText, - bounds: Some(Rect { + }); + builder.set_children(vec![STATIC_TEXT_1_0_ID]); + builder.build(&mut classes) + }; + let static_text_1_0 = { + let mut builder = NodeBuilder::new(Role::StaticText); + builder.set_bounds(Rect { x0: 10.0, y0: 10.0, x1: 90.0, y1: 30.0, - }), - name: Some("static_text_1_0".into()), - ..Default::default() - }); - let paragraph_2 = Arc::new(Node { - role: Role::Paragraph, - children: vec![STATIC_TEXT_2_0_ID], - ..Default::default() - }); - let static_text_2_0 = Arc::new(Node { - role: Role::StaticText, - name: Some("static_text_2_0".into()), - ..Default::default() - }); - let paragraph_3_ignored = Arc::new(Node { - role: Role::Paragraph, - children: vec![ + }); + builder.set_name("static_text_1_0"); + builder.build(&mut classes) + }; + let paragraph_2 = { + let mut builder = NodeBuilder::new(Role::Paragraph); + builder.set_children(vec![STATIC_TEXT_2_0_ID]); + builder.build(&mut classes) + }; + let static_text_2_0 = { + let mut builder = NodeBuilder::new(Role::StaticText); + builder.set_name("static_text_2_0"); + builder.build(&mut classes) + }; + let paragraph_3_ignored = { + let mut builder = NodeBuilder::new(Role::Paragraph); + builder.set_children(vec![ EMPTY_CONTAINER_3_0_IGNORED_ID, LINK_3_1_IGNORED_ID, BUTTON_3_2_ID, EMPTY_CONTAINER_3_3_IGNORED_ID, - ], - ..Default::default() - }); - let empty_container_3_0_ignored = Arc::new(Node { - role: Role::GenericContainer, - ..Default::default() - }); - let link_3_1_ignored = Arc::new(Node { - role: Role::Link, - children: vec![STATIC_TEXT_3_1_0_ID], - linked: true, - ..Default::default() - }); - let static_text_3_1_0 = Arc::new(Node { - role: Role::StaticText, - name: Some("static_text_3_1_0".into()), - ..Default::default() - }); - let button_3_2 = Arc::new(Node { - role: Role::Button, - name: Some("button_3_2".into()), - ..Default::default() - }); - let empty_container_3_3_ignored = Arc::new(Node { - role: Role::GenericContainer, - ..Default::default() - }); + ]); + builder.build(&mut classes) + }; + let empty_container_3_0_ignored = + NodeBuilder::new(Role::GenericContainer).build(&mut classes); + let link_3_1_ignored = { + let mut builder = NodeBuilder::new(Role::Link); + builder.set_children(vec![STATIC_TEXT_3_1_0_ID]); + builder.set_linked(); + builder.build(&mut classes) + }; + let static_text_3_1_0 = { + let mut builder = NodeBuilder::new(Role::StaticText); + builder.set_name("static_text_3_1_0"); + builder.build(&mut classes) + }; + let button_3_2 = { + let mut builder = NodeBuilder::new(Role::Button); + builder.set_name("button_3_2"); + builder.build(&mut classes) + }; + let empty_container_3_3_ignored = + NodeBuilder::new(Role::GenericContainer).build(&mut classes); let initial_update = TreeUpdate { nodes: vec![ (ROOT_ID, root), diff --git a/consumer/src/node.rs b/consumer/src/node.rs index c03e3e526..c66089945 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -8,11 +8,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE.chromium file. -use std::{iter::FusedIterator, ops::Deref, sync::Arc}; +use std::{iter::FusedIterator, ops::Deref}; -use accesskit::kurbo::{Affine, Point, Rect}; use accesskit::{ - Action, CheckedState, DefaultActionVerb, Live, Node as NodeData, NodeId, Role, TextSelection, + Action, Affine, CheckedState, DefaultActionVerb, Live, Node as NodeData, NodeId, Point, Rect, + Role, TextSelection, }; use crate::iterators::{ @@ -28,7 +28,7 @@ pub(crate) struct ParentAndIndex(pub(crate) NodeId, pub(crate) usize); pub struct NodeState { pub(crate) id: NodeId, pub(crate) parent_and_index: Option, - pub(crate) data: Arc, + pub(crate) data: NodeData, } #[derive(Copy, Clone)] @@ -62,9 +62,7 @@ impl<'a> Node<'a> { impl NodeState { pub fn is_focusable(&self) -> bool { - // TBD: Is it ever safe to imply this on a node that doesn't explicitly - // specify it? - self.data().focusable + self.supports_action(Action::Focus) } } @@ -118,7 +116,7 @@ impl NodeState { + FusedIterator + '_ { let data = &self.data; - data.children.iter().copied() + data.children().iter().copied() } } @@ -250,9 +248,8 @@ impl NodeState { /// transform, without taking into account transforms on ancestors. pub fn direct_transform(&self) -> Affine { self.data() - .transform - .as_ref() - .map_or(Affine::IDENTITY, |t| **t) + .transform() + .map_or(Affine::IDENTITY, |value| *value) } } @@ -281,7 +278,7 @@ impl<'a> Node<'a> { impl NodeState { pub fn raw_bounds(&self) -> Option { - self.data().bounds + self.data().bounds() } } @@ -352,22 +349,22 @@ impl NodeState { } pub fn role(&self) -> Role { - self.data().role + self.data().role() } pub fn is_hidden(&self) -> bool { - self.data().hidden + self.data().is_hidden() } pub fn is_disabled(&self) -> bool { - self.data().disabled + self.data().is_disabled() } pub fn is_read_only(&self) -> bool { let data = self.data(); - if data.read_only { + if data.is_read_only() { true - } else if !data.editable { + } else if !data.is_editable() { false } else { self.should_have_read_only_state_by_default() || !self.is_read_only_supported() @@ -379,35 +376,35 @@ impl NodeState { } pub fn checked_state(&self) -> Option { - self.data().checked_state + self.data().checked_state() } pub fn value(&self) -> Option<&str> { - self.data().value.as_deref() + self.data().value() } pub fn numeric_value(&self) -> Option { - self.data().numeric_value + self.data().numeric_value() } pub fn min_numeric_value(&self) -> Option { - self.data().min_numeric_value + self.data().min_numeric_value() } pub fn max_numeric_value(&self) -> Option { - self.data().max_numeric_value + self.data().max_numeric_value() } pub fn numeric_value_step(&self) -> Option { - self.data().numeric_value_step + self.data().numeric_value_step() } pub fn numeric_value_jump(&self) -> Option { - self.data().numeric_value_jump + self.data().numeric_value_jump() } pub fn is_text_field(&self) -> bool { - self.is_atomic_text_field() || self.data().nonatomic_text_field_root + self.is_atomic_text_field() || self.data().is_nonatomic_text_field_root() } pub fn is_atomic_text_field(&self) -> bool { @@ -421,22 +418,22 @@ impl NodeState { // treat them as non-atomic. match self.role() { Role::SearchBox | Role::TextField | Role::TextFieldWithComboBox => { - !self.data().nonatomic_text_field_root + !self.data().is_nonatomic_text_field_root() } _ => false, } } pub fn is_multiline(&self) -> bool { - self.data().multiline + self.data().is_multiline() } pub fn is_protected(&self) -> bool { - self.data().protected + self.data().is_protected() } pub fn default_action_verb(&self) -> Option { - self.data().default_action_verb + self.data().default_action_verb() } // When probing for supported actions as the next several functions do, @@ -467,7 +464,7 @@ impl NodeState { } pub fn supports_expand_collapse(&self) -> bool { - self.data().expanded.is_some() + self.data().is_expanded().is_some() } pub fn is_invocable(&self) -> bool { @@ -491,7 +488,7 @@ impl NodeState { // The future of the `Action` enum is undecided, so keep the following // function private for now. fn supports_action(&self, action: Action) -> bool { - self.data().actions.contains(action) + self.data().supports_action(action) } pub fn supports_increment(&self) -> bool { @@ -515,7 +512,7 @@ impl<'a> Node<'a> { pub fn labelled_by( &self, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { - let explicit = &self.state.data.labelled_by; + let explicit = &self.state.data.labelled_by(); if explicit.is_empty() && matches!(self.role(), Role::Button | Role::Link) { LabelledBy::FromDescendants(FilteredChildren::new(*self, &descendant_label_filter)) } else { @@ -527,7 +524,7 @@ impl<'a> Node<'a> { } pub fn name(&self) -> Option { - if let Some(name) = &self.data().name { + if let Some(name) = &self.data().name() { Some(name.to_string()) } else { let names = self @@ -596,18 +593,18 @@ impl NodeState { impl<'a> Node<'a> { pub fn live(&self) -> Live { self.data() - .live + .live() .unwrap_or_else(|| self.parent().map_or(Live::Off, |parent| parent.live())) } } impl NodeState { pub fn is_selected(&self) -> Option { - self.data().selected + self.data().is_selected() } pub fn raw_text_selection(&self) -> Option<&TextSelection> { - self.data().text_selection.as_ref() + self.data().text_selection() } } @@ -723,9 +720,8 @@ impl Deref for DetachedNode { #[cfg(test)] mod tests { - use accesskit::kurbo::{Point, Rect}; - use accesskit::{Node, NodeId, Role, Tree, TreeUpdate}; - use std::{num::NonZeroU128, sync::Arc}; + use accesskit::{NodeBuilder, NodeClassSet, NodeId, Point, Rect, Role, Tree, TreeUpdate}; + use std::num::NonZeroU128; use crate::tests::*; @@ -995,22 +991,17 @@ mod tests { #[test] fn no_name_or_labelled_by() { + let mut classes = NodeClassSet::new(); let update = TreeUpdate { nodes: vec![ - ( - NODE_ID_1, - Arc::new(Node { - role: Role::Window, - children: vec![NODE_ID_2], - ..Default::default() - }), - ), + (NODE_ID_1, { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_children(vec![NODE_ID_2]); + builder.build(&mut classes) + }), ( NODE_ID_2, - Arc::new(Node { - role: Role::Button, - ..Default::default() - }), + NodeBuilder::new(Role::Button).build(&mut classes), ), ], tree: Some(Tree::new(NODE_ID_1)), @@ -1027,48 +1018,34 @@ mod tests { const LABEL_1: &str = "Check email every"; const LABEL_2: &str = "minutes"; + let mut classes = NodeClassSet::new(); let update = TreeUpdate { nodes: vec![ - ( - NODE_ID_1, - Arc::new(Node { - role: Role::Window, - children: vec![NODE_ID_2, NODE_ID_3, NODE_ID_4, NODE_ID_5], - ..Default::default() - }), - ), - ( - NODE_ID_2, - Arc::new(Node { - role: Role::CheckBox, - labelled_by: vec![NODE_ID_3, NODE_ID_5], - ..Default::default() - }), - ), - ( - NODE_ID_3, - Arc::new(Node { - role: Role::StaticText, - name: Some(LABEL_1.into()), - ..Default::default() - }), - ), - ( - NODE_ID_4, - Arc::new(Node { - role: Role::CheckBox, - labelled_by: vec![NODE_ID_5], - ..Default::default() - }), - ), - ( - NODE_ID_5, - Arc::new(Node { - role: Role::StaticText, - name: Some(LABEL_2.into()), - ..Default::default() - }), - ), + (NODE_ID_1, { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_children(vec![NODE_ID_2, NODE_ID_3, NODE_ID_4, NODE_ID_5]); + builder.build(&mut classes) + }), + (NODE_ID_2, { + let mut builder = NodeBuilder::new(Role::CheckBox); + builder.set_labelled_by(vec![NODE_ID_3, NODE_ID_5]); + builder.build(&mut classes) + }), + (NODE_ID_3, { + let mut builder = NodeBuilder::new(Role::StaticText); + builder.set_name(LABEL_1); + builder.build(&mut classes) + }), + (NODE_ID_4, { + let mut builder = NodeBuilder::new(Role::TextField); + builder.push_labelled_by(NODE_ID_5); + builder.build(&mut classes) + }), + (NODE_ID_5, { + let mut builder = NodeBuilder::new(Role::StaticText); + builder.set_name(LABEL_2); + builder.build(&mut classes) + }), ], tree: Some(Tree::new(NODE_ID_1)), focus: None, @@ -1089,56 +1066,39 @@ mod tests { const BUTTON_LABEL: &str = "Play"; const LINK_LABEL: &str = "Watch in browser"; + let mut classes = NodeClassSet::new(); let update = TreeUpdate { nodes: vec![ - ( - NODE_ID_1, - Arc::new(Node { - role: Role::Window, - children: vec![NODE_ID_2, NODE_ID_4], - ..Default::default() - }), - ), - ( - NODE_ID_2, - Arc::new(Node { - role: Role::Button, - children: vec![NODE_ID_3], - ..Default::default() - }), - ), - ( - NODE_ID_3, - Arc::new(Node { - role: Role::Image, - name: Some(BUTTON_LABEL.into()), - ..Default::default() - }), - ), - ( - NODE_ID_4, - Arc::new(Node { - role: Role::Link, - children: vec![NODE_ID_5], - ..Default::default() - }), - ), - ( - NODE_ID_5, - Arc::new(Node { - role: Role::GenericContainer, - children: vec![NODE_ID_6], - ..Default::default() - }), - ), - ( - NODE_ID_6, - Arc::new(Node { - role: Role::StaticText, - name: Some(LINK_LABEL.into()), - ..Default::default() - }), - ), + (NODE_ID_1, { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_children(vec![NODE_ID_2, NODE_ID_4]); + builder.build(&mut classes) + }), + (NODE_ID_2, { + let mut builder = NodeBuilder::new(Role::Button); + builder.push_child(NODE_ID_3); + builder.build(&mut classes) + }), + (NODE_ID_3, { + let mut builder = NodeBuilder::new(Role::Image); + builder.set_name(BUTTON_LABEL); + builder.build(&mut classes) + }), + (NODE_ID_4, { + let mut builder = NodeBuilder::new(Role::Link); + builder.push_child(NODE_ID_5); + builder.build(&mut classes) + }), + (NODE_ID_5, { + let mut builder = NodeBuilder::new(Role::GenericContainer); + builder.push_child(NODE_ID_6); + builder.build(&mut classes) + }), + (NODE_ID_6, { + let mut builder = NodeBuilder::new(Role::StaticText); + builder.set_name(LINK_LABEL); + builder.build(&mut classes) + }), ], tree: Some(Tree::new(NODE_ID_1)), focus: None, diff --git a/consumer/src/text.rs b/consumer/src/text.rs index 808fa570b..0e89d728b 100644 --- a/consumer/src/text.rs +++ b/consumer/src/text.rs @@ -3,8 +3,7 @@ // 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 accesskit::{NodeId, Point, Rect, Role, TextDirection, TextPosition as WeakPosition}; use std::{cmp::Ordering, iter::FusedIterator}; use crate::{FilterResult, Node, TreeState}; @@ -22,7 +21,7 @@ impl<'a> InnerPosition<'a> { return None; } let character_index = weak.character_index; - if character_index > node.data().character_lengths.len() { + if character_index > node.data().character_lengths().len() { return None; } Some(Self { @@ -33,7 +32,7 @@ impl<'a> InnerPosition<'a> { fn is_word_start(&self) -> bool { let mut total_length = 0usize; - for length in self.node.data().word_lengths.iter() { + for length in self.node.data().word_lengths().iter() { if total_length == self.character_index { return true; } @@ -47,15 +46,15 @@ impl<'a> InnerPosition<'a> { } fn is_line_start(&self) -> bool { - self.is_box_start() && self.node.data().previous_on_line.is_none() + 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() + 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() + self.is_box_end() && self.node.data().next_on_line().is_none() } fn is_paragraph_end(&self) -> bool { @@ -97,7 +96,7 @@ impl<'a> InnerPosition<'a> { if let Some(node) = self.node.preceding_inline_text_boxes(root_node).next() { return Self { node, - character_index: node.data().character_lengths.len(), + character_index: node.data().character_lengths().len(), }; } } @@ -114,7 +113,7 @@ impl<'a> InnerPosition<'a> { fn previous_word_start(&self) -> Self { let mut total_length_before = 0usize; - for length in self.node.data().word_lengths.iter() { + 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; @@ -129,7 +128,7 @@ impl<'a> InnerPosition<'a> { fn word_end(&self) -> Self { let mut total_length = 0usize; - for length in self.node.data().word_lengths.iter() { + for length in self.node.data().word_lengths().iter() { total_length += *length as usize; if total_length > self.character_index { break; @@ -143,7 +142,7 @@ impl<'a> InnerPosition<'a> { fn line_start(&self) -> Self { let mut node = self.node; - while let Some(id) = node.data().previous_on_line { + while let Some(id) = node.data().previous_on_line() { node = node.tree_state.node_by_id(id).unwrap(); } Self { @@ -154,12 +153,12 @@ impl<'a> InnerPosition<'a> { fn line_end(&self) -> Self { let mut node = self.node; - while let Some(id) = node.data().next_on_line { + 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(), + character_index: node.data().character_lengths().len(), } } @@ -234,7 +233,7 @@ impl<'a> Position<'a> { for node in self.root_node.inline_text_boxes() { let node_text = node.value().unwrap(); if node.id() == self.inner.node.id() { - let character_lengths = &node.data().character_lengths; + let character_lengths = node.data().character_lengths(); let slice_end = character_lengths[..self.inner.character_index] .iter() .copied() @@ -522,7 +521,7 @@ impl<'a> Range<'a> { pub fn text(&self) -> String { let mut result = String::new(); self.walk::<_, ()>(|node| { - let character_lengths = &node.data().character_lengths; + let character_lengths = node.data().character_lengths(); let start_index = if node.id() == self.start.node.id() { self.start.character_index } else { @@ -568,31 +567,31 @@ impl<'a> Range<'a> { pub fn bounding_boxes(&self) -> Vec { let mut result = Vec::new(); self.walk(|node| { - let mut rect = match &node.data().bounds { - Some(rect) => *rect, + let mut rect = match node.data().bounds() { + Some(rect) => rect, None => { return Some(Vec::new()); } }; - let positions = match &node.data().character_positions { + let positions = match node.data().character_positions() { Some(positions) => positions, None => { return Some(Vec::new()); } }; - let widths = match &node.data().character_widths { + let widths = match node.data().character_widths() { Some(widths) => widths, None => { return Some(Vec::new()); } }; - let direction = match node.data().text_direction { + let direction = match node.data().text_direction() { Some(direction) => direction, None => { return Some(Vec::new()); } }; - let character_lengths = &node.data().character_lengths; + let character_lengths = node.data().character_lengths(); let start_index = if node.id() == self.start.node.id() { self.start.character_index } else { @@ -762,21 +761,21 @@ fn text_node_filter(root_id: NodeId, node: &Node) -> FilterResult { 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 { + let rect = node.data().bounds().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 { + let widths = match node.data().character_widths() { Some(widths) => widths, None => { return 0; } }; - let direction = match node.data().text_direction { + let direction = match node.data().text_direction() { Some(direction) => direction, None => { return 0; @@ -842,7 +841,7 @@ impl<'a> Node<'a> { let node = self.inline_text_boxes().next_back().unwrap(); InnerPosition { node, - character_index: node.data().character_lengths.len(), + character_index: node.data().character_lengths().len(), } } @@ -853,11 +852,11 @@ impl<'a> Node<'a> { } pub fn has_text_selection(&self) -> bool { - self.data().text_selection.is_some() + self.data().text_selection().is_some() } pub fn text_selection(&self) -> Option { - self.data().text_selection.map(|selection| { + 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) @@ -865,7 +864,7 @@ impl<'a> Node<'a> { } pub fn text_selection_focus(&self) -> Option { - self.data().text_selection.map(|selection| { + self.data().text_selection().map(|selection| { let focus = InnerPosition::upgrade(self.tree_state, selection.focus).unwrap(); Position { root_node: *self, @@ -908,7 +907,7 @@ impl<'a> Node<'a> { 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 { + 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 @@ -931,7 +930,7 @@ impl<'a> Node<'a> { root_node: *self, inner: InnerPosition { node, - character_index: node.data().character_lengths.len(), + character_index: node.data().character_lengths().len(), }, }; } @@ -979,7 +978,7 @@ impl<'a> Node<'a> { let mut utf8_length = 0usize; let mut utf16_length = 0usize; for (character_index, utf8_char_length) in - node.data().character_lengths.iter().enumerate() + node.data().character_lengths().iter().enumerate() { let new_utf8_length = utf8_length + (*utf8_char_length as usize); let char_str = &node_text[utf8_length..new_utf8_length]; @@ -1013,9 +1012,8 @@ impl<'a> Node<'a> { #[cfg(test)] mod tests { - use accesskit::kurbo::{Point, Rect}; - use accesskit::{NodeId, TextSelection}; - use std::{num::NonZeroU128, sync::Arc}; + use accesskit::{NodeId, Point, Rect, TextSelection}; + use std::num::NonZeroU128; use crate::tests::NullActionHandler; @@ -1030,233 +1028,183 @@ mod tests { // 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}; + use accesskit::{ + Action, Affine, NodeBuilder, NodeClassSet, Role, TextDirection, Tree, TreeUpdate, + }; + let mut classes = NodeClassSet::new(); 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, - }), - // The non-breaking space in the following text - // is in an arbitrary spot; its only purpose - // is to test conversion between UTF-8 and UTF-16 - // indices. - value: Some("This paragraph is\u{a0}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, 2, 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, - }), - // Use an arbitrary emoji that encodes to two - // UTF-16 code units to fully test conversion between - // UTF-8, UTF-16, and character indices. - value: Some("Last non-blank line\u{1f60a}\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, 4, 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() - }), - ), + (NODE_ID_1, { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_transform(Affine::scale(1.5)); + builder.set_children(vec![NODE_ID_2]); + builder.build(&mut classes) + }), + (NODE_ID_2, { + let mut builder = NodeBuilder::new(Role::TextField); + builder.set_bounds(Rect { + x0: 8.0, + y0: 31.666664123535156, + x1: 296.0, + y1: 123.66666412353516, + }); + builder.set_children(vec![ + NODE_ID_3, NODE_ID_4, NODE_ID_5, NODE_ID_6, NODE_ID_7, NODE_ID_8, + ]); + builder.add_action(Action::Focus); + if let Some(selection) = selection { + builder.set_text_selection(selection); + } + builder.build(&mut classes) + }), + (NODE_ID_3, { + let mut builder = NodeBuilder::new(Role::InlineTextBox); + builder.set_bounds(Rect { + x0: 12.0, + y0: 33.666664123535156, + x1: 290.9189147949219, + y1: 48.33333206176758, + }); + // The non-breaking space in the following text + // is in an arbitrary spot; its only purpose + // is to test conversion between UTF-8 and UTF-16 + // indices. + builder.set_value("This paragraph is\u{a0}long enough to wrap "); + builder.set_text_direction(TextDirection::LeftToRight); + builder.set_character_lengths([ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + ]); + builder.set_character_positions([ + 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, + ]); + builder.set_character_widths([ + 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, + ]); + builder.set_word_lengths([5, 10, 3, 5, 7, 3, 5]); + builder.build(&mut classes) + }), + (NODE_ID_4, { + let mut builder = NodeBuilder::new(Role::InlineTextBox); + builder.set_bounds(Rect { + x0: 12.0, + y0: 48.33333206176758, + x1: 129.5855712890625, + y1: 63.0, + }); + builder.set_value("to another line.\n"); + builder.set_text_direction(TextDirection::LeftToRight); + builder + .set_character_lengths([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + builder.set_character_positions([ + 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, + ]); + builder.set_character_widths([ + 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, + ]); + builder.set_word_lengths([3, 8, 6]); + builder.build(&mut classes) + }), + (NODE_ID_5, { + let mut builder = NodeBuilder::new(Role::InlineTextBox); + builder.set_bounds(Rect { + x0: 12.0, + y0: 63.0, + x1: 144.25222778320313, + y1: 77.66666412353516, + }); + builder.set_value("Another paragraph.\n"); + builder.set_text_direction(TextDirection::LeftToRight); + builder.set_character_lengths([ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + ]); + builder.set_character_positions([ + 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, + ]); + builder.set_character_widths([ + 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, + ]); + builder.set_word_lengths([8, 11]); + builder.build(&mut classes) + }), + (NODE_ID_6, { + let mut builder = NodeBuilder::new(Role::InlineTextBox); + builder.set_bounds(Rect { + x0: 12.0, + y0: 77.66666412353516, + x1: 12.0, + y1: 92.33332824707031, + }); + builder.set_value("\n"); + builder.set_text_direction(TextDirection::LeftToRight); + builder.set_character_lengths([1]); + builder.set_character_positions([0.0]); + builder.set_character_widths([0.0]); + builder.set_word_lengths([1]); + builder.build(&mut classes) + }), + (NODE_ID_7, { + let mut builder = NodeBuilder::new(Role::InlineTextBox); + builder.set_bounds(Rect { + x0: 12.0, + y0: 92.33332824707031, + x1: 158.9188995361328, + y1: 107.0, + }); + // Use an arbitrary emoji that encodes to two + // UTF-16 code units to fully test conversion between + // UTF-8, UTF-16, and character indices. + builder.set_value("Last non-blank line\u{1f60a}\n"); + builder.set_text_direction(TextDirection::LeftToRight); + builder.set_character_lengths([ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, + ]); + builder.set_character_positions([ + 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, + ]); + builder.set_character_widths([ + 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, + ]); + builder.set_word_lengths([5, 4, 6, 6]); + builder.build(&mut classes) + }), + (NODE_ID_8, { + let mut builder = NodeBuilder::new(Role::InlineTextBox); + builder.set_bounds(Rect { + x0: 12.0, + y0: 107.0, + x1: 12.0, + y1: 121.66666412353516, + }); + builder.set_value(""); + builder.set_text_direction(TextDirection::LeftToRight); + builder.set_character_lengths([]); + builder.set_character_positions([]); + builder.set_character_widths([]); + builder.set_word_lengths([0]); + builder.build(&mut classes) + }), ], tree: Some(Tree::new(NODE_ID_1)), focus: Some(NODE_ID_2), diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 197ed116f..8a4c2bf6c 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -3,16 +3,14 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::kurbo::Point; use accesskit::{ - Action, ActionData, ActionHandler, ActionRequest, Live, Node as NodeData, NodeId, + Action, ActionData, ActionHandler, ActionRequest, Live, Node as NodeData, NodeId, Point, TextSelection, Tree as TreeData, TreeUpdate, }; use parking_lot::{RwLock, RwLockWriteGuard}; use std::{ collections::{HashMap, HashSet}, ops::Deref, - sync::Arc, }; use crate::{ @@ -84,7 +82,7 @@ impl State { changes: &mut Option<&mut InternalChanges>, parent_and_index: Option, id: NodeId, - data: Arc, + data: NodeData, ) { let state = NodeState { id, @@ -101,7 +99,7 @@ impl State { orphans.remove(&node_id); let mut seen_child_ids = HashSet::new(); - for (child_index, child_id) in node_data.children.iter().enumerate() { + for (child_index, child_id) in node_data.children().iter().enumerate() { assert!(!seen_child_ids.contains(child_id)); orphans.remove(child_id); let parent_and_index = ParentAndIndex(node_id, child_index); @@ -127,7 +125,7 @@ impl State { if node_id == root { node_state.parent_and_index = None } - for child_id in node_state.data.children.iter() { + for child_id in node_state.data.children().iter() { if !seen_child_ids.contains(child_id) { orphans.insert(*child_id); } @@ -177,7 +175,7 @@ impl State { ) { to_remove.insert(id); let node = nodes.get(&id).unwrap(); - for child_id in node.data.children.iter() { + for child_id in node.data.children().iter() { traverse_orphan(nodes, to_remove, *child_id); } } @@ -209,11 +207,11 @@ impl State { pub fn serialize(&self) -> TreeUpdate { let mut nodes = Vec::new(); - fn traverse(state: &State, nodes: &mut Vec<(NodeId, Arc)>, id: NodeId) { + fn traverse(state: &State, nodes: &mut Vec<(NodeId, NodeData)>, id: NodeId) { let node = state.nodes.get(&id).unwrap(); - nodes.push((id, Arc::clone(&node.data))); + nodes.push((id, node.data.clone())); - for child_id in node.data.children.iter() { + for child_id in node.data.children().iter() { traverse(state, nodes, *child_id); } } @@ -426,8 +424,8 @@ impl Tree { #[cfg(test)] mod tests { - use accesskit::{Node, NodeId, Role, Tree, TreeUpdate}; - use std::{num::NonZeroU128, sync::Arc}; + use accesskit::{NodeBuilder, NodeClassSet, NodeId, Role, Tree, TreeUpdate}; + use std::num::NonZeroU128; use crate::tests::NullActionHandler; @@ -437,13 +435,11 @@ mod tests { #[test] fn init_tree_with_root_node() { + let mut classes = NodeClassSet::new(); let update = TreeUpdate { nodes: vec![( NODE_ID_1, - Arc::new(Node { - role: Role::Window, - ..Node::default() - }), + NodeBuilder::new(Role::Window).build(&mut classes), )], tree: Some(Tree::new(NODE_ID_1)), focus: None, @@ -456,29 +452,21 @@ mod tests { #[test] fn root_node_has_children() { + let mut classes = NodeClassSet::new(); let update = TreeUpdate { nodes: vec![ - ( - NODE_ID_1, - Arc::new(Node { - role: Role::Window, - children: vec![NODE_ID_2, NODE_ID_3], - ..Default::default() - }), - ), + (NODE_ID_1, { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_children(vec![NODE_ID_2, NODE_ID_3]); + builder.build(&mut classes) + }), ( NODE_ID_2, - Arc::new(Node { - role: Role::Button, - ..Default::default() - }), + NodeBuilder::new(Role::Button).build(&mut classes), ), ( NODE_ID_3, - Arc::new(Node { - role: Role::Button, - ..Default::default() - }), + NodeBuilder::new(Role::Button).build(&mut classes), ), ], tree: Some(Tree::new(NODE_ID_1)), @@ -499,12 +487,10 @@ mod tests { #[test] fn add_child_to_root_node() { - let root_node = Node { - role: Role::Window, - ..Default::default() - }; + let mut classes = NodeClassSet::new(); + let root_builder = NodeBuilder::new(Role::Window); let first_update = TreeUpdate { - nodes: vec![(NODE_ID_1, Arc::new(root_node.clone()))], + nodes: vec![(NODE_ID_1, root_builder.clone().build(&mut classes))], tree: Some(Tree::new(NODE_ID_1)), focus: None, }; @@ -512,19 +498,14 @@ mod tests { assert_eq!(0, tree.read().root().children().count()); let second_update = TreeUpdate { nodes: vec![ - ( - NODE_ID_1, - Arc::new(Node { - children: vec![NODE_ID_2], - ..root_node - }), - ), + (NODE_ID_1, { + let mut builder = root_builder; + builder.push_child(NODE_ID_2); + builder.build(&mut classes) + }), ( NODE_ID_2, - Arc::new(Node { - role: Role::RootWebArea, - ..Default::default() - }), + NodeBuilder::new(Role::RootWebArea).build(&mut classes), ), ], tree: None, @@ -547,8 +528,8 @@ mod tests { } fn node_updated(&mut self, old_node: &crate::DetachedNode, new_node: &crate::Node) { if new_node.id() == NODE_ID_1 - && old_node.data().children == vec![] - && new_node.data().children == vec![NODE_ID_2] + && old_node.data().children().is_empty() + && new_node.data().children() == [NODE_ID_2] { self.got_updated_root_node = true; return; @@ -588,25 +569,18 @@ mod tests { #[test] fn remove_child_from_root_node() { - let root_node = Node { - role: Role::Window, - ..Default::default() - }; + let mut classes = NodeClassSet::new(); + let root_builder = NodeBuilder::new(Role::Window); let first_update = TreeUpdate { nodes: vec![ - ( - NODE_ID_1, - Arc::new(Node { - children: vec![NODE_ID_2], - ..root_node.clone() - }), - ), + (NODE_ID_1, { + let mut builder = root_builder.clone(); + builder.push_child(NODE_ID_2); + builder.build(&mut classes) + }), ( NODE_ID_2, - Arc::new(Node { - role: Role::RootWebArea, - ..Default::default() - }), + NodeBuilder::new(Role::RootWebArea).build(&mut classes), ), ], tree: Some(Tree::new(NODE_ID_1)), @@ -615,7 +589,7 @@ mod tests { let tree = super::Tree::new(first_update, Box::new(NullActionHandler {})); assert_eq!(1, tree.read().root().children().count()); let second_update = TreeUpdate { - nodes: vec![(NODE_ID_1, Arc::new(root_node))], + nodes: vec![(NODE_ID_1, root_builder.build(&mut classes))], tree: None, focus: None, }; @@ -632,8 +606,8 @@ mod tests { } fn node_updated(&mut self, old_node: &crate::DetachedNode, new_node: &crate::Node) { if new_node.id() == NODE_ID_1 - && old_node.data().children == vec![NODE_ID_2] - && new_node.data().children == vec![] + && old_node.data().children() == [NODE_ID_2] + && new_node.data().children().is_empty() { self.got_updated_root_node = true; return; @@ -672,29 +646,21 @@ mod tests { #[test] fn move_focus_between_siblings() { + let mut classes = NodeClassSet::new(); let first_update = TreeUpdate { nodes: vec![ - ( - NODE_ID_1, - Arc::new(Node { - role: Role::Window, - children: vec![NODE_ID_2, NODE_ID_3], - ..Default::default() - }), - ), + (NODE_ID_1, { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_children(vec![NODE_ID_2, NODE_ID_3]); + builder.build(&mut classes) + }), ( NODE_ID_2, - Arc::new(Node { - role: Role::Button, - ..Default::default() - }), + NodeBuilder::new(Role::Button).build(&mut classes), ), ( NODE_ID_3, - Arc::new(Node { - role: Role::Button, - ..Default::default() - }), + NodeBuilder::new(Role::Button).build(&mut classes), ), ], tree: Some(Tree::new(NODE_ID_1)), @@ -774,27 +740,20 @@ mod tests { #[test] fn update_node() { - let child_node = Node { - role: Role::Button, - ..Default::default() - }; + let mut classes = NodeClassSet::new(); + let child_builder = NodeBuilder::new(Role::Button); let first_update = TreeUpdate { nodes: vec![ - ( - NODE_ID_1, - Arc::new(Node { - role: Role::Window, - children: vec![NODE_ID_2], - ..Default::default() - }), - ), - ( - NODE_ID_2, - Arc::new(Node { - name: Some("foo".into()), - ..child_node.clone() - }), - ), + (NODE_ID_1, { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_children(vec![NODE_ID_2]); + builder.build(&mut classes) + }), + (NODE_ID_2, { + let mut builder = child_builder.clone(); + builder.set_name("foo"); + builder.build(&mut classes) + }), ], tree: Some(Tree::new(NODE_ID_1)), focus: None, @@ -805,13 +764,11 @@ mod tests { tree.read().node_by_id(NODE_ID_2).unwrap().name() ); let second_update = TreeUpdate { - nodes: vec![( - NODE_ID_2, - Arc::new(Node { - name: Some("bar".into()), - ..child_node - }), - )], + nodes: vec![(NODE_ID_2, { + let mut builder = child_builder; + builder.set_name("bar"); + builder.build(&mut classes) + })], tree: None, focus: None, }; diff --git a/platforms/macos/src/util.rs b/platforms/macos/src/util.rs index 39cd25de3..4de7b306a 100644 --- a/platforms/macos/src/util.rs +++ b/platforms/macos/src/util.rs @@ -3,7 +3,7 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::kurbo::{Point, Rect}; +use accesskit::{Point, Rect}; use accesskit_consumer::{Node, TextPosition, TextRange}; use objc2::foundation::{NSPoint, NSRange, NSRect, NSSize}; diff --git a/platforms/unix/src/adapter.rs b/platforms/unix/src/adapter.rs index d9fca322d..a95071ddd 100644 --- a/platforms/unix/src/adapter.rs +++ b/platforms/unix/src/adapter.rs @@ -14,7 +14,7 @@ use crate::{ node::{filter, filter_detached, NodeWrapper, PlatformNode, PlatformRootNode}, util::{AppContext, WindowBounds}, }; -use accesskit::{kurbo::Rect, ActionHandler, NodeId, Role, TreeUpdate}; +use accesskit::{ActionHandler, NodeId, Rect, Role, TreeUpdate}; use accesskit_consumer::{DetachedNode, FilterResult, Node, Tree, TreeChangeHandler, TreeState}; use async_channel::{Receiver, Sender}; use atspi::{Interface, InterfaceSet, State}; diff --git a/platforms/unix/src/atspi/mod.rs b/platforms/unix/src/atspi/mod.rs index fc85461d7..342f50de4 100644 --- a/platforms/unix/src/atspi/mod.rs +++ b/platforms/unix/src/atspi/mod.rs @@ -29,8 +29,8 @@ impl Rect { }; } -impl From for Rect { - fn from(value: accesskit::kurbo::Rect) -> Rect { +impl From for Rect { + fn from(value: accesskit::Rect) -> Rect { Rect { x: value.x0 as i32, y: value.y0 as i32, diff --git a/platforms/unix/src/node.rs b/platforms/unix/src/node.rs index 3baad926b..5ebbbcab5 100644 --- a/platforms/unix/src/node.rs +++ b/platforms/unix/src/node.rs @@ -15,10 +15,7 @@ use crate::{ }, util::{AppContext, WindowBounds}, }; -use accesskit::{ - kurbo::{Affine, Point, Rect}, - CheckedState, DefaultActionVerb, NodeId, Role, -}; +use accesskit::{Affine, CheckedState, DefaultActionVerb, NodeId, Point, Rect, Role}; use accesskit_consumer::{DetachedNode, FilterResult, Node, NodeState, Tree, TreeState}; use async_channel::Sender; use atspi::{ diff --git a/platforms/unix/src/util.rs b/platforms/unix/src/util.rs index 35c0fed5b..995b57a9b 100644 --- a/platforms/unix/src/util.rs +++ b/platforms/unix/src/util.rs @@ -4,7 +4,7 @@ // the LICENSE-MIT file), at your option. use crate::atspi::OwnedObjectAddress; -use accesskit::kurbo::{Point, Rect}; +use accesskit::{Point, Rect}; use atspi::CoordType; pub(crate) struct AppContext { diff --git a/platforms/windows/examples/hello_world.rs b/platforms/windows/examples/hello_world.rs index 228f7b5e3..6c72ab0e8 100644 --- a/platforms/windows/examples/hello_world.rs +++ b/platforms/windows/examples/hello_world.rs @@ -1,13 +1,12 @@ // Based on the create_window sample in windows-samples-rs. -use accesskit::kurbo::Rect; use accesskit::{ - Action, ActionHandler, ActionRequest, DefaultActionVerb, Live, Node, NodeId, Role, Tree, - TreeUpdate, + Action, ActionHandler, ActionRequest, DefaultActionVerb, Live, Node, NodeBuilder, NodeClassSet, + NodeId, Rect, Role, Tree, TreeUpdate, }; use accesskit_windows::UiaInitMarker; use once_cell::{sync::Lazy, unsync::OnceCell}; -use std::{cell::RefCell, convert::TryInto, num::NonZeroU128, sync::Arc}; +use std::{cell::RefCell, convert::TryInto, num::NonZeroU128}; use windows::{ core::*, Win32::{ @@ -43,7 +42,7 @@ const WINDOW_TITLE: &str = "Hello world"; const WINDOW_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(1) }); const BUTTON_1_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(2) }); const BUTTON_2_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(3) }); -const PRESSED_TEXT_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(4) }); +const ANNOUNCEMENT_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(4) }); const INITIAL_FOCUS: NodeId = BUTTON_1_ID; const BUTTON_1_RECT: Rect = Rect { @@ -63,45 +62,70 @@ const BUTTON_2_RECT: Rect = Rect { const SET_FOCUS_MSG: u32 = WM_USER; const DO_DEFAULT_ACTION_MSG: u32 = WM_USER + 1; -fn make_button(id: NodeId, name: &str) -> Arc { +fn build_button(id: NodeId, name: &str, classes: &mut NodeClassSet) -> Node { let rect = match id { BUTTON_1_ID => BUTTON_1_RECT, BUTTON_2_ID => BUTTON_2_RECT, _ => unreachable!(), }; - Arc::new(Node { - role: Role::Button, - bounds: Some(rect), - name: Some(name.into()), - focusable: true, - default_action_verb: Some(DefaultActionVerb::Click), - ..Default::default() - }) + let mut builder = NodeBuilder::new(Role::Button); + builder.set_bounds(rect); + builder.set_name(name); + builder.add_action(Action::Focus); + builder.set_default_action_verb(DefaultActionVerb::Click); + builder.build(classes) } -fn get_initial_state() -> TreeUpdate { - let root = Arc::new(Node { - role: Role::Window, - children: vec![BUTTON_1_ID, BUTTON_2_ID], - ..Default::default() - }); - let button_1 = make_button(BUTTON_1_ID, "Button 1"); - let button_2 = make_button(BUTTON_2_ID, "Button 2"); - TreeUpdate { - nodes: vec![ - (WINDOW_ID, root), - (BUTTON_1_ID, button_1), - (BUTTON_2_ID, button_2), - ], - tree: Some(Tree::new(WINDOW_ID)), - focus: None, - } +fn build_announcement(text: &str, classes: &mut NodeClassSet) -> Node { + let mut builder = NodeBuilder::new(Role::StaticText); + builder.set_name(text); + builder.set_live(Live::Polite); + builder.build(classes) } struct InnerWindowState { focus: NodeId, is_window_focused: bool, + announcement: Option, + node_classes: NodeClassSet, +} + +impl InnerWindowState { + fn focus(&self) -> Option { + self.is_window_focused.then_some(self.focus) + } + + fn build_root(&mut self) -> Node { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_children(vec![BUTTON_1_ID, BUTTON_2_ID]); + if self.announcement.is_some() { + builder.push_child(ANNOUNCEMENT_ID); + } + builder.build(&mut self.node_classes) + } + + fn build_initial_tree(&mut self) -> TreeUpdate { + let root = self.build_root(); + let button_1 = build_button(BUTTON_1_ID, "Button 1", &mut self.node_classes); + let button_2 = build_button(BUTTON_2_ID, "Button 2", &mut self.node_classes); + let mut result = TreeUpdate { + nodes: vec![ + (WINDOW_ID, root), + (BUTTON_1_ID, button_1), + (BUTTON_2_ID, button_2), + ], + tree: Some(Tree::new(WINDOW_ID)), + focus: self.focus(), + }; + if let Some(announcement) = &self.announcement { + result.nodes.push(( + ANNOUNCEMENT_ID, + build_announcement(announcement, &mut self.node_classes), + )); + } + result + } } struct WindowState { @@ -113,14 +137,9 @@ struct WindowState { impl WindowState { fn get_or_init_accesskit_adapter(&self, window: HWND) -> &accesskit_windows::Adapter { self.adapter.get_or_init(|| { - // Note: For this simple example, the initial tree is mostly static. - // In a real GUI framework, the initial tree would be dynamically - // generated by the framework based on application logic and data - // associated with the window. Here, only the focus is based on - // window state. - let mut initial_tree = get_initial_state(); - let inner_state = self.inner_state.borrow(); - initial_tree.focus = inner_state.is_window_focused.then_some(inner_state.focus); + let mut inner_state = self.inner_state.borrow_mut(); + let initial_tree = inner_state.build_initial_tree(); + drop(inner_state); let action_handler = Box::new(SimpleActionHandler { window }); accesskit_windows::Adapter::new( window, @@ -131,45 +150,25 @@ impl WindowState { }) } - fn press_button(&self, window: HWND, id: NodeId) { - // This is a pretty hacky way of adding or updating a node. - // A real GUI framework would have a consistent way - // of building nodes from underlying data. - // Also, this update isn't as lazy as it could be; - // we force the AccessKit tree to be initialized. - // This is expedient in this case, because that tree - // is the only place where the state of the announcement - // is stored. It's not a problem because we're really - // only concerned with testing lazy updates in the context - // of focus changes. - let adapter = self.get_or_init_accesskit_adapter(window); - let inner_state = self.inner_state.borrow(); - let is_window_focused = inner_state.is_window_focused; - let focus = inner_state.focus; - drop(inner_state); - let name = if id == BUTTON_1_ID { + fn press_button(&self, id: NodeId) { + let mut inner_state = self.inner_state.borrow_mut(); + let text = if id == BUTTON_1_ID { "You pressed button 1" } else { "You pressed button 2" }; - let node = Arc::new(Node { - role: Role::StaticText, - name: Some(name.into()), - live: Some(Live::Polite), - ..Default::default() - }); - let root = Arc::new(Node { - role: Role::Window, - children: vec![BUTTON_1_ID, BUTTON_2_ID, PRESSED_TEXT_ID], - ..Node::default() - }); - let update = TreeUpdate { - nodes: vec![(PRESSED_TEXT_ID, node), (WINDOW_ID, root)], - tree: None, - focus: is_window_focused.then_some(focus), - }; - let events = adapter.update(update); - events.raise(); + inner_state.announcement = Some(text.into()); + if let Some(adapter) = self.adapter.get() { + let announcement = build_announcement(text, &mut inner_state.node_classes); + let root = inner_state.build_root(); + let update = TreeUpdate { + nodes: vec![(ANNOUNCEMENT_ID, announcement), (WINDOW_ID, root)], + tree: None, + focus: inner_state.focus(), + }; + let events = adapter.update(update); + events.raise(); + } } } @@ -181,13 +180,13 @@ fn update_focus(window: HWND, is_window_focused: bool) { let window_state = unsafe { &*get_window_state(window) }; let mut inner_state = window_state.inner_state.borrow_mut(); inner_state.is_window_focused = is_window_focused; - let focus = inner_state.focus; + let focus = inner_state.focus(); drop(inner_state); if let Some(adapter) = window_state.adapter.get() { let events = adapter.update(TreeUpdate { nodes: vec![], tree: None, - focus: is_window_focused.then_some(focus), + focus, }); events.raise(); } @@ -237,6 +236,8 @@ extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: L let inner_state = RefCell::new(InnerWindowState { focus: initial_focus, is_window_focused: false, + announcement: None, + node_classes: NodeClassSet::new(), }); let state = Box::new(WindowState { uia_init_marker: UiaInitMarker::new(), @@ -298,7 +299,7 @@ extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: L VK_SPACE => { let window_state = unsafe { &*get_window_state(window) }; let id = window_state.inner_state.borrow().focus; - window_state.press_button(window, id); + window_state.press_button(id); LRESULT(0) } _ => unsafe { DefWindowProcW(window, message, wparam, lparam) }, @@ -322,7 +323,7 @@ extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: L let id = NodeId(id); if id == BUTTON_1_ID || id == BUTTON_2_ID { let window_state = unsafe { &*get_window_state(window) }; - window_state.press_button(window, id); + window_state.press_button(id); } } LRESULT(0) diff --git a/platforms/windows/src/node.rs b/platforms/windows/src/node.rs index e621f4216..d104ec7e5 100644 --- a/platforms/windows/src/node.rs +++ b/platforms/windows/src/node.rs @@ -10,8 +10,7 @@ #![allow(non_upper_case_globals)] -use accesskit::kurbo::Point; -use accesskit::{CheckedState, Live, NodeId, NodeIdContent, Role}; +use accesskit::{CheckedState, Live, NodeId, NodeIdContent, Point, Role}; use accesskit_consumer::{DetachedNode, FilterResult, Node, NodeState, Tree, TreeState}; use arrayvec::ArrayVec; use paste::paste; diff --git a/platforms/windows/src/tests/simple.rs b/platforms/windows/src/tests/simple.rs index 097fe797a..c8e66d3ef 100644 --- a/platforms/windows/src/tests/simple.rs +++ b/platforms/windows/src/tests/simple.rs @@ -3,9 +3,12 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use std::{convert::TryInto, num::NonZeroU128, sync::Arc}; +use std::{convert::TryInto, num::NonZeroU128}; -use accesskit::{ActionHandler, ActionRequest, Node, NodeId, Role, Tree, TreeUpdate}; +use accesskit::{ + Action, ActionHandler, ActionRequest, Node, NodeBuilder, NodeClassSet, NodeId, Role, Tree, + TreeUpdate, +}; use windows::{core::*, Win32::UI::Accessibility::*}; use super::*; @@ -16,23 +19,22 @@ const WINDOW_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(1) }); const BUTTON_1_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(2) }); const BUTTON_2_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(3) }); -fn make_button(name: &str) -> Arc { - Arc::new(Node { - role: Role::Button, - name: Some(name.into()), - focusable: true, - ..Default::default() - }) +fn make_button(name: &str, classes: &mut NodeClassSet) -> Node { + let mut builder = NodeBuilder::new(Role::Button); + builder.set_name(name); + builder.add_action(Action::Focus); + builder.build(classes) } fn get_initial_state() -> TreeUpdate { - let root = Arc::new(Node { - role: Role::Window, - children: vec![BUTTON_1_ID, BUTTON_2_ID], - ..Default::default() - }); - let button_1 = make_button("Button 1"); - let button_2 = make_button("Button 2"); + let mut classes = NodeClassSet::new(); + let root = { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_children(vec![BUTTON_1_ID, BUTTON_2_ID]); + builder.build(&mut classes) + }; + let button_1 = make_button("Button 1", &mut classes); + let button_2 = make_button("Button 2", &mut classes); TreeUpdate { nodes: vec![ (WINDOW_ID, root), diff --git a/platforms/windows/src/tests/subclassed.rs b/platforms/windows/src/tests/subclassed.rs index 063562e40..142ab9a80 100644 --- a/platforms/windows/src/tests/subclassed.rs +++ b/platforms/windows/src/tests/subclassed.rs @@ -3,9 +3,12 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use std::{num::NonZeroU128, sync::Arc}; +use std::num::NonZeroU128; -use accesskit::{ActionHandler, ActionRequest, Node, NodeId, Role, Tree, TreeUpdate}; +use accesskit::{ + Action, ActionHandler, ActionRequest, Node, NodeBuilder, NodeClassSet, NodeId, Role, Tree, + TreeUpdate, +}; use windows::Win32::{Foundation::*, UI::Accessibility::*}; use winit::{ event_loop::EventLoopBuilder, @@ -21,24 +24,23 @@ const WINDOW_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(1) }); const BUTTON_1_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(2) }); const BUTTON_2_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(3) }); -fn make_button(name: &str) -> Arc { - Arc::new(Node { - role: Role::Button, - name: Some(name.into()), - focusable: true, - ..Default::default() - }) +fn make_button(name: &str, classes: &mut NodeClassSet) -> Node { + let mut builder = NodeBuilder::new(Role::Button); + builder.set_name(name); + builder.add_action(Action::Focus); + builder.build(classes) } fn get_initial_state() -> TreeUpdate { - let root = Arc::new(Node { - role: Role::Window, - children: vec![BUTTON_1_ID, BUTTON_2_ID], - name: Some(WINDOW_TITLE.into()), - ..Default::default() - }); - let button_1 = make_button("Button 1"); - let button_2 = make_button("Button 2"); + let mut classes = NodeClassSet::new(); + let root = { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_children(vec![BUTTON_1_ID, BUTTON_2_ID]); + builder.set_name(WINDOW_TITLE); + builder.build(&mut classes) + }; + let button_1 = make_button("Button 1", &mut classes); + let button_2 = make_button("Button 2", &mut classes); TreeUpdate { nodes: vec![ (WINDOW_ID, root), diff --git a/platforms/windows/src/util.rs b/platforms/windows/src/util.rs index 05228eae6..c34b44361 100644 --- a/platforms/windows/src/util.rs +++ b/platforms/windows/src/util.rs @@ -3,7 +3,7 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::kurbo::Point; +use accesskit::Point; use std::{convert::TryInto, mem::ManuallyDrop}; use windows::{ core::*, diff --git a/platforms/winit/examples/simple.rs b/platforms/winit/examples/simple.rs index d5070dd16..81033e778 100644 --- a/platforms/winit/examples/simple.rs +++ b/platforms/winit/examples/simple.rs @@ -1,6 +1,6 @@ -use accesskit::kurbo::Rect; use accesskit::{ - Action, ActionRequest, DefaultActionVerb, Live, Node, NodeId, Role, Tree, TreeUpdate, + Action, ActionRequest, DefaultActionVerb, Live, Node, NodeBuilder, NodeClassSet, NodeId, Rect, + Role, Tree, TreeUpdate, }; use accesskit_winit::{ActionRequestEvent, Adapter}; use std::{ @@ -18,7 +18,7 @@ const WINDOW_TITLE: &str = "Hello world"; const WINDOW_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(1) }); const BUTTON_1_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(2) }); const BUTTON_2_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(3) }); -const PRESSED_TEXT_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(4) }); +const ANNOUNCEMENT_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(4) }); const INITIAL_FOCUS: NodeId = BUTTON_1_ID; const BUTTON_1_RECT: Rect = Rect { @@ -35,27 +35,33 @@ const BUTTON_2_RECT: Rect = Rect { y1: 100.0, }; -fn make_button(id: NodeId, name: &str) -> Arc { +fn build_button(id: NodeId, name: &str, classes: &mut NodeClassSet) -> Node { let rect = match id { BUTTON_1_ID => BUTTON_1_RECT, BUTTON_2_ID => BUTTON_2_RECT, _ => unreachable!(), }; - Arc::new(Node { - role: Role::Button, - bounds: Some(rect), - name: Some(name.into()), - focusable: true, - default_action_verb: Some(DefaultActionVerb::Click), - ..Default::default() - }) + let mut builder = NodeBuilder::new(Role::Button); + builder.set_bounds(rect); + builder.set_name(name); + builder.add_action(Action::Focus); + builder.set_default_action_verb(DefaultActionVerb::Click); + builder.build(classes) +} + +fn build_announcement(text: &str, classes: &mut NodeClassSet) -> Node { + let mut builder = NodeBuilder::new(Role::StaticText); + builder.set_name(text); + builder.set_live(Live::Polite); + builder.build(classes) } -#[derive(Debug)] struct State { focus: NodeId, is_window_focused: bool, + announcement: Option, + node_classes: NodeClassSet, } impl State { @@ -63,71 +69,71 @@ impl State { Arc::new(Mutex::new(Self { focus: INITIAL_FOCUS, is_window_focused: false, + announcement: None, + node_classes: NodeClassSet::new(), })) } + fn focus(&self) -> Option { + self.is_window_focused.then_some(self.focus) + } + + fn build_root(&mut self) -> Node { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_children(vec![BUTTON_1_ID, BUTTON_2_ID]); + if self.announcement.is_some() { + builder.push_child(ANNOUNCEMENT_ID); + } + builder.set_name(WINDOW_TITLE); + builder.build(&mut self.node_classes) + } + + fn build_initial_tree(&mut self) -> TreeUpdate { + let root = self.build_root(); + let button_1 = build_button(BUTTON_1_ID, "Button 1", &mut self.node_classes); + let button_2 = build_button(BUTTON_2_ID, "Button 2", &mut self.node_classes); + let mut result = TreeUpdate { + nodes: vec![ + (WINDOW_ID, root), + (BUTTON_1_ID, button_1), + (BUTTON_2_ID, button_2), + ], + tree: Some(Tree::new(WINDOW_ID)), + focus: self.focus(), + }; + if let Some(announcement) = &self.announcement { + result.nodes.push(( + ANNOUNCEMENT_ID, + build_announcement(announcement, &mut self.node_classes), + )); + } + result + } + fn update_focus(&mut self, adapter: &Adapter) { adapter.update_if_active(|| TreeUpdate { nodes: vec![], tree: None, - focus: self.is_window_focused.then_some(self.focus), + focus: self.focus(), }); } - fn press_button(&self, adapter: &Adapter, id: NodeId) { - // This is a pretty hacky way of adding or updating a node. - // A real GUI framework would have a consistent way - // of building nodes from underlying data. - // Also, this update isn't as lazy as it could be; - // we force the AccessKit tree to be initialized. - // This is expedient in this case, because that tree - // is the only place where the state of the announcement - // is stored. It's not a problem because we're really - // only concerned with testing lazy updates in the context - // of focus changes. - let name = if id == BUTTON_1_ID { + fn press_button(&mut self, adapter: &Adapter, id: NodeId) { + let text = if id == BUTTON_1_ID { "You pressed button 1" } else { "You pressed button 2" }; - let node = Arc::new(Node { - role: Role::StaticText, - name: Some(name.into()), - live: Some(Live::Polite), - ..Default::default() - }); - let root = Arc::new(Node { - role: Role::Window, - children: vec![BUTTON_1_ID, BUTTON_2_ID, PRESSED_TEXT_ID], - name: Some(WINDOW_TITLE.into()), - ..Default::default() + self.announcement = Some(text.into()); + adapter.update_if_active(|| { + let announcement = build_announcement(text, &mut self.node_classes); + let root = self.build_root(); + TreeUpdate { + nodes: vec![(ANNOUNCEMENT_ID, announcement), (WINDOW_ID, root)], + tree: None, + focus: self.focus(), + } }); - let update = TreeUpdate { - nodes: vec![(PRESSED_TEXT_ID, node), (WINDOW_ID, root)], - tree: None, - focus: self.is_window_focused.then_some(self.focus), - }; - adapter.update(update); - } -} - -fn initial_tree_update(state: &State) -> TreeUpdate { - let root = Arc::new(Node { - role: Role::Window, - children: vec![BUTTON_1_ID, BUTTON_2_ID], - name: Some(WINDOW_TITLE.into()), - ..Default::default() - }); - let button_1 = make_button(BUTTON_1_ID, "Button 1"); - let button_2 = make_button(BUTTON_2_ID, "Button 2"); - TreeUpdate { - nodes: vec![ - (WINDOW_ID, root), - (BUTTON_1_ID, button_1), - (BUTTON_2_ID, button_2), - ], - tree: Some(Tree::new(WINDOW_ID)), - focus: state.is_window_focused.then_some(state.focus), } } @@ -161,8 +167,8 @@ fn main() { Adapter::new( &window, move || { - let state = state.lock().unwrap(); - initial_tree_update(&state) + let mut state = state.lock().unwrap(); + state.build_initial_tree() }, event_loop.create_proxy(), ) @@ -202,8 +208,9 @@ fn main() { state.update_focus(&adapter); } VirtualKeyCode::Space => { - let state = state.lock().unwrap(); - state.press_button(&adapter, state.focus); + let mut state = state.lock().unwrap(); + let id = state.focus; + state.press_button(&adapter, id); } _ => (), }, diff --git a/platforms/winit/src/lib.rs b/platforms/winit/src/lib.rs index 2fdcb80b7..9d0c18975 100644 --- a/platforms/winit/src/lib.rs +++ b/platforms/winit/src/lib.rs @@ -84,7 +84,7 @@ impl Adapter { ))] #[must_use] pub fn on_event(&self, window: &Window, event: &WindowEvent) -> bool { - use accesskit::kurbo::Rect; + use accesskit::Rect; match event { WindowEvent::Moved(outer_position) => { diff --git a/platforms/winit/src/platform_impl/unix.rs b/platforms/winit/src/platform_impl/unix.rs index 3a0d1c0d8..5a58347fe 100644 --- a/platforms/winit/src/platform_impl/unix.rs +++ b/platforms/winit/src/platform_impl/unix.rs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0 (found in // the LICENSE-APACHE file). -use accesskit::{kurbo::Rect, ActionHandler, TreeUpdate}; +use accesskit::{ActionHandler, Rect, TreeUpdate}; use accesskit_unix::Adapter as UnixAdapter; use winit::window::Window;