From e69c46778558a4f18e4105cd27851e216b92b5e4 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 23 Mar 2025 17:16:15 +0100 Subject: [PATCH 01/24] chore: Enable Dependabot to check for GitHub Actions updates (#534) --- .github/dependabot.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..b11b44a1c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "chore" + groups: + ci: + patterns: + - "*" From 22f14338021e0417494e7ed281a2153b6c1ab475 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 23 Mar 2025 17:34:30 +0100 Subject: [PATCH 02/24] chore: Update dependencies to remove some duplicates (#532) --- Cargo.lock | 165 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 93 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6a6b5e47..b31cd234d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,7 +141,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.8", "once_cell", "version_check", "zerocopy", @@ -253,7 +253,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 0.38.44", "slab", "tracing", "windows-sys 0.59.0", @@ -285,7 +285,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix", + "rustix 0.38.44", "tracing", ] @@ -312,7 +312,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 0.38.44", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -473,7 +473,7 @@ dependencies = [ "bitflags 2.8.0", "log", "polling", - "rustix", + "rustix 0.38.44", "slab", "thiserror", ] @@ -485,7 +485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", - "rustix", + "rustix 0.38.44", "wayland-backend", "wayland-client", ] @@ -561,19 +561,18 @@ dependencies = [ "bitflags 1.3.2", "core-foundation", "core-graphics-types", - "foreign-types 0.5.0", + "foreign-types", "libc", ] [[package]] name = "core-graphics-types" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" dependencies = [ "bitflags 1.3.2", "core-foundation", - "foreign-types 0.3.2", "libc", ] @@ -712,15 +711,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared 0.1.1", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -728,7 +718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared 0.3.1", + "foreign-types-shared", ] [[package]] @@ -742,12 +732,6 @@ dependencies = [ "syn", ] -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -828,7 +812,19 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -854,9 +850,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hex" @@ -969,6 +965,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + [[package]] name = "log" version = "0.4.17" @@ -1019,13 +1021,12 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1413,16 +1414,17 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "polling" -version = "3.3.0" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53b6af1f60f36f8c2ac2aad5459d75a5a9b4be1e8cdd40264f315d78193e531" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", + "hermit-abi", "pin-project-lite", - "rustix", + "rustix 0.38.44", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1540,6 +1542,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "raw-window-handle" version = "0.5.2" @@ -1576,7 +1584,20 @@ dependencies = [ "bitflags 2.8.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +dependencies = [ + "bitflags 2.8.0", + "errno", + "libc", + "linux-raw-sys 0.9.3", "windows-sys 0.59.0", ] @@ -1740,7 +1761,7 @@ dependencies = [ "libc", "log", "memmap2", - "rustix", + "rustix 0.38.44", "thiserror", "wayland-backend", "wayland-client", @@ -1802,15 +1823,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.8.1" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if", "fastrand", - "redox_syscall", - "rustix", - "windows-sys 0.48.0", + "getrandom 0.3.2", + "once_cell", + "rustix 1.0.3", + "windows-sys 0.59.0", ] [[package]] @@ -1903,13 +1924,13 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "toml_datetime", - "winnow 0.6.20", + "winnow", ] [[package]] @@ -2000,6 +2021,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2079,7 +2109,7 @@ checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" dependencies = [ "cc", "downcast-rs", - "rustix", + "rustix 0.38.44", "scoped-tls", "smallvec", "wayland-sys", @@ -2092,7 +2122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" dependencies = [ "bitflags 2.8.0", - "rustix", + "rustix 0.38.44", "wayland-backend", "wayland-scanner", ] @@ -2114,7 +2144,7 @@ version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" dependencies = [ - "rustix", + "rustix 0.38.44", "wayland-client", "xcursor", ] @@ -2304,15 +2334,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.1", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -2542,7 +2563,7 @@ dependencies = [ "raw-window-handle 0.5.2", "raw-window-handle 0.6.2", "redox_syscall", - "rustix", + "rustix 0.38.44", "sctk-adwaita", "smithay-client-toolkit", "smol_str", @@ -2564,20 +2585,20 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" dependencies = [ "memchr", ] [[package]] -name = "winnow" -version = "0.7.3" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "memchr", + "bitflags 2.8.0", ] [[package]] @@ -2602,7 +2623,7 @@ dependencies = [ "libc", "libloading", "once_cell", - "rustix", + "rustix 0.38.44", "x11rb-protocol", ] @@ -2679,7 +2700,7 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.59.0", - "winnow 0.7.3", + "winnow", "xdg-home", "zbus_macros", "zbus_names", @@ -2733,7 +2754,7 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow 0.7.3", + "winnow", "zvariant", ] @@ -2780,7 +2801,7 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "winnow 0.7.3", + "winnow", "zvariant_derive", "zvariant_utils", ] @@ -2809,5 +2830,5 @@ dependencies = [ "serde", "static_assertions", "syn", - "winnow 0.7.3", + "winnow", ] From f4a4c4280cd10e530d8dda8b9837c1ba80baaa07 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 23 Mar 2025 17:45:41 +0100 Subject: [PATCH 03/24] chore: Update cargo deny configuration to prevent duplicate dependencies (#533) --- .github/workflows/ci.yml | 21 ++++++++++++++++- deny.toml | 49 ++++++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc26698f6..51a393ca1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,10 +28,29 @@ jobs: args: "--aosp --set-exit-if-changed" cargo-deny: + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-apple-darwin + - target: aarch64-linux-android + - target: i686-pc-windows-gnu + - target: i686-pc-windows-msvc + - target: x86_64-pc-windows-gnu + - target: x86_64-pc-windows-msvc + - target: x86_64-unknown-linux-gnu + + name: cargo-deny ${{ matrix.target }} runs-on: ubuntu-22.04 + needs: find-msrv steps: - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v1 + - uses: EmbarkStudios/cargo-deny-action@v2 + with: + rust-version: ${{ needs.find-msrv.outputs.version }} + log-level: error + command: check + arguments: --target ${{ matrix.target }} clippy: runs-on: ${{ matrix.os }} diff --git a/deny.toml b/deny.toml index 005ca0b72..b40581e9c 100644 --- a/deny.toml +++ b/deny.toml @@ -1,19 +1,23 @@ -targets = [] +[graph] +# Note: running just `cargo deny check` without a `--target` can result in +# false positives due to https://github.com/EmbarkStudios/cargo-deny/issues/324 +targets = [ + { triple = "aarch64-apple-darwin" }, + { triple = "aarch64-linux-android" }, + { triple = "i686-pc-windows-gnu" }, + { triple = "i686-pc-windows-msvc" }, + { triple = "x86_64-pc-windows-gnu" }, + { triple = "x86_64-pc-windows-msvc" }, + { triple = "x86_64-unknown-linux-gnu" }, +] all-features = true -no-default-features = false -feature-depth = 1 [advisories] db-path = "~/.cargo/advisory-db" db-urls = ["https://github.com/rustsec/advisory-db"] -vulnerability = "deny" -unmaintained = "warn" -yanked = "warn" -notice = "warn" ignore = [] [licenses] -unlicensed = "deny" allow = [ "Apache-2.0", "Apache-2.0 WITH LLVM-exception", @@ -23,10 +27,6 @@ allow = [ "MIT", "Zlib", ] -deny = [] -copyleft = "warn" -allow-osi-fsf-free = "neither" -default = "deny" confidence-threshold = 0.8 exceptions = [ { name = "unicode-ident", allow = [ @@ -35,19 +35,24 @@ exceptions = [ ] [bans] -multiple-versions = "warn" -wildcards = "allow" +multiple-versions = "deny" +wildcards = "deny" highlight = "all" -workspace-default-features = "allow" -external-default-features = "allow" allow = [] deny = [] - -skip = [] +skip = [ + "bitflags:<2", + "quick-xml:<0.37", + "raw-window-handle:<0.6", + "windows-sys:<0.59", + "windows-targets:<0.52", + "windows_i686_gnu:<0.52", + "windows_i686_msvc:<0.52", + "windows_x86_64_gnu:<0.52", + "windows_x86_64_msvc:<0.52", +] skip-tree = [] [sources] -unknown-registry = "warn" -unknown-git = "warn" -allow-registry = ["https://github.com/rust-lang/crates.io-index"] -allow-git = [] +unknown-registry = "deny" +unknown-git = "deny" From d04a8b7c65b348b0c23fc8a2ce0dd98ea0034fb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 13:23:00 -0500 Subject: [PATCH 04/24] chore: bump the ci group across 1 directory with 3 updates (#536) Co-authored-by: Arnold Loubriat --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release-please.yml | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51a393ca1..70f640787 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,11 +110,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' - - uses: android-actions/setup-android@v2 + - uses: android-actions/setup-android@v3 - run: sdkmanager "platforms;android-30" - run: sdkmanager "build-tools;33.0.2" - run: cp platforms/android/classes.dex platforms/android/classes.dex.orig diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 0389da4a5..08c2655b9 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -2,11 +2,15 @@ on: push: branches: - main + +permissions: + contents: write + pull-requests: write + name: release-please + jobs: release-please: runs-on: ubuntu-latest steps: - - uses: GoogleCloudPlatform/release-please-action@v3 - with: - command: manifest + - uses: googleapis/release-please-action@v4 From 23b4d8d49fed378899855a40e63aff10e829f6e8 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Tue, 25 Mar 2025 17:12:41 +0100 Subject: [PATCH 05/24] fix: Fix a compilation error in atspi-common `Event::new` (#537) --- .github/workflows/ci.yml | 4 ++++ platforms/atspi-common/src/simplified.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70f640787..9e78c0857 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,10 @@ jobs: - name: cargo clippy run: cargo clippy --all-targets -- -D warnings + - name: cargo clippy -p accesskit_atspi_common + if: matrix.os == 'ubuntu-latest' + run: cargo clippy -p accesskit_atspi_common --all-features -- -D warnings + find-msrv: runs-on: ubuntu-latest outputs: diff --git a/platforms/atspi-common/src/simplified.rs b/platforms/atspi-common/src/simplified.rs index 8c914b72f..33d83761e 100644 --- a/platforms/atspi-common/src/simplified.rs +++ b/platforms/atspi-common/src/simplified.rs @@ -618,7 +618,7 @@ impl Event { data: None, }, ObjectEvent::StateChanged(state, value) => Self { - kind: format!("object:state-changed:{}", String::from(state)), + kind: format!("object:state-changed:{}", state.to_static_str()), source, detail1: value as i32, detail2: 0, From 70e03485c45cc1934250e00bdf7a61c697ec6be9 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Tue, 25 Mar 2025 20:55:34 +0100 Subject: [PATCH 06/24] chore: Use a PAT for release-please (#539) --- .github/workflows/release-please.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 08c2655b9..09cfe2487 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -14,3 +14,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: googleapis/release-please-action@v4 + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} From a74dbfcd2d30f9fbec781db811243ec070cbf8c5 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sun, 6 Apr 2025 13:01:24 -0500 Subject: [PATCH 07/24] refactor!: Replace `immutable-chunkmap` with dual tree states (#495) --- Cargo.lock | 10 ----- consumer/Cargo.toml | 1 - consumer/src/node.rs | 3 +- consumer/src/tree.rs | 101 ++++++++++++++++++++++++------------------- 4 files changed, 57 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b31cd234d..605ba5072 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,7 +57,6 @@ version = "0.27.0" dependencies = [ "accesskit", "hashbrown", - "immutable-chunkmap", ] [[package]] @@ -860,15 +859,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "immutable-chunkmap" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f97096f508d54f8f8ab8957862eee2ccd628847b6217af1a335e1c44dee578" -dependencies = [ - "arrayvec", -] - [[package]] name = "indexmap" version = "2.6.0" diff --git a/consumer/Cargo.toml b/consumer/Cargo.toml index 743cc6cfc..66165ded6 100644 --- a/consumer/Cargo.toml +++ b/consumer/Cargo.toml @@ -14,4 +14,3 @@ rust-version.workspace = true [dependencies] accesskit = { version = "0.18.0", path = "../common" } hashbrown = { version = "0.15", default-features = false, features = ["default-hasher"] } -immutable-chunkmap = "2.0.6" diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 2c1214381..7ba158513 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -14,7 +14,6 @@ use accesskit::{ }; use alloc::{ string::{String, ToString}, - sync::Arc, vec::Vec, }; use core::{fmt, iter::FusedIterator}; @@ -32,7 +31,7 @@ pub(crate) struct ParentAndIndex(pub(crate) NodeId, pub(crate) usize); #[derive(Clone, Debug)] pub(crate) struct NodeState { pub(crate) parent_and_index: Option, - pub(crate) data: Arc, + pub(crate) data: NodeData, } #[derive(Copy, Clone)] diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index c45ad7fd6..22359fdc4 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -4,16 +4,15 @@ // the LICENSE-MIT file), at your option. use accesskit::{FrozenNode as NodeData, NodeId, Tree as TreeData, TreeUpdate}; -use alloc::{sync::Arc, vec}; +use alloc::vec; use core::fmt; use hashbrown::{HashMap, HashSet}; -use immutable_chunkmap::map::MapM as ChunkMap; use crate::node::{Node, NodeState, ParentAndIndex}; #[derive(Clone, Debug)] pub struct State { - pub(crate) nodes: ChunkMap, + pub(crate) nodes: HashMap, pub(crate) data: TreeData, pub(crate) focus: NodeId, is_host_focused: bool, @@ -28,10 +27,10 @@ struct InternalChanges { impl State { fn validate_global(&self) { - if self.nodes.get_key(&self.data.root).is_none() { + if !self.nodes.contains_key(&self.data.root) { panic!("Root id #{} is not in the node list", self.data.root.0); } - if self.nodes.get_key(&self.focus).is_none() { + if !self.nodes.contains_key(&self.focus) { panic!("Focused id #{} is not in the node list", self.focus.0); } } @@ -56,7 +55,7 @@ impl State { let mut pending_children = HashMap::new(); fn add_node( - nodes: &mut ChunkMap, + nodes: &mut HashMap, changes: &mut Option<&mut InternalChanges>, parent_and_index: Option, id: NodeId, @@ -64,9 +63,9 @@ impl State { ) { let state = NodeState { parent_and_index, - data: Arc::new(data), + data, }; - nodes.insert_cow(id, state); + nodes.insert(id, state); if let Some(changes) = changes { changes.added_node_ids.insert(id); } @@ -87,9 +86,12 @@ impl State { } unreachable.remove(child_id); let parent_and_index = ParentAndIndex(node_id, child_index); - if let Some(child_state) = self.nodes.get_mut_cow(child_id) { + if let Some(child_state) = self.nodes.get_mut(child_id) { if child_state.parent_and_index != Some(parent_and_index) { child_state.parent_and_index = Some(parent_and_index); + if let Some(changes) = &mut changes { + changes.updated_node_ids.insert(*child_id); + } } } else if let Some(child_data) = pending_nodes.remove(child_id) { add_node( @@ -105,7 +107,7 @@ impl State { seen_child_ids.insert(*child_id); } - if let Some(node_state) = self.nodes.get_mut_cow(&node_id) { + if let Some(node_state) = self.nodes.get_mut(&node_id) { if node_id == root { node_state.parent_and_index = None; } @@ -114,8 +116,8 @@ impl State { unreachable.insert(*child_id); } } - if *node_state.data != node_data { - node_state.data = Arc::new(node_data); + if node_state.data != node_data { + node_state.data.clone_from(&node_data); if let Some(changes) = &mut changes { changes.updated_node_ids.insert(node_id); } @@ -147,14 +149,14 @@ impl State { if !unreachable.is_empty() { fn traverse_unreachable( - nodes: &mut ChunkMap, + nodes: &mut HashMap, changes: &mut Option<&mut InternalChanges>, id: NodeId, ) { if let Some(changes) = changes { changes.removed_node_ids.insert(id); } - let node = nodes.remove_cow(&id).unwrap(); + let node = nodes.remove(&id).unwrap(); for child_id in node.data.children().iter() { traverse_unreachable(nodes, changes, *child_id); } @@ -240,6 +242,7 @@ pub trait ChangeHandler { #[derive(Debug)] pub struct Tree { state: State, + next_state: State, } impl Tree { @@ -248,17 +251,16 @@ impl Tree { panic!("Tried to initialize the accessibility tree without a root tree. TreeUpdate::tree must be Some."); }; let mut state = State { - nodes: ChunkMap::new(), + nodes: HashMap::new(), data: tree, focus: initial_state.focus, is_host_focused, }; state.update(initial_state, is_host_focused, None); - Self { state } - } - - pub fn update(&mut self, update: TreeUpdate) { - self.state.update(update, self.state.is_host_focused, None); + Self { + next_state: state.clone(), + state, + } } pub fn update_and_process_changes( @@ -267,14 +269,9 @@ impl Tree { handler: &mut impl ChangeHandler, ) { let mut changes = InternalChanges::default(); - let old_state = self.state.clone(); - self.state + self.next_state .update(update, self.state.is_host_focused, Some(&mut changes)); - self.process_changes(old_state, changes, handler); - } - - pub fn update_host_focus_state(&mut self, is_host_focused: bool) { - self.state.update_host_focus_state(is_host_focused, None); + self.process_changes(changes, handler); } pub fn update_host_focus_state_and_process_changes( @@ -283,45 +280,39 @@ impl Tree { handler: &mut impl ChangeHandler, ) { let mut changes = InternalChanges::default(); - let old_state = self.state.clone(); - self.state + self.next_state .update_host_focus_state(is_host_focused, Some(&mut changes)); - self.process_changes(old_state, changes, handler); + self.process_changes(changes, handler); } - fn process_changes( - &self, - old_state: State, - changes: InternalChanges, - handler: &mut impl ChangeHandler, - ) { + fn process_changes(&mut self, changes: InternalChanges, handler: &mut impl ChangeHandler) { for id in &changes.added_node_ids { - let node = self.state.node_by_id(*id).unwrap(); + let node = self.next_state.node_by_id(*id).unwrap(); handler.node_added(&node); } for id in &changes.updated_node_ids { - let old_node = old_state.node_by_id(*id).unwrap(); - let new_node = self.state.node_by_id(*id).unwrap(); + let old_node = self.state.node_by_id(*id).unwrap(); + let new_node = self.next_state.node_by_id(*id).unwrap(); handler.node_updated(&old_node, &new_node); } - if old_state.focus_id() != self.state.focus_id() { - let old_node = old_state.focus(); + if self.state.focus_id() != self.next_state.focus_id() { + let old_node = self.state.focus(); if let Some(old_node) = &old_node { let id = old_node.id(); if !changes.updated_node_ids.contains(&id) && !changes.removed_node_ids.contains(&id) { - if let Some(old_node_new_version) = self.state.node_by_id(id) { + if let Some(old_node_new_version) = self.next_state.node_by_id(id) { handler.node_updated(old_node, &old_node_new_version); } } } - let new_node = self.state.focus(); + let new_node = self.next_state.focus(); if let Some(new_node) = &new_node { let id = new_node.id(); if !changes.added_node_ids.contains(&id) && !changes.updated_node_ids.contains(&id) { - if let Some(new_node_old_version) = old_state.node_by_id(id) { + if let Some(new_node_old_version) = self.state.node_by_id(id) { handler.node_updated(&new_node_old_version, new_node); } } @@ -329,9 +320,29 @@ impl Tree { handler.focus_moved(old_node.as_ref(), new_node.as_ref()); } for id in &changes.removed_node_ids { - let node = old_state.node_by_id(*id).unwrap(); + let node = self.state.node_by_id(*id).unwrap(); handler.node_removed(&node); } + for id in changes.added_node_ids { + self.state + .nodes + .insert(id, self.next_state.nodes.get(&id).unwrap().clone()); + } + for id in changes.updated_node_ids { + self.state + .nodes + .get_mut(&id) + .unwrap() + .clone_from(self.next_state.nodes.get(&id).unwrap()); + } + for id in changes.removed_node_ids { + self.state.nodes.remove(&id); + } + if self.state.data != self.next_state.data { + self.state.data.clone_from(&self.next_state.data); + } + self.state.focus = self.next_state.focus; + self.state.is_host_focused = self.next_state.is_host_focused; } pub fn state(&self) -> &State { From 6338e45097662bf39994e19a09054c20cb2ee782 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 6 Apr 2025 20:22:56 +0200 Subject: [PATCH 08/24] fix: Update pyo3 to 0.24 (#544) --- Cargo.lock | 24 ++++++++++++------------ common/Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 605ba5072..b5f5e524b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1443,9 +1443,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc" +checksum = "17da310086b068fbdcefbba30aeb3721d5bb9af8db4987d6735b2183ca567229" dependencies = [ "cfg-if", "indoc", @@ -1461,9 +1461,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7" +checksum = "e27165889bd793000a098bb966adc4300c312497ea25cf7a690a9f0ac5aa5fc1" dependencies = [ "once_cell", "target-lexicon", @@ -1471,9 +1471,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d" +checksum = "05280526e1dbf6b420062f3ef228b78c0c54ba94e157f5cb724a609d0f2faabc" dependencies = [ "libc", "pyo3-build-config", @@ -1481,9 +1481,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7" +checksum = "5c3ce5686aa4d3f63359a5100c62a127c9f15e8398e5fdeb5deef1fed5cd5f44" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1493,9 +1493,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4" +checksum = "f4cf6faa0cbfb0ed08e89beb8103ae9724eb4750e3a78084ba4017cbe94f3855" dependencies = [ "heck", "proc-macro2", @@ -1807,9 +1807,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" diff --git a/common/Cargo.toml b/common/Cargo.toml index 9dddb678a..5247f1db6 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -16,7 +16,7 @@ features = ["schemars", "serde"] [dependencies] enumn = { version = "0.1.6", optional = true } -pyo3 = { version = "0.23", optional = true } +pyo3 = { version = "0.24", optional = true } schemars = { version = "0.8.7", optional = true } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true } From f8c0d0a6fc9613cf1a2a6d8cfba11ebc892dfeb8 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sun, 6 Apr 2025 15:13:10 -0500 Subject: [PATCH 09/24] refactor!: Drop `FrozenNode` (#496) --- common/src/lib.rs | 168 ------------------------------------------- consumer/src/node.rs | 4 +- consumer/src/tree.rs | 4 +- 3 files changed, 3 insertions(+), 173 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index f2d232994..e804a75e3 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -884,23 +884,6 @@ impl Default for PropertyIndices { } } -#[derive(Clone, Debug, PartialEq)] -struct FrozenProperties { - indices: PropertyIndices, - values: Box<[PropertyValue]>, -} - -/// An accessibility node snapshot that can't be modified. This is not used by -/// toolkits or applications, but only by code that retains an AccessKit tree -/// in memory, such as the `accesskit_consumer` crate. -#[derive(Clone, PartialEq)] -pub struct FrozenNode { - role: Role, - actions: u32, - flags: u32, - properties: FrozenProperties, -} - #[derive(Clone, Debug, Default, PartialEq)] struct Properties { indices: PropertyIndices, @@ -974,31 +957,8 @@ impl Properties { } } -impl From for FrozenProperties { - fn from(props: Properties) -> Self { - Self { - indices: props.indices, - values: props.values.into_boxed_slice(), - } - } -} - macro_rules! flag_methods { ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { - impl FrozenNode { - $($(#[$doc])* - #[inline] - pub fn $getter(&self) -> bool { - (self.flags & (Flag::$id).mask()) != 0 - })* - fn debug_flag_properties(&self, fmt: &mut fmt::DebugStruct) { - $( - if self.$getter() { - fmt.field(stringify!($getter), &true); - } - )* - } - } impl Node { $($(#[$doc])* #[inline] @@ -1109,13 +1069,6 @@ macro_rules! vec_type_methods { 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 FrozenNode { - $($(#[$doc])* - #[inline] - pub fn $getter(&self) -> $getter_result { - self.properties.indices.$type_getter(&self.properties.values, PropertyId::$id) - })* - } impl Node { $($(#[$doc])* #[inline] @@ -1168,9 +1121,6 @@ macro_rules! node_id_vec_property_methods { $(#[$doc])* ($id, NodeId, $getter, get_node_id_vec, $setter, set_node_id_vec, $pusher, push_to_node_id_vec, $clearer) })* - impl FrozenNode { - slice_properties_debug_method! { debug_node_id_vec_properties, [$($getter,)*] } - } impl Node { slice_properties_debug_method! { debug_node_id_vec_properties, [$($getter,)*] } } @@ -1195,9 +1145,6 @@ macro_rules! node_id_property_methods { $(#[$doc])* ($id, $getter, get_node_id_property, Option, $setter, set_node_id_property, NodeId, $clearer) })* - impl FrozenNode { - option_properties_debug_method! { debug_node_id_properties, [$($getter,)*] } - } impl Node { option_properties_debug_method! { debug_node_id_properties, [$($getter,)*] } } @@ -1210,9 +1157,6 @@ macro_rules! string_property_methods { $(#[$doc])* ($id, $getter, get_string_property, Option<&str>, $setter, set_string_property, impl Into>, $clearer) })* - impl FrozenNode { - option_properties_debug_method! { debug_string_properties, [$($getter,)*] } - } impl Node { option_properties_debug_method! { debug_string_properties, [$($getter,)*] } } @@ -1225,9 +1169,6 @@ macro_rules! f64_property_methods { $(#[$doc])* ($id, $getter, get_f64_property, Option, $setter, set_f64_property, f64, $clearer) })* - impl FrozenNode { - option_properties_debug_method! { debug_f64_properties, [$($getter,)*] } - } impl Node { option_properties_debug_method! { debug_f64_properties, [$($getter,)*] } } @@ -1240,9 +1181,6 @@ macro_rules! usize_property_methods { $(#[$doc])* ($id, $getter, get_usize_property, Option, $setter, set_usize_property, usize, $clearer) })* - impl FrozenNode { - option_properties_debug_method! { debug_usize_properties, [$($getter,)*] } - } impl Node { option_properties_debug_method! { debug_usize_properties, [$($getter,)*] } } @@ -1255,9 +1193,6 @@ macro_rules! color_property_methods { $(#[$doc])* ($id, $getter, get_color_property, Option, $setter, set_color_property, u32, $clearer) })* - impl FrozenNode { - option_properties_debug_method! { debug_color_properties, [$($getter,)*] } - } impl Node { option_properties_debug_method! { debug_color_properties, [$($getter,)*] } } @@ -1270,9 +1205,6 @@ macro_rules! text_decoration_property_methods { $(#[$doc])* ($id, $getter, get_text_decoration_property, Option, $setter, set_text_decoration_property, TextDecoration, $clearer) })* - impl FrozenNode { - option_properties_debug_method! { debug_text_decoration_properties, [$($getter,)*] } - } impl Node { option_properties_debug_method! { debug_text_decoration_properties, [$($getter,)*] } } @@ -1285,9 +1217,6 @@ macro_rules! length_slice_property_methods { $(#[$doc])* ($id, $getter, get_length_slice_property, &[u8], $setter, set_length_slice_property, impl Into>, $clearer) })* - impl FrozenNode { - slice_properties_debug_method! { debug_length_slice_properties, [$($getter,)*] } - } impl Node { slice_properties_debug_method! { debug_length_slice_properties, [$($getter,)*] } } @@ -1300,9 +1229,6 @@ macro_rules! coord_slice_property_methods { $(#[$doc])* ($id, $getter, get_coord_slice_property, Option<&[f32]>, $setter, set_coord_slice_property, impl Into>, $clearer) })* - impl FrozenNode { - option_properties_debug_method! { debug_coord_slice_properties, [$($getter,)*] } - } impl Node { option_properties_debug_method! { debug_coord_slice_properties, [$($getter,)*] } } @@ -1315,9 +1241,6 @@ macro_rules! bool_property_methods { $(#[$doc])* ($id, $getter, get_bool_property, Option, $setter, set_bool_property, bool, $clearer) })* - impl FrozenNode { - option_properties_debug_method! { debug_bool_properties, [$($getter,)*] } - } impl Node { option_properties_debug_method! { debug_bool_properties, [$($getter,)*] } } @@ -1326,18 +1249,6 @@ macro_rules! bool_property_methods { macro_rules! unique_enum_property_methods { ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { - impl FrozenNode { - $($(#[$doc])* - #[inline] - pub fn $getter(&self) -> Option<$id> { - match self.properties.indices.get(&self.properties.values, PropertyId::$id) { - PropertyValue::None => None, - PropertyValue::$id(value) => Some(*value), - _ => unexpected_property_type(), - } - })* - option_properties_debug_method! { debug_unique_enum_properties, [$($getter,)*] } - } impl Node { $($(#[$doc])* #[inline] @@ -1371,38 +1282,6 @@ impl Node { } } -impl From for FrozenNode { - fn from(node: Node) -> Self { - Self { - role: node.role, - actions: node.actions, - flags: node.flags, - properties: node.properties.into(), - } - } -} - -impl From<&FrozenNode> for Node { - fn from(node: &FrozenNode) -> Self { - Self { - role: node.role, - actions: node.actions, - flags: node.flags, - properties: Properties { - indices: node.properties.indices, - values: node.properties.values.to_vec(), - }, - } - } -} - -impl FrozenNode { - #[inline] - pub fn role(&self) -> Role { - self.role - } -} - impl Node { #[inline] pub fn role(&self) -> Role { @@ -1412,16 +1291,7 @@ impl Node { pub fn set_role(&mut self, value: Role) { self.role = value; } -} -impl FrozenNode { - #[inline] - pub fn supports_action(&self, action: Action) -> bool { - (self.actions & action.mask()) != 0 - } -} - -impl Node { #[inline] pub fn supports_action(&self, action: Action) -> bool { (self.actions & action.mask()) != 0 @@ -1806,10 +1676,6 @@ property_methods! { (TextSelection, text_selection, get_text_selection_property, Option<&TextSelection>, set_text_selection, set_text_selection_property, impl Into>, clear_text_selection) } -impl FrozenNode { - option_properties_debug_method! { debug_option_properties, [transform, bounds, text_selection,] } -} - impl Node { option_properties_debug_method! { debug_option_properties, [transform, bounds, text_selection,] } } @@ -1818,40 +1684,6 @@ 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) } -impl fmt::Debug for FrozenNode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut fmt = f.debug_struct("FrozenNode"); - - fmt.field("role", &self.role()); - - let supported_actions = action_mask_to_action_vec(self.actions); - if !supported_actions.is_empty() { - fmt.field("actions", &supported_actions); - } - - self.debug_flag_properties(&mut fmt); - self.debug_node_id_vec_properties(&mut fmt); - self.debug_node_id_properties(&mut fmt); - self.debug_string_properties(&mut fmt); - self.debug_f64_properties(&mut fmt); - self.debug_usize_properties(&mut fmt); - self.debug_color_properties(&mut fmt); - self.debug_text_decoration_properties(&mut fmt); - self.debug_length_slice_properties(&mut fmt); - self.debug_coord_slice_properties(&mut fmt); - self.debug_bool_properties(&mut fmt); - self.debug_unique_enum_properties(&mut fmt); - self.debug_option_properties(&mut fmt); - - let custom_actions = self.custom_actions(); - if !custom_actions.is_empty() { - fmt.field("custom_actions", &custom_actions); - } - - fmt.finish() - } -} - impl fmt::Debug for Node { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut fmt = f.debug_struct("Node"); diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 7ba158513..0422123a9 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -9,8 +9,8 @@ // found in the LICENSE.chromium file. use accesskit::{ - Action, Affine, FrozenNode as NodeData, Live, NodeId, Orientation, Point, Rect, Role, - TextSelection, Toggled, + Action, Affine, Live, Node as NodeData, NodeId, Orientation, Point, Rect, Role, TextSelection, + Toggled, }; use alloc::{ string::{String, ToString}, diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 22359fdc4..7534d28de 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -3,7 +3,7 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -use accesskit::{FrozenNode as NodeData, NodeId, Tree as TreeData, TreeUpdate}; +use accesskit::{Node as NodeData, NodeId, Tree as TreeData, TreeUpdate}; use alloc::vec; use core::fmt; use hashbrown::{HashMap, HashSet}; @@ -72,8 +72,6 @@ impl State { } for (node_id, node_data) in update.nodes { - let node_data = NodeData::from(node_data); - unreachable.remove(&node_id); let mut seen_child_ids = HashSet::with_capacity(node_data.children().len()); From 2f86c453a776956ca36c06c9689be22323646421 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 6 Apr 2025 23:29:06 +0200 Subject: [PATCH 10/24] fix: Update windows-rs to 0.61 (#541) --- Cargo.lock | 71 +++++++++++++++++------ platforms/windows/Cargo.toml | 5 +- platforms/windows/examples/hello_world.rs | 11 ++-- platforms/windows/src/node.rs | 16 ++--- platforms/windows/src/subclass.rs | 2 +- platforms/windows/src/tests/mod.rs | 11 ++-- platforms/windows/src/text.rs | 17 +++--- platforms/windows/src/util.rs | 8 +-- 8 files changed, 92 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5f5e524b..e2e541770 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2253,32 +2253,54 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.58.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ "windows-core", - "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.58.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement", "windows-interface", + "windows-link", "windows-result", "windows-strings", - "windows-targets 0.52.6", +] + +[[package]] +name = "windows-future" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +dependencies = [ + "windows-core", + "windows-link", ] [[package]] name = "windows-implement" -version = "0.58.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", @@ -2287,9 +2309,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", @@ -2297,22 +2319,37 @@ dependencies = [ ] [[package]] -name = "windows-result" +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-targets 0.52.6", + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link", ] [[package]] diff --git a/platforms/windows/Cargo.toml b/platforms/windows/Cargo.toml index ca8498a9a..c68989391 100644 --- a/platforms/windows/Cargo.toml +++ b/platforms/windows/Cargo.toml @@ -20,12 +20,11 @@ accesskit = { version = "0.18.0", path = "../../common" } accesskit_consumer = { version = "0.27.0", path = "../../consumer" } hashbrown = { version = "0.15", default-features = false, features = ["default-hasher"] } static_assertions = "1.1.0" -windows-core = "0.58.0" +windows-core = "0.61.0" [dependencies.windows] -version = "0.58.0" +version = "0.61.1" features = [ - "implement", "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_Com", diff --git a/platforms/windows/examples/hello_world.rs b/platforms/windows/examples/hello_world.rs index 8872b7c16..b5c1d29cd 100644 --- a/platforms/windows/examples/hello_world.rs +++ b/platforms/windows/examples/hello_world.rs @@ -197,7 +197,7 @@ impl ActionHandler for SimpleActionHandler { Action::Focus => { unsafe { PostMessageW( - self.window, + Some(self.window), SET_FOCUS_MSG, WPARAM(0), LPARAM(request.target.0 as _), @@ -208,7 +208,7 @@ impl ActionHandler for SimpleActionHandler { Action::Click => { unsafe { PostMessageW( - self.window, + Some(self.window), CLICK_MSG, WPARAM(0), LPARAM(request.target.0 as _), @@ -241,7 +241,7 @@ extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: L unsafe { DefWindowProcW(window, message, wparam, lparam) } } WM_PAINT => { - unsafe { ValidateRect(window, None) }.unwrap(); + unsafe { ValidateRect(Some(window), None) }.unwrap(); LRESULT(0) } WM_DESTROY => { @@ -321,6 +321,7 @@ extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: L fn create_window(title: &str, initial_focus: NodeId) -> Result { let create_params = Box::new(WindowCreateParams(initial_focus)); + let module = HINSTANCE::from(unsafe { GetModuleHandleW(None)? }); let window = unsafe { CreateWindowExW( @@ -334,7 +335,7 @@ fn create_window(title: &str, initial_focus: NodeId) -> Result { CW_USEDEFAULT, None, None, - GetModuleHandleW(None).unwrap(), + Some(module), Some(Box::into_raw(create_params) as _), )? }; @@ -355,7 +356,7 @@ fn main() -> Result<()> { let _ = unsafe { ShowWindow(window, SW_SHOW) }; let mut message = MSG::default(); - while unsafe { GetMessageW(&mut message, HWND::default(), 0, 0) }.into() { + while unsafe { GetMessageW(&mut message, None, 0, 0) }.into() { let _ = unsafe { TranslateMessage(&message) }; unsafe { DispatchMessageW(&message) }; } diff --git a/platforms/windows/src/node.rs b/platforms/windows/src/node.rs index 994cc08f4..d390318a8 100644 --- a/platforms/windows/src/node.rs +++ b/platforms/windows/src/node.rs @@ -18,7 +18,11 @@ use accesskit_consumer::{FilterResult, Node, TreeState}; use std::sync::{atomic::Ordering, Arc, Weak}; use windows::{ core::*, - Win32::{Foundation::*, System::Com::*, UI::Accessibility::*}, + Win32::{ + Foundation::*, + System::{Com::*, Variant::*}, + UI::Accessibility::*, + }, }; use crate::{ @@ -768,8 +772,7 @@ impl IRawElementProviderFragment_Impl for PlatformNode_Impl { fn FragmentRoot(&self) -> Result { self.with_tree_state(|state| { if self.is_root(state) { - // SAFETY: We know &self is inside a full COM implementation. - unsafe { self.cast() } + Ok(self.to_interface()) } else { let root_id = state.root_id(); Ok(self.relative(root_id).into()) @@ -850,15 +853,14 @@ macro_rules! patterns { ), ( $($extra_trait_method:item),* ))),+) => { - impl PlatformNode { + impl PlatformNode_Impl { fn pattern_provider(&self, pattern_id: UIA_PATTERN_ID) -> Result { self.resolve(|node| { let wrapper = NodeWrapper(&node); match pattern_id { $($pattern_id => { if wrapper.$is_supported() { - // SAFETY: We know we're running inside a full COM implementation. - let intermediate: $provider_interface = unsafe { self.cast() }?; + let intermediate: $provider_interface = self.to_interface(); return intermediate.cast(); } })* @@ -1031,7 +1033,7 @@ patterns! { Ok(std::ptr::null_mut()) }, - fn RangeFromChild(&self, _child: Option<&IRawElementProviderSimple>) -> Result { + fn RangeFromChild(&self, _child: Ref) -> Result { // We don't support embedded objects in text. Err(not_implemented()) }, diff --git a/platforms/windows/src/subclass.rs b/platforms/windows/src/subclass.rs index 7a988fe3d..b14c39347 100644 --- a/platforms/windows/src/subclass.rs +++ b/platforms/windows/src/subclass.rs @@ -97,7 +97,7 @@ impl SubclassImpl { SetPropW( self.hwnd, PROP_NAME, - HANDLE(self as *const SubclassImpl as _), + Some(HANDLE(self as *const SubclassImpl as _)), ) } .unwrap(); diff --git a/platforms/windows/src/tests/mod.rs b/platforms/windows/src/tests/mod.rs index dc9bba4ef..2ee2cbae4 100644 --- a/platforms/windows/src/tests/mod.rs +++ b/platforms/windows/src/tests/mod.rs @@ -91,7 +91,7 @@ extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: L unsafe { DefWindowProcW(window, message, wparam, lparam) } } WM_PAINT => { - unsafe { ValidateRect(window, None) }.unwrap(); + unsafe { ValidateRect(Some(window), None) }.unwrap(); LRESULT(0) } WM_DESTROY => { @@ -142,6 +142,7 @@ fn create_window( activation_handler: Box::new(activation_handler), action_handler: Arc::new(ActionHandlerWrapper::new(action_handler)), }); + let module = HINSTANCE::from(unsafe { GetModuleHandleW(None)? }); let window = unsafe { CreateWindowExW( @@ -155,7 +156,7 @@ fn create_window( CW_USEDEFAULT, None, None, - GetModuleHandleW(None).unwrap(), + Some(module), Some(Box::into_raw(create_params) as _), )? }; @@ -213,7 +214,7 @@ where } let mut message = MSG::default(); - while unsafe { GetMessageW(&mut message, HWND::default(), 0, 0) }.into() { + while unsafe { GetMessageW(&mut message, None, 0, 0) }.into() { let _ = unsafe { TranslateMessage(&message) }; unsafe { DispatchMessageW(&message) }; } @@ -230,7 +231,7 @@ where }; let _window_guard = scopeguard::guard((), |_| { - unsafe { PostMessageW(window.0, WM_CLOSE, WPARAM(0), LPARAM(0)) }.unwrap() + unsafe { PostMessageW(Some(window.0), WM_CLOSE, WPARAM(0), LPARAM(0)) }.unwrap() }); // We must initialize COM before creating the UIA client. The MTA option @@ -326,7 +327,7 @@ impl FocusEventHandler { #[allow(non_snake_case)] impl IUIAutomationFocusChangedEventHandler_Impl for FocusEventHandler_Impl { - fn HandleFocusChangedEvent(&self, sender: Option<&IUIAutomationElement>) -> Result<()> { + fn HandleFocusChangedEvent(&self, sender: Ref) -> Result<()> { self.received.put(sender.unwrap().clone()); Ok(()) } diff --git a/platforms/windows/src/text.rs b/platforms/windows/src/text.rs index 45d149ff9..73af7ed1a 100644 --- a/platforms/windows/src/text.rs +++ b/platforms/windows/src/text.rs @@ -12,7 +12,10 @@ use accesskit_consumer::{ use std::sync::{Arc, RwLock, Weak}; use windows::{ core::*, - Win32::{Foundation::*, System::Com::*, UI::Accessibility::*}, + Win32::{ + System::{Com::*, Variant::*}, + UI::Accessibility::*, + }, }; use crate::{context::Context, node::PlatformNode, util::*}; @@ -328,8 +331,8 @@ impl ITextRangeProvider_Impl for PlatformRange_Impl { Ok(self.this.clone().into()) } - fn Compare(&self, other: Option<&ITextRangeProvider>) -> Result { - let other = unsafe { required_param(other)?.as_impl() }; + fn Compare(&self, other: Ref) -> Result { + let other = unsafe { required_param(&other)?.as_impl() }; Ok((self.context.ptr_eq(&other.context) && *self.state.read().unwrap() == *other.state.read().unwrap()) .into()) @@ -338,10 +341,10 @@ impl ITextRangeProvider_Impl for PlatformRange_Impl { fn CompareEndpoints( &self, endpoint: TextPatternRangeEndpoint, - other: Option<&ITextRangeProvider>, + other: Ref, other_endpoint: TextPatternRangeEndpoint, ) -> Result { - let other = unsafe { required_param(other)?.as_impl() }; + let other = unsafe { required_param(&other)?.as_impl() }; if std::ptr::eq(other as *const _, &self.this as *const _) { // Comparing endpoints within the same range can be done // safely without upgrading the range. This allows ATs @@ -531,10 +534,10 @@ impl ITextRangeProvider_Impl for PlatformRange_Impl { fn MoveEndpointByRange( &self, endpoint: TextPatternRangeEndpoint, - other: Option<&ITextRangeProvider>, + other: Ref, other_endpoint: TextPatternRangeEndpoint, ) -> Result<()> { - let other = unsafe { required_param(other)?.as_impl() }; + let other = unsafe { required_param(&other)?.as_impl() }; self.require_same_context(other)?; // We have to obtain the tree state and ranges manually to avoid // lifetime issues, and work with the two locks in a specific order diff --git a/platforms/windows/src/util.rs b/platforms/windows/src/util.rs index bb746ee82..b2e9d38dd 100644 --- a/platforms/windows/src/util.rs +++ b/platforms/windows/src/util.rs @@ -38,7 +38,7 @@ impl Write for WideString { impl From for BSTR { fn from(value: WideString) -> Self { - Self::from_wide(&value.0).unwrap() + Self::from_wide(&value.0) } } @@ -199,8 +199,8 @@ pub(crate) fn invalid_arg() -> Error { E_INVALIDARG.into() } -pub(crate) fn required_param(param: Option<&T>) -> Result<&T> { - param.map_or_else(|| Err(invalid_arg()), Ok) +pub(crate) fn required_param<'a, T: Interface>(param: &'a Ref) -> Result<&'a T> { + param.ok().map_err(|_| invalid_arg()) } pub(crate) fn element_not_available() -> Error { @@ -248,7 +248,7 @@ pub(crate) fn window_title(hwnd: WindowHandle) -> Option { } let len = result.0 as usize; unsafe { buffer.set_len(len) }; - Some(BSTR::from_wide(&buffer).unwrap()) + Some(BSTR::from_wide(&buffer)) } pub(crate) fn toolkit_description(state: &TreeState) -> Option { From 14cab5120471b0f9ab103d1cf4158a7a902f2659 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Wed, 9 Apr 2025 22:52:48 +0200 Subject: [PATCH 11/24] fix: Update tokio and tokio-stream dependencies (#546) --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2e541770..bdd6066e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1871,9 +1871,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.40.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "libc", @@ -1886,9 +1886,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -1897,9 +1897,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", From 3aab4ac6f0193b8a06d7962f933582a4dbdf0c98 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sat, 12 Apr 2025 20:15:37 +0200 Subject: [PATCH 12/24] refactor!: Drop unused `Node::is_linked` (#545) --- common/src/lib.rs | 2 -- consumer/src/lib.rs | 1 - 2 files changed, 3 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index e804a75e3..41c2c8ba0 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -696,7 +696,6 @@ pub struct TextSelection { #[repr(u8)] enum Flag { Hidden, - Linked, Multiselectable, Required, Visited, @@ -1314,7 +1313,6 @@ flag_methods! { /// 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), (Multiselectable, is_multiselectable, set_multiselectable, clear_multiselectable), (Required, is_required, set_required, clear_required), (Visited, is_visited, set_visited, clear_visited), diff --git a/consumer/src/lib.rs b/consumer/src/lib.rs index 5ab13d382..8b0bfa045 100644 --- a/consumer/src/lib.rs +++ b/consumer/src/lib.rs @@ -145,7 +145,6 @@ mod tests { let link_3_1_ignored = { let mut node = Node::new(Role::Link); node.set_children(vec![LABEL_3_1_0_ID]); - node.set_linked(); node }; let label_3_1_0 = { From a47bca1e376de7b0a22a7dfe6c23dedad315c449 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sat, 12 Apr 2025 20:20:04 +0200 Subject: [PATCH 13/24] fix: Improve `NodeId`'s debug representation (#547) --- common/src/lib.rs | 8 +++++++- consumer/src/tree.rs | 12 ++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index 41c2c8ba0..a5fb8ef3c 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -625,7 +625,7 @@ pub enum TextDecoration { pub type NodeIdContent = u64; /// The stable identity of a [`Node`], unique within the node's tree. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "schemars", derive(JsonSchema))] #[repr(transparent)] @@ -645,6 +645,12 @@ impl From for NodeIdContent { } } +impl fmt::Debug for NodeId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "#{}", self.0) + } +} + /// Defines a custom action for a UI element. /// /// For example, a list UI can allow a user to reorder items in the list by dragging the diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 7534d28de..371582410 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -28,10 +28,10 @@ struct InternalChanges { impl State { fn validate_global(&self) { if !self.nodes.contains_key(&self.data.root) { - panic!("Root id #{} is not in the node list", self.data.root.0); + panic!("Root ID {:?} is not in the node list", self.data.root); } if !self.nodes.contains_key(&self.focus) { - panic!("Focused id #{} is not in the node list", self.focus.0); + panic!("Focused ID {:?} is not in the node list", self.focus); } } @@ -78,8 +78,8 @@ impl State { for (child_index, child_id) in node_data.children().iter().enumerate() { if seen_child_ids.contains(child_id) { panic!( - "Node #{} of TreeUpdate includes duplicate child #{};", - node_id.0, child_id.0 + "Node {:?} of TreeUpdate includes duplicate child {:?};", + node_id, child_id ); } unreachable.remove(child_id); @@ -139,7 +139,7 @@ impl State { panic!("TreeUpdate includes {} nodes which are neither in the current tree nor a child of another node from the update: {}", pending_nodes.len(), ShortNodeList(&pending_nodes)); } if !pending_children.is_empty() { - panic!("TreeUpdate's nodes include {} children ids which are neither in the current tree nor the id of another node from the update: {}", pending_children.len(), ShortNodeList(&pending_children)); + panic!("TreeUpdate's nodes include {} children ids which are neither in the current tree nor the ID of another node from the update: {}", pending_children.len(), ShortNodeList(&pending_children)); } self.focus = update.focus; @@ -361,7 +361,7 @@ impl fmt::Display for ShortNodeList<'_, T> { if i != 0 { write!(f, ", ")?; } - write!(f, "#{}", id.0)?; + write!(f, "{:?}", id)?; } if iter.next().is_some() { write!(f, " ...")?; From 56abf17356e4c7f13f64aaeaca6a63c8f7ede553 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Tue, 15 Apr 2025 21:25:04 +0200 Subject: [PATCH 14/24] refactor!: Drop redundant `HasPopup::True` (#550) --- common/src/lib.rs | 1 - platforms/android/Cargo.toml | 1 + platforms/atspi-common/Cargo.toml | 1 + platforms/macos/Cargo.toml | 1 + platforms/unix/Cargo.toml | 1 + platforms/windows/Cargo.toml | 1 + platforms/winit/Cargo.toml | 1 + 7 files changed, 6 insertions(+), 1 deletion(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index a5fb8ef3c..a48d7d046 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -544,7 +544,6 @@ pub enum Live { )] #[repr(u8)] pub enum HasPopup { - True, Menu, Listbox, Tree, diff --git a/platforms/android/Cargo.toml b/platforms/android/Cargo.toml index ab5863951..832dcca94 100644 --- a/platforms/android/Cargo.toml +++ b/platforms/android/Cargo.toml @@ -20,3 +20,4 @@ accesskit_consumer = { version = "0.27.0", path = "../../consumer" } jni = "0.21.1" log = "0.4.17" once_cell = "1.17.1" + diff --git a/platforms/atspi-common/Cargo.toml b/platforms/atspi-common/Cargo.toml index 8b21ead00..2feaff99b 100644 --- a/platforms/atspi-common/Cargo.toml +++ b/platforms/atspi-common/Cargo.toml @@ -21,3 +21,4 @@ atspi-common = { version = "0.9", default-features = false } serde = "1.0" thiserror = "1.0" zvariant = { version = "5.4", default-features = false } + diff --git a/platforms/macos/Cargo.toml b/platforms/macos/Cargo.toml index 9a10a2105..3f54087c0 100644 --- a/platforms/macos/Cargo.toml +++ b/platforms/macos/Cargo.toml @@ -34,3 +34,4 @@ objc2-app-kit = { version = "0.2.0", features = [ "NSView", "NSWindow", ] } + diff --git a/platforms/unix/Cargo.toml b/platforms/unix/Cargo.toml index 5418af9cf..c7d4193d7 100644 --- a/platforms/unix/Cargo.toml +++ b/platforms/unix/Cargo.toml @@ -36,3 +36,4 @@ tokio-stream = { version = "0.1.14", optional = true } version = "1.32.0" optional = true features = ["macros", "net", "rt", "sync", "time"] + diff --git a/platforms/windows/Cargo.toml b/platforms/windows/Cargo.toml index c68989391..05eb4de35 100644 --- a/platforms/windows/Cargo.toml +++ b/platforms/windows/Cargo.toml @@ -40,3 +40,4 @@ features = [ once_cell = "1.13.0" scopeguard = "1.1.0" winit = "0.30" + diff --git a/platforms/winit/Cargo.toml b/platforms/winit/Cargo.toml index 6d318ac7b..3fa56672b 100644 --- a/platforms/winit/Cargo.toml +++ b/platforms/winit/Cargo.toml @@ -40,3 +40,4 @@ accesskit_android = { version = "0.1.1", path = "../android", optional = true, f version = "0.30.5" default-features = false features = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] + From 4b74090dc0b848747296b4a66d3bbe3cef96fc56 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Tue, 15 Apr 2025 14:36:09 -0500 Subject: [PATCH 15/24] fix: Return text content from multiline inputs (#552) --- platforms/android/src/node.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index be68aefaf..d480be448 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -67,7 +67,11 @@ impl NodeWrapper<'_> { } pub(crate) fn text(&self) -> Option { - self.0.value() + self.0.value().or_else(|| { + self.0 + .supports_text_ranges() + .then(|| self.0.document_range().text()) + }) } pub(crate) fn text_selection(&self) -> Option<(usize, usize)> { From b1fb5b3de12c001e34021263038b66a6e3a7dd1e Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Thu, 26 Dec 2024 18:58:10 +0100 Subject: [PATCH 16/24] feat: Expose tabs in consumer and atspi-common --- consumer/src/node.rs | 12 ++++++++++++ platforms/atspi-common/src/lib.rs | 2 +- platforms/atspi-common/src/node.rs | 23 +++++++++++++++++++++-- platforms/atspi-common/src/simplified.rs | 11 ++++++++++- platforms/macos/src/node.rs | 3 +++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 0422123a9..5154ac121 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -407,6 +407,8 @@ impl<'a> Node<'a> { self.data().orientation().or_else(|| { if self.role() == Role::ListBox { Some(Orientation::Vertical) + } else if self.role() == Role::TabList { + Some(Orientation::Horizontal) } else { None } @@ -691,6 +693,16 @@ impl<'a> Node<'a> { ) } + pub fn controls( + &self, + ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { + let state = self.tree_state; + let data = &self.state.data; + data.controls() + .iter() + .map(move |id| state.node_by_id(*id).unwrap()) + } + pub fn raw_text_selection(&self) -> Option<&TextSelection> { self.data().text_selection() } diff --git a/platforms/atspi-common/src/lib.rs b/platforms/atspi-common/src/lib.rs index fa2ba0729..8680944d3 100644 --- a/platforms/atspi-common/src/lib.rs +++ b/platforms/atspi-common/src/lib.rs @@ -17,7 +17,7 @@ pub mod simplified; mod util; pub use atspi_common::{ - CoordType, Granularity, InterfaceSet, Layer, Role, ScrollType, State, StateSet, + CoordType, Granularity, InterfaceSet, Layer, RelationType, Role, ScrollType, State, StateSet, }; pub use action::*; diff --git a/platforms/atspi-common/src/node.rs b/platforms/atspi-common/src/node.rs index 46f50fe5f..2e47aa23e 100644 --- a/platforms/atspi-common/src/node.rs +++ b/platforms/atspi-common/src/node.rs @@ -14,8 +14,8 @@ use accesskit::{ }; use accesskit_consumer::{FilterResult, Node, TreeState}; use atspi_common::{ - CoordType, Granularity, Interface, InterfaceSet, Layer, Politeness, Role as AtspiRole, - ScrollType, State, StateSet, + CoordType, Granularity, Interface, InterfaceSet, Layer, Politeness, RelationType, + Role as AtspiRole, ScrollType, State, StateSet, }; use std::{ collections::HashMap, @@ -829,6 +829,25 @@ impl PlatformNode { }) } + pub fn relation_set( + &self, + f: impl Fn(NodeId) -> T, + ) -> Result>> { + self.resolve(|node| { + let mut relations = HashMap::new(); + let controls: Vec<_> = node + .controls() + .filter(|controlled| filter(controlled) == FilterResult::Include) + .map(|controlled| controlled.id()) + .map(f) + .collect(); + if !controls.is_empty() { + relations.insert(RelationType::ControllerFor, controls); + } + Ok(relations) + }) + } + pub fn role(&self) -> Result { self.resolve(|node| { let wrapper = NodeWrapper(&node); diff --git a/platforms/atspi-common/src/simplified.rs b/platforms/atspi-common/src/simplified.rs index 33d83761e..a4c80ca89 100644 --- a/platforms/atspi-common/src/simplified.rs +++ b/platforms/atspi-common/src/simplified.rs @@ -14,7 +14,9 @@ use crate::{ WindowEvent, }; -pub use crate::{CoordType, Error, Granularity, Layer, Rect, Result, Role, ScrollType, StateSet}; +pub use crate::{ + CoordType, Error, Granularity, Layer, Rect, RelationType, Result, Role, ScrollType, StateSet, +}; #[derive(Clone, Hash, PartialEq)] pub enum Accessible { @@ -117,6 +119,13 @@ impl Accessible { } } + pub fn relation_set(&self) -> Result>> { + match self { + Self::Node(node) => node.relation_set(|id| Self::Node(node.relative(id))), + Self::Root(_) => Ok(HashMap::new()), + } + } + pub fn application(&self) -> Result { match self { Self::Node(node) => node.root().map(Self::Root), diff --git a/platforms/macos/src/node.rs b/platforms/macos/src/node.rs index 16c51aeba..014b5f718 100644 --- a/platforms/macos/src/node.rs +++ b/platforms/macos/src/node.rs @@ -324,6 +324,9 @@ impl NodeWrapper<'_> { if let Some(toggled) = self.0.toggled() { return Some(Value::Bool(toggled != Toggled::False)); } + if self.0.role() == Role::Tab { + return Some(Value::Bool(self.0.is_selected().unwrap_or(false))); + } if let Some(value) = self.0.value() { return Some(Value::String(value)); } From 341a11bca2c8a29682c11ddcfe91fa58776ea11d Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Thu, 26 Dec 2024 19:30:59 +0100 Subject: [PATCH 17/24] fix: Expose tabs in the platform adapters --- platforms/macos/src/event.rs | 47 ++++++---- platforms/macos/src/node.rs | 86 +++++++++++++++++-- .../unix/src/atspi/interfaces/accessible.rs | 22 ++++- platforms/windows/src/adapter.rs | 7 +- platforms/windows/src/node.rs | 40 ++++++++- platforms/windows/src/util.rs | 22 +++++ 6 files changed, 195 insertions(+), 29 deletions(-) diff --git a/platforms/macos/src/event.rs b/platforms/macos/src/event.rs index 13ea288a9..b1d4fcc95 100644 --- a/platforms/macos/src/event.rs +++ b/platforms/macos/src/event.rs @@ -11,7 +11,11 @@ use objc2_app_kit::*; use objc2_foundation::{NSMutableDictionary, NSNumber, NSString}; use std::rc::Rc; -use crate::{context::Context, filters::filter, node::NodeWrapper}; +use crate::{ + context::Context, + filters::filter, + node::{NodeWrapper, Value}, +}; // This type is designed to be safe to create on a non-main thread // and send to the main thread. This ability isn't yet used though. @@ -185,9 +189,6 @@ impl EventGenerator { } fn enqueue_selected_rows_change_if_needed_parent(&mut self, node: Node) { - if !node.is_container_with_selectable_children() { - return; - } let id = node.id(); if self.selected_rows_changed.contains(&id) { return; @@ -200,7 +201,8 @@ impl EventGenerator { } fn enqueue_selected_rows_change_if_needed(&mut self, node: &Node) { - if !node.is_item_like() { + let wrapper = NodeWrapper(node); + if !wrapper.is_item_like() { return; } if let Some(node) = node.selection_container(&filter) { @@ -230,10 +232,7 @@ impl TreeChangeHandler for EventGenerator { } let old_node_was_filtered_out = filter(old_node) != FilterResult::Include; if filter(new_node) != FilterResult::Include { - if !old_node_was_filtered_out - && old_node.is_item_like() - && old_node.is_selected() == Some(true) - { + if !old_node_was_filtered_out && old_node.is_selected() == Some(true) { self.enqueue_selected_rows_change_if_needed(old_node); } return; @@ -247,11 +246,26 @@ impl TreeChangeHandler for EventGenerator { notification: unsafe { NSAccessibilityTitleChangedNotification }, }); } - if old_wrapper.value() != new_wrapper.value() { - self.events.push(QueuedEvent::Generic { - node_id, - notification: unsafe { NSAccessibilityValueChangedNotification }, - }); + let new_value = new_wrapper.value(); + if old_wrapper.value() != new_value { + if !new_node.is_focused() && new_value.is_some_and(|v| matches!(v, Value::Bool(_))) { + // Bool value changed event for the focused node must come last + // in order for VoiceOver to announce it. Otherwise, if we raise + // bool value changed events for other nodes after this one, VoiceOver + // will announce them instead. + self.events.insert( + 0, + QueuedEvent::Generic { + node_id, + notification: unsafe { NSAccessibilityValueChangedNotification }, + }, + ); + } else { + self.events.push(QueuedEvent::Generic { + node_id, + notification: unsafe { NSAccessibilityValueChangedNotification }, + }); + } } if old_wrapper.supports_text_ranges() && new_wrapper.supports_text_ranges() @@ -271,9 +285,8 @@ impl TreeChangeHandler for EventGenerator { self.events .push(QueuedEvent::live_region_announcement(new_node)); } - if new_node.is_item_like() - && (new_node.is_selected() != old_node.is_selected() - || (old_node_was_filtered_out && new_node.is_selected() == Some(true))) + if new_node.is_selected() != old_node.is_selected() + || (old_node_was_filtered_out && new_node.is_selected() == Some(true)) { self.enqueue_selected_rows_change_if_needed(new_node); } diff --git a/platforms/macos/src/node.rs b/platforms/macos/src/node.rs index 014b5f718..13e1f0f8c 100644 --- a/platforms/macos/src/node.rs +++ b/platforms/macos/src/node.rs @@ -325,6 +325,8 @@ impl NodeWrapper<'_> { return Some(Value::Bool(toggled != Toggled::False)); } if self.0.role() == Role::Tab { + // On Mac, tabs are exposed as radio buttons, and are treated as checkable. + // Also, `Node::is_selected` is mapped to checked via `accessibilityValue`. return Some(Value::Bool(self.0.is_selected().unwrap_or(false))); } if let Some(value) = self.0.value() { @@ -343,6 +345,14 @@ impl NodeWrapper<'_> { pub(crate) fn raw_text_selection(&self) -> Option<&TextSelection> { self.0.raw_text_selection() } + + fn is_container_with_selectable_children(&self) -> bool { + self.0.is_container_with_selectable_children() && self.0.role() != Role::TabList + } + + pub(crate) fn is_item_like(&self) -> bool { + self.0.is_item_like() && self.0.role() != Role::Tab + } } pub(crate) struct PlatformNodeIvars { @@ -417,7 +427,8 @@ declare_class!( #[method_id(accessibilitySelectedChildren)] fn selected_children(&self) -> Option>> { self.resolve_with_context(|node, context| { - if !node.is_container_with_selectable_children() { + let wrapper = NodeWrapper(node); + if !wrapper.is_container_with_selectable_children() { return None; } let platform_nodes = node @@ -835,13 +846,23 @@ declare_class!( #[method(isAccessibilitySelected)] fn is_selected(&self) -> bool { - self.resolve(|node| node.is_selected()).flatten().unwrap_or(false) + self.resolve(|node| { + let wrapper = NodeWrapper(node); + wrapper.is_item_like() + && node.is_selectable() + && node.is_selected().unwrap_or(false) + }) + .unwrap_or(false) } #[method(setAccessibilitySelected:)] fn set_selected(&self, selected: bool) { self.resolve_with_context(|node, context| { - if !node.is_clickable() || !node.is_selectable() { + let wrapper = NodeWrapper(node); + if !node.is_clickable() + || !wrapper.is_item_like() + || !node.is_selectable() + { return; } if node.is_selected() == Some(selected) { @@ -858,7 +879,8 @@ declare_class!( #[method_id(accessibilityRows)] fn rows(&self) -> Option>> { self.resolve_with_context(|node, context| { - if !node.is_container_with_selectable_children() { + let wrapper = NodeWrapper(node); + if !wrapper.is_container_with_selectable_children() { return None; } let platform_nodes = node @@ -873,7 +895,8 @@ declare_class!( #[method_id(accessibilitySelectedRows)] fn selected_rows(&self) -> Option>> { self.resolve_with_context(|node, context| { - if !node.is_container_with_selectable_children() { + let wrapper = NodeWrapper(node); + if !wrapper.is_container_with_selectable_children() { return None; } let platform_nodes = node @@ -889,7 +912,10 @@ declare_class!( #[method(accessibilityPerformPick)] fn pick(&self) -> bool { self.resolve_with_context(|node, context| { - let selectable = node.is_clickable() && node.is_selectable(); + let wrapper = NodeWrapper(node); + let selectable = node.is_clickable() + && wrapper.is_item_like() + && node.is_selectable(); if selectable { context.do_action(ActionRequest { action: Action::Click, @@ -902,6 +928,39 @@ declare_class!( .unwrap_or(false) } + #[method_id(accessibilityLinkedUIElements)] + fn linked_ui_elements(&self) -> Option>> { + self.resolve_with_context(|node, context| { + let platform_nodes: Vec> = node + .controls() + .filter(|controlled| filter(controlled) == FilterResult::Include) + .map(|controlled| context.get_or_create_platform_node(controlled.id())) + .collect(); + if platform_nodes.is_empty() { + None + } else { + Some(NSArray::from_vec(platform_nodes)) + } + }) + .flatten() + } + + #[method_id(accessibilityTabs)] + fn tabs(&self) -> Option>> { + self.resolve_with_context(|node, context| { + if node.role() != Role::TabList { + return None; + } + let platform_nodes = node + .filtered_children(filter) + .filter(|child| child.role() == Role::Tab) + .map(|tab| context.get_or_create_platform_node(tab.id())) + .collect::>>(); + Some(NSArray::from_vec(platform_nodes)) + }) + .flatten() + } + #[method(isAccessibilitySelectorAllowed:)] fn is_selector_allowed(&self, selector: Sel) -> bool { self.resolve(|node| { @@ -939,17 +998,25 @@ declare_class!( return node.supports_text_ranges() && !node.is_read_only(); } if selector == sel!(isAccessibilitySelected) { - return node.is_selectable(); + let wrapper = NodeWrapper(node); + return wrapper.is_item_like(); } if selector == sel!(accessibilityRows) || selector == sel!(accessibilitySelectedRows) { - return node.is_container_with_selectable_children() + let wrapper = NodeWrapper(node); + return wrapper.is_container_with_selectable_children() } if selector == sel!(setAccessibilitySelected:) || selector == sel!(accessibilityPerformPick) { - return node.is_clickable() && node.is_selectable(); + let wrapper = NodeWrapper(node); + return node.is_clickable() + && wrapper.is_item_like() + && node.is_selectable(); + } + if selector == sel!(accessibilityTabs) { + return node.role() == Role::TabList; } selector == sel!(accessibilityParent) || selector == sel!(accessibilityChildren) @@ -961,6 +1028,7 @@ declare_class!( || selector == sel!(isAccessibilityEnabled) || selector == sel!(accessibilityWindow) || selector == sel!(accessibilityTopLevelUIElement) + || selector == sel!(accessibilityLinkedUIElements) || selector == sel!(accessibilityRoleDescription) || selector == sel!(accessibilityIdentifier) || selector == sel!(accessibilityTitle) diff --git a/platforms/unix/src/atspi/interfaces/accessible.rs b/platforms/unix/src/atspi/interfaces/accessible.rs index f809be010..16d50a447 100644 --- a/platforms/unix/src/atspi/interfaces/accessible.rs +++ b/platforms/unix/src/atspi/interfaces/accessible.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use accesskit_atspi_common::{NodeIdOrRoot, PlatformNode, PlatformRoot}; -use atspi::{Interface, InterfaceSet, Role, StateSet}; +use atspi::{Interface, InterfaceSet, RelationType, Role, StateSet}; use zbus::{fdo, interface, names::OwnedUniqueName}; use super::map_root_error; @@ -99,6 +99,22 @@ impl NodeAccessibleInterface { self.node.index_in_parent().map_err(self.map_error()) } + fn get_relation_set(&self) -> fdo::Result)>> { + self.node + .relation_set(|relation| { + ObjectId::Node { + adapter: self.node.adapter_id(), + node: relation, + } + .to_address(self.bus_name.inner()) + }) + .map(|set| { + set.into_iter() + .collect::)>>() + }) + .map_err(self.map_error()) + } + fn get_role(&self) -> fdo::Result { self.node.role().map_err(self.map_error()) } @@ -191,6 +207,10 @@ impl RootAccessibleInterface { -1 } + fn get_relation_set(&self) -> Vec<(RelationType, Vec)> { + Vec::new() + } + fn get_role(&self) -> Role { Role::Application } diff --git a/platforms/windows/src/adapter.rs b/platforms/windows/src/adapter.rs index e9b4f1488..d24387a7f 100644 --- a/platforms/windows/src/adapter.rs +++ b/platforms/windows/src/adapter.rs @@ -245,7 +245,12 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> { let element: IRawElementProviderSimple = platform_node.into(); let old_wrapper = NodeWrapper(old_node); let new_wrapper = NodeWrapper(new_node); - new_wrapper.enqueue_property_changes(&mut self.queue, &element, &old_wrapper); + new_wrapper.enqueue_property_changes( + &mut self.queue, + &PlatformNode::new(self.context, new_node.id()), + &element, + &old_wrapper, + ); let new_name = new_wrapper.name(); if new_name.is_some() && new_node.live() != Live::Off diff --git a/platforms/windows/src/node.rs b/platforms/windows/src/node.rs index d390318a8..e2ff22bd6 100644 --- a/platforms/windows/src/node.rs +++ b/platforms/windows/src/node.rs @@ -446,10 +446,11 @@ impl NodeWrapper<'_> { pub(crate) fn enqueue_property_changes( &self, queue: &mut Vec, + platform_node: &PlatformNode, element: &IRawElementProviderSimple, old: &NodeWrapper, ) { - self.enqueue_simple_property_changes(queue, element, old); + self.enqueue_simple_property_changes(queue, platform_node, element, old); self.enqueue_pattern_property_changes(queue, element, old); self.enqueue_property_implied_events(queue, element, old); } @@ -692,6 +693,16 @@ impl IRawElementProviderSimple_Impl for PlatformNode_Impl { match property_id { UIA_FrameworkIdPropertyId => result = state.toolkit_name().into(), UIA_ProviderDescriptionPropertyId => result = toolkit_description(state).into(), + UIA_ControllerForPropertyId => { + let controlled: Vec = node + .controls() + .filter(|controlled| filter(controlled) == FilterResult::Include) + .map(|controlled| self.relative(controlled.id())) + .map(IRawElementProviderSimple::from) + .filter_map(|controlled| controlled.cast::().ok()) + .collect(); + result = controlled.into(); + } _ => (), } } @@ -826,6 +837,7 @@ macro_rules! properties { fn enqueue_simple_property_changes( &self, queue: &mut Vec, + platform_node: &PlatformNode, element: &IRawElementProviderSimple, old: &NodeWrapper, ) { @@ -842,6 +854,32 @@ macro_rules! properties { ); } })* + + let mut old_controls = old.0.controls().filter(|controlled| filter(controlled) == FilterResult::Include); + let mut new_controls = self.0.controls().filter(|controlled| filter(controlled) == FilterResult::Include); + let mut are_equal = true; + let mut controls: Vec = Vec::new(); + loop { + let old_controlled = old_controls.next(); + let new_controlled = new_controls.next(); + match (old_controlled, new_controlled) { + (Some(a), Some(b)) => { + are_equal = are_equal && a.id() == b.id(); + controls.push(platform_node.relative(b.id()).into()); + } + (None, None) => break, + _ => are_equal = false, + } + } + if !are_equal { + self.enqueue_property_change( + queue, + &element, + UIA_ControllerForPropertyId, + Variant::empty(), + controls.into(), + ); + } } } }; diff --git a/platforms/windows/src/util.rs b/platforms/windows/src/util.rs index b2e9d38dd..a1260cebb 100644 --- a/platforms/windows/src/util.rs +++ b/platforms/windows/src/util.rs @@ -7,6 +7,7 @@ use accesskit::Point; use accesskit_consumer::TreeState; use std::{ fmt::{self, Write}, + mem::ManuallyDrop, sync::{Arc, Weak}, }; use windows::{ @@ -146,6 +147,27 @@ impl> From> for Variant { } } +impl From> for Variant { + fn from(value: Vec) -> Self { + if value.is_empty() { + Variant::empty() + } else { + let parray = safe_array_from_com_slice(&value); + Self(VARIANT { + Anonymous: VARIANT_0 { + Anonymous: ManuallyDrop::new(VARIANT_0_0 { + vt: VT_ARRAY | VT_UNKNOWN, + wReserved1: 0, + wReserved2: 0, + wReserved3: 0, + Anonymous: VARIANT_0_0_0 { parray }, + }), + }, + }) + } + } +} + fn safe_array_from_primitive_slice(vt: VARENUM, slice: &[T]) -> *mut SAFEARRAY { let sa = unsafe { SafeArrayCreateVector(VARENUM(vt.0), 0, slice.len().try_into().unwrap()) }; if sa.is_null() { From 735cb7e292b87e7660586a924954689e4894dcea Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Thu, 17 Apr 2025 15:33:52 -0500 Subject: [PATCH 18/24] fix: Fix Android adapter after dropping `FrozenNode` (#553) --- platforms/android/src/adapter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index a80a3679e..44bfbcb5b 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -447,7 +447,7 @@ impl Adapter { anchor: anchor.to_raw(), focus: focus.to_raw(), }; - let mut new_node = NodeData::from(node.data()); + let mut new_node = node.data().clone(); new_node.set_text_selection(selection); let update = TreeUpdate { nodes: vec![(node.id(), new_node)], From f9c97559f6817ef70b387b89a2ba1521e5f81f44 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 20 Apr 2025 21:33:55 +0200 Subject: [PATCH 19/24] test: Add unit tests to the common crate (#556) --- common/src/lib.rs | 633 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 598 insertions(+), 35 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index a48d7d046..7ff45c67c 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -923,10 +923,6 @@ impl PropertyIndices { } } -fn unexpected_property_type() -> ! { - panic!(); -} - impl Properties { fn get_mut(&mut self, id: PropertyId, default: PropertyValue) -> &mut PropertyValue { let index = self.indices.0[id as usize] as usize; @@ -936,9 +932,6 @@ impl Properties { self.indices.0[id as usize] = index as u8; &mut self.values[index] } else { - if matches!(self.values[index], PropertyValue::None) { - self.values[index] = default; - } &mut self.values[index] } } @@ -985,6 +978,31 @@ macro_rules! flag_methods { )* } } + $(#[cfg(test)] + mod $getter { + use super::{Node, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(!node.$getter()); + } + + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(); + assert!(node.$getter()); + } + + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(); + node.$clearer(); + assert!(!node.$getter()); + } + })* } } @@ -993,9 +1011,8 @@ macro_rules! option_ref_type_getters { impl PropertyIndices { $(fn $method<'a>(&self, values: &'a [PropertyValue], id: PropertyId) -> Option<&'a $type> { match self.get(values, id) { - PropertyValue::None => None, PropertyValue::$variant(value) => Some(value), - _ => unexpected_property_type(), + _ => None, } })* } @@ -1007,9 +1024,8 @@ macro_rules! slice_type_getters { impl PropertyIndices { $(fn $method<'a>(&self, values: &'a [PropertyValue], id: PropertyId) -> &'a [$type] { match self.get(values, id) { - PropertyValue::None => &[], PropertyValue::$variant(value) => value, - _ => unexpected_property_type(), + _ => &[], } })* } @@ -1021,9 +1037,8 @@ macro_rules! copy_type_getters { impl PropertyIndices { $(fn $method(&self, values: &[PropertyValue], id: PropertyId) -> Option<$type> { match self.get(values, id) { - PropertyValue::None => None, PropertyValue::$variant(value) => Some(*value), - _ => unexpected_property_type(), + _ => None, } })* } @@ -1060,11 +1075,8 @@ macro_rules! vec_type_methods { self.properties.set(id, PropertyValue::$variant(value.into())); } fn $pusher(&mut self, id: PropertyId, item: $type) { - match self.properties.get_mut(id, PropertyValue::$variant(Vec::new())) { - PropertyValue::$variant(v) => { - v.push(item); - } - _ => unexpected_property_type(), + if let PropertyValue::$variant(v) = self.properties.get_mut(id, PropertyValue::$variant(Vec::new())) { + v.push(item); } })* } @@ -1128,6 +1140,39 @@ macro_rules! node_id_vec_property_methods { impl Node { slice_properties_debug_method! { debug_node_id_vec_properties, [$($getter,)*] } } + $(#[cfg(test)] + mod $getter { + use super::{Node, NodeId, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.$getter().is_empty()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter([]); + assert!(node.$getter().is_empty()); + node.$setter([NodeId(0), NodeId(1)]); + assert_eq!(node.$getter(), &[NodeId(0), NodeId(1)]); + } + #[test] + fn pusher_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$pusher(NodeId(0)); + assert_eq!(node.$getter(), &[NodeId(0)]); + node.$pusher(NodeId(1)); + assert_eq!(node.$getter(), &[NodeId(0), NodeId(1)]); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter([NodeId(0)]); + node.$clearer(); + assert!(node.$getter().is_empty()); + } + })* } } @@ -1152,6 +1197,29 @@ macro_rules! node_id_property_methods { impl Node { option_properties_debug_method! { debug_node_id_properties, [$($getter,)*] } } + $(#[cfg(test)] + mod $getter { + use super::{Node, NodeId, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.$getter().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(NodeId(1)); + assert_eq!(node.$getter(), Some(NodeId(1))); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(NodeId(1)); + node.$clearer(); + assert!(node.$getter().is_none()); + } + })* } } @@ -1164,6 +1232,29 @@ macro_rules! string_property_methods { impl Node { option_properties_debug_method! { debug_string_properties, [$($getter,)*] } } + $(#[cfg(test)] + mod $getter { + use super::{Node, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.$getter().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter("test"); + assert_eq!(node.$getter(), Some("test")); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter("test"); + node.$clearer(); + assert!(node.$getter().is_none()); + } + })* } } @@ -1176,6 +1267,29 @@ macro_rules! f64_property_methods { impl Node { option_properties_debug_method! { debug_f64_properties, [$($getter,)*] } } + $(#[cfg(test)] + mod $getter { + use super::{Node, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.$getter().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(1.0); + assert_eq!(node.$getter(), Some(1.0)); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(1.0); + node.$clearer(); + assert!(node.$getter().is_none()); + } + })* } } @@ -1188,6 +1302,29 @@ macro_rules! usize_property_methods { impl Node { option_properties_debug_method! { debug_usize_properties, [$($getter,)*] } } + $(#[cfg(test)] + mod $getter { + use super::{Node, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.$getter().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(1); + assert_eq!(node.$getter(), Some(1)); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(1); + node.$clearer(); + assert!(node.$getter().is_none()); + } + })* } } @@ -1200,6 +1337,29 @@ macro_rules! color_property_methods { impl Node { option_properties_debug_method! { debug_color_properties, [$($getter,)*] } } + $(#[cfg(test)] + mod $getter { + use super::{Node, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.$getter().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(1); + assert_eq!(node.$getter(), Some(1)); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(1); + node.$clearer(); + assert!(node.$getter().is_none()); + } + })* } } @@ -1212,6 +1372,29 @@ macro_rules! text_decoration_property_methods { impl Node { option_properties_debug_method! { debug_text_decoration_properties, [$($getter,)*] } } + $(#[cfg(test)] + mod $getter { + use super::{Node, Role, TextDecoration}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.$getter().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(TextDecoration::Dotted); + assert_eq!(node.$getter(), Some(TextDecoration::Dotted)); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(TextDecoration::Dotted); + node.$clearer(); + assert!(node.$getter().is_none()); + } + })* } } @@ -1224,6 +1407,31 @@ macro_rules! length_slice_property_methods { impl Node { slice_properties_debug_method! { debug_length_slice_properties, [$($getter,)*] } } + $(#[cfg(test)] + mod $getter { + use super::{Node, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.$getter().is_empty()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter([]); + assert!(node.$getter().is_empty()); + node.$setter([1, 2]); + assert_eq!(node.$getter(), &[1, 2]); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter([1, 2]); + node.$clearer(); + assert!(node.$getter().is_empty()); + } + })* } } @@ -1236,6 +1444,33 @@ macro_rules! coord_slice_property_methods { impl Node { option_properties_debug_method! { debug_coord_slice_properties, [$($getter,)*] } } + $(#[cfg(test)] + mod $getter { + use super::{Node, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.$getter().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter([]); + let expected: Option<&[f32]> = Some(&[]); + assert_eq!(node.$getter(), expected); + node.$setter([1.0, 2.0]); + let expected: Option<&[f32]> = Some(&[1.0, 2.0]); + assert_eq!(node.$getter(), expected); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter([1.0, 2.0]); + node.$clearer(); + assert!(node.$getter().is_none()); + } + })* } } @@ -1248,19 +1483,41 @@ macro_rules! bool_property_methods { impl Node { option_properties_debug_method! { debug_bool_properties, [$($getter,)*] } } + $(#[cfg(test)] + mod $getter { + use super::{Node, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.$getter().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(true); + assert_eq!(node.$getter(), Some(true)); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(true); + node.$clearer(); + assert!(node.$getter().is_none()); + } + })* } } macro_rules! unique_enum_property_methods { - ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident)),+) => { + ($($(#[$doc:meta])* ($id:ident, $getter:ident, $setter:ident, $clearer:ident, $variant:ident)),+) => { impl Node { $($(#[$doc])* #[inline] pub fn $getter(&self) -> Option<$id> { match self.properties.indices.get(&self.properties.values, PropertyId::$id) { - PropertyValue::None => None, PropertyValue::$id(value) => Some(*value), - _ => unexpected_property_type(), + _ => None, } } #[inline] @@ -1273,6 +1530,30 @@ macro_rules! unique_enum_property_methods { })* option_properties_debug_method! { debug_unique_enum_properties, [$($getter,)*] } } + $(#[cfg(test)] + mod $getter { + use super::{Node, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.$getter().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + let variant = super::$id::$variant; + node.$setter(variant); + assert_eq!(node.$getter(), Some(variant)); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.$setter(super::$id::$variant); + node.$clearer(); + assert!(node.$getter().is_none()); + } + })* } } @@ -1634,19 +1915,19 @@ bool_property_methods! { } unique_enum_property_methods! { - (Invalid, invalid, set_invalid, clear_invalid), - (Toggled, toggled, set_toggled, clear_toggled), - (Live, live, set_live, clear_live), - (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), - (AutoComplete, auto_complete, set_auto_complete, clear_auto_complete), - (HasPopup, has_popup, set_has_popup, clear_has_popup), + (Invalid, invalid, set_invalid, clear_invalid, Grammar), + (Toggled, toggled, set_toggled, clear_toggled, True), + (Live, live, set_live, clear_live, Polite), + (TextDirection, text_direction, set_text_direction, clear_text_direction, RightToLeft), + (Orientation, orientation, set_orientation, clear_orientation, Vertical), + (SortDirection, sort_direction, set_sort_direction, clear_sort_direction, Descending), + (AriaCurrent, aria_current, set_aria_current, clear_aria_current, True), + (AutoComplete, auto_complete, set_auto_complete, clear_auto_complete, List), + (HasPopup, has_popup, set_has_popup, clear_has_popup, Menu), /// 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) + (ListStyle, list_style, set_list_style, clear_list_style, Disc), + (TextAlign, text_align, set_text_align, clear_text_align, Right), + (VerticalOffset, vertical_offset, set_vertical_offset, clear_vertical_offset, Superscript) } property_methods! { @@ -1683,10 +1964,165 @@ impl Node { option_properties_debug_method! { debug_option_properties, [transform, bounds, text_selection,] } } +#[cfg(test)] +mod transform { + use super::{Affine, Node, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.transform().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + node.set_transform(Affine::IDENTITY); + assert_eq!(node.transform(), Some(&Affine::IDENTITY)); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.set_transform(Affine::IDENTITY); + node.clear_transform(); + assert!(node.transform().is_none()); + } +} + +#[cfg(test)] +mod bounds { + use super::{Node, Rect, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.bounds().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + let value = Rect { + x0: 0.0, + y0: 1.0, + x1: 2.0, + y1: 3.0, + }; + node.set_bounds(value); + assert_eq!(node.bounds(), Some(value)); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.set_bounds(Rect { + x0: 0.0, + y0: 1.0, + x1: 2.0, + y1: 3.0, + }); + node.clear_bounds(); + assert!(node.bounds().is_none()); + } +} + +#[cfg(test)] +mod text_selection { + use super::{Node, NodeId, Role, TextPosition, TextSelection}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.text_selection().is_none()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + let value = TextSelection { + anchor: TextPosition { + node: NodeId(0), + character_index: 0, + }, + focus: TextPosition { + node: NodeId(0), + character_index: 2, + }, + }; + node.set_text_selection(value); + assert_eq!(node.text_selection(), Some(&value)); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.set_text_selection(TextSelection { + anchor: TextPosition { + node: NodeId(0), + character_index: 0, + }, + focus: TextPosition { + node: NodeId(0), + character_index: 2, + }, + }); + node.clear_text_selection(); + assert!(node.text_selection().is_none()); + } +} + 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(test)] +mod custom_actions { + use super::{CustomAction, Node, Role}; + + #[test] + fn getter_should_return_default_value() { + let node = Node::new(Role::Unknown); + assert!(node.custom_actions().is_empty()); + } + #[test] + fn setter_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + let value = alloc::vec![ + CustomAction { + id: 0, + description: "first test action".into(), + }, + CustomAction { + id: 1, + description: "second test action".into(), + }, + ]; + node.set_custom_actions(value.clone()); + assert_eq!(node.custom_actions(), value); + } + #[test] + fn pusher_should_update_the_property() { + let mut node = Node::new(Role::Unknown); + let first_action = CustomAction { + id: 0, + description: "first test action".into(), + }; + let second_action = CustomAction { + id: 1, + description: "second test action".into(), + }; + node.push_custom_action(first_action.clone()); + assert_eq!(node.custom_actions(), &[first_action.clone()]); + node.push_custom_action(second_action.clone()); + assert_eq!(node.custom_actions(), &[first_action, second_action]); + } + #[test] + fn clearer_should_reset_the_property() { + let mut node = Node::new(Role::Unknown); + node.set_custom_actions([CustomAction { + id: 0, + description: "test action".into(), + }]); + node.clear_custom_actions(); + assert!(node.custom_actions().is_empty()); + } +} + impl fmt::Debug for Node { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut fmt = f.debug_struct("Node"); @@ -2251,9 +2687,28 @@ pub trait DeactivationHandler { #[cfg(test)] mod tests { use super::*; + use alloc::format; #[test] - fn action_n() { + fn u64_should_be_convertible_to_node_id() { + assert_eq!(NodeId::from(0u64), NodeId(0)); + assert_eq!(NodeId::from(1u64), NodeId(1)); + } + + #[test] + fn node_id_should_be_convertible_to_u64() { + assert_eq!(u64::from(NodeId(0)), 0u64); + assert_eq!(u64::from(NodeId(1)), 1u64); + } + + #[test] + fn node_id_should_have_debug_repr() { + assert_eq!(&format!("{:?}", NodeId(0)), "#0"); + assert_eq!(&format!("{:?}", NodeId(1)), "#1"); + } + + #[test] + fn action_n_should_return_the_corresponding_variant() { assert_eq!(Action::n(0), Some(Action::Click)); assert_eq!(Action::n(1), Some(Action::Focus)); assert_eq!(Action::n(2), Some(Action::Blur)); @@ -2285,12 +2740,15 @@ mod tests { } #[test] - fn test_action_mask_to_action_vec() { + fn empty_action_mask_should_be_converted_to_empty_vec() { assert_eq!( Vec::::new(), action_mask_to_action_vec(Node::new(Role::Unknown).actions) ); + } + #[test] + fn action_mask_should_be_convertible_to_vec() { let mut node = Node::new(Role::Unknown); node.add_action(Action::Click); assert_eq!( @@ -2322,4 +2780,109 @@ mod tests { action_mask_to_action_vec(node.actions).as_slice() ); } + + #[test] + fn new_node_should_have_user_provided_role() { + let node = Node::new(Role::Button); + assert_eq!(node.role(), Role::Button); + } + + #[test] + fn node_role_setter_should_update_the_role() { + let mut node = Node::new(Role::Button); + node.set_role(Role::CheckBox); + assert_eq!(node.role(), Role::CheckBox); + } + + #[test] + fn new_node_should_not_support_anyaction() { + let node = Node::new(Role::Unknown); + assert!(!node.supports_action(Action::Click)); + assert!(!node.supports_action(Action::Focus)); + assert!(!node.supports_action(Action::Blur)); + assert!(!node.supports_action(Action::Collapse)); + assert!(!node.supports_action(Action::Expand)); + assert!(!node.supports_action(Action::CustomAction)); + assert!(!node.supports_action(Action::Decrement)); + assert!(!node.supports_action(Action::Increment)); + assert!(!node.supports_action(Action::HideTooltip)); + assert!(!node.supports_action(Action::ShowTooltip)); + assert!(!node.supports_action(Action::ReplaceSelectedText)); + assert!(!node.supports_action(Action::ScrollBackward)); + assert!(!node.supports_action(Action::ScrollDown)); + assert!(!node.supports_action(Action::ScrollForward)); + assert!(!node.supports_action(Action::ScrollLeft)); + assert!(!node.supports_action(Action::ScrollRight)); + assert!(!node.supports_action(Action::ScrollUp)); + assert!(!node.supports_action(Action::ScrollIntoView)); + assert!(!node.supports_action(Action::ScrollToPoint)); + assert!(!node.supports_action(Action::SetScrollOffset)); + assert!(!node.supports_action(Action::SetTextSelection)); + assert!(!node.supports_action(Action::SetSequentialFocusNavigationStartingPoint)); + assert!(!node.supports_action(Action::SetValue)); + assert!(!node.supports_action(Action::ShowContextMenu)); + } + + #[test] + fn node_add_action_should_add_the_action() { + let mut node = Node::new(Role::Unknown); + node.add_action(Action::Focus); + assert!(node.supports_action(Action::Focus)); + node.add_action(Action::Blur); + assert!(node.supports_action(Action::Blur)); + } + + #[test] + fn node_add_action_should_do_nothing_if_the_action_is_already_supported() { + let mut node = Node::new(Role::Unknown); + node.add_action(Action::Focus); + node.add_action(Action::Focus); + assert!(node.supports_action(Action::Focus)); + } + + #[test] + fn node_remove_action_should_remove_the_action() { + let mut node = Node::new(Role::Unknown); + node.add_action(Action::Blur); + node.remove_action(Action::Blur); + assert!(!node.supports_action(Action::Blur)); + } + + #[test] + fn node_clear_actions_should_remove_all_actions() { + let mut node = Node::new(Role::Unknown); + node.add_action(Action::Focus); + node.add_action(Action::Blur); + node.clear_actions(); + assert!(!node.supports_action(Action::Focus)); + assert!(!node.supports_action(Action::Blur)); + } + + #[test] + fn node_should_have_debug_repr() { + let mut node = Node::new(Role::Unknown); + node.add_action(Action::Click); + node.add_action(Action::Focus); + node.set_hidden(); + node.set_multiselectable(); + node.set_children([NodeId(0), NodeId(1)]); + node.set_active_descendant(NodeId(2)); + node.push_custom_action(CustomAction { + id: 0, + description: "test action".into(), + }); + + assert_eq!( + &format!("{:?}", node), + r#"Node { role: Unknown, actions: [Click, Focus], is_hidden: true, is_multiselectable: true, children: [#0, #1], active_descendant: #2, custom_actions: [CustomAction { id: 0, description: "test action" }] }"# + ); + } + + #[test] + fn new_tree_should_have_root_id() { + let tree = Tree::new(NodeId(1)); + assert_eq!(tree.root, NodeId(1)); + assert_eq!(tree.toolkit_name, None); + assert_eq!(tree.toolkit_version, None); + } } From 0316518b94cf1bc9755e67f0cf48e37c096975fa Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Fri, 25 Apr 2025 05:56:12 -0500 Subject: [PATCH 20/24] refactor!: Use the queued-events pattern in the Android adapter (#555) --- platforms/android/classes.dex | Bin 10064 -> 10028 bytes .../java/dev/accesskit/android/Delegate.java | 14 +- platforms/android/src/adapter.rs | 498 ++++++++++-------- platforms/android/src/inject.rs | 85 ++- platforms/android/src/lib.rs | 2 +- platforms/android/src/util.rs | 1 + 6 files changed, 318 insertions(+), 282 deletions(-) diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index 8685c5d7c65ac3c4f46e6b60f314d5d60675cb77..f0b648ed071282fbdf8c77750ddec1fee4e2084b 100644 GIT binary patch delta 3005 zcmZ9O4@_0p9mjv?zW3n$10V1C_$%<>2_pCa%T#C|O8rMs)U|z`h)w3Y&N4JAbseC9Rr;E_Ja}H>b8uGupIiYqw~(bz@1lbeUU8tJ&5tw`A?YvVG3w(!6mV zpL4$V`#bmix#!$-`PJ65TPyDW%7DMIX@yMw@Pl9UTwWHr=lngN{I|FM!R%{ems5Ld z4lR4KBBFYpT+x({EfNU^L^c+RY=*mGKRg3(z=seh5=lTKd=A#ZM(BVoa5rp+d*O@l zW!Me9un!Kx!!QQV!&@*7(PF8T+G3Fn@O5|F&KlBa0-3|7vLrMDZCAnrIMCEAwGtG zLspqc7|Ng)R>E4?3f=Gk9D)(}4jhN?!^?0PK7fzGTP{)!%b^uIpbK`vL-06^!ZYP* zk@JWbVFIqh-{Dj6#6(J=1{z@{w80&4C+va;VE~5UTW}iA!Rv4p{s`0Hs}PC92Izu= za14HsCi@Az2ET+4;h$htiWEaVtcGpS4SQh#M&KzJhl?-)KZoDL6x;-fi-e#O60izx zhZO7tFIDtn*Qko=7hLhyngWqrEDIXZ3mlz9ha9~=!|z0g@m-MAQFlXY42M5q*Gmeq zkTM61A9eIDbiSi|9N+Z1OAoml(x&E0a|f95ta9`LbhD!eGIU{vHZ8X}bxg~*I@*ln zHb)Plla3xmw>o+py;|6fF^scZtZ@RD&~qJ6WcY7p_*XNuc_?kpy^|UK`{>*8Sz0@- zb@UYeI!8|zqfC$&d_GicQ}3<{+teF_-3F%erB~z znN1nKn8V#_Q>kRr2%aE8cS z{2K8}CvG-_Nty9ah&MTLvq4PCjEAa4HaT&#X-p!X{;Y!U`%$(Y@m(!kQ3)Y z*vafRlgtFVh(GJZIX5K_%#$$t%EV22#fiJcEsE>=qB=iNrh4>>z;<;|9}ASI0sVYn zjT+Q90&z90bAoYoR4)%MQQy@&$WQ2@V4XUp&ykX1y=G9pkltKBd7LUyG*l!@F*z09>I9(5u z?*;QR7|fC8;%AmLs*6J9YLRXUty4AnaHyoEp6%u#(w}`Vx!*nvJvhc|c4B?0Z4$a= zHkQrmnf0sJ?}kcLoBkVlyY}UksL$)Fyc24-ejUAA{~@oSXb<I|jb& zIwxG9`gB!ziQ1#LgbRw=sOhF=Ut>x_RxaB77=b=aw0$m`>S7$Zl4bwQ7|zkoBl@jy zTs^99kbhg}M9S4uIuVJh1G{ku{bPM9@{xK+A6wX<-qWuv+-BX#*SXPp>tFeL zb@Y4be<&rn$(_lEdw;z-ap6d^emL2i+$rx^*jp0A!|}u$!mFj%4QZps6NL-axk)^`QYWe1%dnT?1$KB5@R!3qJCYz3t9jzdHI!-oeyuX_4 zu`04D+cQB>$ovc#+(b(*D+8D24OjRl0W7W!kx?1_(qnx3f zs!{&2rIbr4Yweq9phHc@JZh%G?9QukGx6CE^Ka4bgPHVHhW<;2zL}vvH9AdZzXhCR zb2n!@`fD7)CYg>*S_(aQgY*14$3HsHPdNV5^ZW+Ke-2-JtAiEG9pn7GicO9`p$}Id z>bj5fPw{H<|CfEpTQR=JX!BpR8|_2e&jam6o3D?H`o=fI&cZkEq4_eISKLgiheQ5< D0HLo` delta 2984 zcmZ9Odu&w46^GB$7u$c^aH;3|p1P+`-Bd<}*{!zs87zEY89SPv=afhS=IHbWYQVFb3qHW-7Q z@Ev#^z6Wo@NjMKzps-A&wM;6c8}Su*1O5U<!upM^6ZumYNfFHmi_%R%XpTV!-Ec^~G!H4iC_#`G7`4_@nArgWZ z)I$rbga@D(1|SX3!%jE=hv6MK1@FQ%T!Gw5#)1aufD~+mufhmygT3%0I98bvnL_*l zK7fzFALjwWT&RO~=!7ppFMJs`!8c(W?16nS0h4eJF2Kid4Xi4W2sFZ47=#h{E*!~V z-h+$qSGWP*c_PJ718uM#o`A2z^Y9AngTrtFPQ&}~5nP4O!IKb)LIRe+8h8l$pdW_e z+o0pF-DxC3L-_aY<1&$uf@;`+R<9ESE9d$Rit;WMw$ar}MQAiyG zL6`1|qS(syV1%|Ziw%x;_7 zLmfWWl>2i_EzpH^F@<>M{}Xi4Va(~kn(UH2KtJ&bC(eVg zi^(=q%ndw8{EQRl+>{WQCt>o+#7&uT;%;$^qWxl27x^pH4&CB^L}|UtU#|A*xBV;B zL4C!aP(RecKtfIGMS=P1gzm-uQoj&rQ19x)*mL@WK(l&ZdxOntTDJzzsekI6yx~k9 zgPVyhZu2+PVF`yC0~4s!;bKV_tl~xTTj};TS$xhHYz>a{dM*n#NLe6Pw#1d3XSvHH zA!U+dw}awSt;!{J+|`!2%fxeM6%kKra(2uBW2PobL}gA4=dL3ceExiYRxpq{SR(26 zwK9FgL`2Fgw>?}FgG4M*OB&r0E~&G^bBG08#4IzFH){-6>JStBbl8*Rq_{o)YI4pO zSTZ$7Oy5lS^K^WTOw%ZlEd?eajfw`9q!%uisbIdchosNc9eoOevXx&4r&mM3ykT{@ z=4#ug^39%EIrdvgN3r+NWIeb05_S{ZA2$ZfIVVclwHxW;W3FGW|D9i{R_W4$`&75y zP*C2QVxw}140|5K`s~xvWQe)yaVM5;e^3e*&&E7#ui4;~K3Y(&9@pow1NsVfi_Qt{ zQ=|H=P&xKgsJQfHt~aM@uPPO@{Mj{_GxF0=v3f=4gy*Z5byK*w)XZdNOfO7Hfn}0t zb_IL&2BHJEqNyI%ktezK$E;yjxR`W36i%qO^?TT(`qOZwI;n$^g!;8!6lo?=pFm&I z+as4r(anX;>OcC;!UwJVV*QW8yR2BTu81DU+~8T|B_B;bJM{a`rgwHE?;1-EB_EYb z7VZa{#>Ns&r^n{WF3a3ao^?;}X{$HZwBwD6u8OXvC#~T@Ot80N(@4c8RL|hpP~V94 zRIIOWr_~bc+91~y8W|+>`_4=ZEPt-gW$9re+yamL1((U#B201Psz3%Pe+Hi`@Yz9?Dbg8W4#*JN2|*+`>WN~gYGlcYDaw9 zm{+SXlg6A(U_LXx@fys8F{7q=)q zw{>2Yj%Mk(SvrxW7dX0fFNeA*ChtwDhjDxp`p-3ym{CXW=+Wf#n9`xz*&b)_=&{J@ zaZ#_VjpUdyW^`}uNY9I$`ihs1zg_m3ZpHZzg*Jbf-Doe`zF9O!jQwNdqQCK*QF8H{ Qx9X~y55HOSIA{9*0QBg+hyVZp diff --git a/platforms/android/java/dev/accesskit/android/Delegate.java b/platforms/android/java/dev/accesskit/android/Delegate.java index 3abca62c0..79498355b 100644 --- a/platforms/android/java/dev/accesskit/android/Delegate.java +++ b/platforms/android/java/dev/accesskit/android/Delegate.java @@ -218,7 +218,8 @@ private static native boolean populateNodeInfo( private static native int getVirtualViewAtPoint(long adapterHandle, float x, float y); - private static native boolean performAction(long adapterHandle, int virtualViewId, int action); + private static native boolean performAction( + long adapterHandle, View host, int virtualViewId, int action); private static native boolean setTextSelection( long adapterHandle, View host, int virtualViewId, int anchor, int focus); @@ -324,16 +325,7 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) { forward, extendSelection); } - if (!Delegate.performAction(adapterHandle, virtualViewId, action)) { - return false; - } - switch (action) { - case AccessibilityNodeInfo.ACTION_CLICK: - sendEventInternal( - host, virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED); - break; - } - return true; + return Delegate.performAction(adapterHandle, host, virtualViewId, action); } @Override diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index 44bfbcb5b..0de31b70f 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -22,36 +22,151 @@ use jni::{ use crate::{filters::filter, node::NodeWrapper, util::*}; -fn send_event( - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, - virtual_view_id: jint, - event_type: jint, -) { - env.call_static_method( - callback_class, - "sendEvent", - "(Landroid/view/View;II)V", - &[host.into(), virtual_view_id.into(), event_type.into()], - ) - .unwrap(); +enum QueuedEvent { + Simple { + virtual_view_id: jint, + event_type: jint, + }, + TextChanged { + virtual_view_id: jint, + old: Option, + new: Option, + }, + TextSelectionChanged { + virtual_view_id: jint, + text: String, + start: jint, + end: jint, + }, + TextTraversed { + virtual_view_id: jint, + granularity: jint, + forward: bool, + segment_start: jint, + segment_end: jint, + }, +} + +/// Events generated by a tree update or accessibility action. +#[must_use = "events must be explicitly raised"] +pub struct QueuedEvents(Vec); + +impl QueuedEvents { + /// Raise all queued events. + /// + /// The `callback_class` parameter refers to a Java class with methods + /// that actually send the events. The reference implementation of the + /// contract for this class is `dev.accesskit.android.Delegate`, the + /// source code for which is in the `java` directory of this crate. + /// The methods of that class that are called by this function are all + /// marked `public static` and have names starting with `send`. + /// + /// Whether it is safe to call this function on a thread other than the + /// UI thread depends on the implementation of the `send` methods in + /// `callback_class`. If these methods dispatch events asynchronously + /// to the UI thread, as the implementations in + /// `dev.accesskit.android.Delegate` do, then it is safe to call this + /// function on any thread without concern for reentrancy. If they + /// synchronously call the Android framework to send the events, + /// then this function must be called on the UI thread, while not holding + /// any locks required by the host view's implementations of Android + /// framework callbacks. + /// + /// The `host` paramter is the Android view for the adapter that + /// returned this struct. It must be an instance of `android.view.View` + /// or a subclass. + pub fn raise(self, env: &mut JNIEnv, callback_class: &JClass, host: &JObject) { + for event in self.0 { + match event { + QueuedEvent::Simple { + virtual_view_id, + event_type, + } => { + env.call_static_method( + callback_class, + "sendEvent", + "(Landroid/view/View;II)V", + &[host.into(), virtual_view_id.into(), event_type.into()], + ) + .unwrap(); + } + QueuedEvent::TextChanged { + virtual_view_id, + old, + new, + } => { + let old = env.new_string(old.unwrap_or_else(String::new)).unwrap(); + let new = env.new_string(new.unwrap_or_else(String::new)).unwrap(); + env.call_static_method( + callback_class, + "sendTextChanged", + "(Landroid/view/View;ILjava/lang/String;Ljava/lang/String;)V", + &[ + host.into(), + virtual_view_id.into(), + (&old).into(), + (&new).into(), + ], + ) + .unwrap(); + } + QueuedEvent::TextSelectionChanged { + virtual_view_id, + text, + start, + end, + } => { + let text = env.new_string(text).unwrap(); + env.call_static_method( + callback_class, + "sendTextSelectionChanged", + "(Landroid/view/View;ILjava/lang/String;II)V", + &[ + host.into(), + virtual_view_id.into(), + (&text).into(), + start.into(), + end.into(), + ], + ) + .unwrap(); + } + QueuedEvent::TextTraversed { + virtual_view_id, + granularity, + forward, + segment_start, + segment_end, + } => { + env.call_static_method( + callback_class, + "sendTextTraversed", + "(Landroid/view/View;IIZII)V", + &[ + host.into(), + virtual_view_id.into(), + granularity.into(), + forward.into(), + segment_start.into(), + segment_end.into(), + ], + ) + .unwrap(); + } + } + } + } } -fn send_window_content_changed(env: &mut JNIEnv, callback_class: &JClass, host: &JObject) { - send_event( - env, - callback_class, - host, - HOST_VIEW_ID, - EVENT_WINDOW_CONTENT_CHANGED, - ); +fn enqueue_window_content_changed(events: &mut Vec) { + events.push(QueuedEvent::Simple { + virtual_view_id: HOST_VIEW_ID, + event_type: EVENT_WINDOW_CONTENT_CHANGED, + }); } -fn send_focus_event_if_applicable( - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, +fn enqueue_focus_event_if_applicable( + events: &mut Vec, node_id_map: &mut NodeIdMap, node: &Node, ) { @@ -59,52 +174,46 @@ fn send_focus_event_if_applicable( return; } let id = node_id_map.get_or_create_java_id(node); - send_event(env, callback_class, host, id, EVENT_VIEW_FOCUSED); + events.push(QueuedEvent::Simple { + virtual_view_id: id, + event_type: EVENT_VIEW_FOCUSED, + }); } -struct AdapterChangeHandler<'a, 'b, 'c, 'd> { - env: &'a mut JNIEnv<'b>, - callback_class: &'a JClass<'c>, - host: &'a JObject<'d>, +struct AdapterChangeHandler<'a> { + events: &'a mut Vec, node_id_map: &'a mut NodeIdMap, - sent_window_content_changed: bool, + enqueued_window_content_changed: bool, } -impl<'a, 'b, 'c, 'd> AdapterChangeHandler<'a, 'b, 'c, 'd> { - fn new( - env: &'a mut JNIEnv<'b>, - callback_class: &'a JClass<'c>, - host: &'a JObject<'d>, - node_id_map: &'a mut NodeIdMap, - ) -> Self { +impl<'a> AdapterChangeHandler<'a> { + fn new(events: &'a mut Vec, node_id_map: &'a mut NodeIdMap) -> Self { Self { - env, - callback_class, - host, + events, node_id_map, - sent_window_content_changed: false, + enqueued_window_content_changed: false, } } } -impl AdapterChangeHandler<'_, '_, '_, '_> { - fn send_window_content_changed_if_needed(&mut self) { - if self.sent_window_content_changed { +impl AdapterChangeHandler<'_> { + fn enqueue_window_content_changed_if_needed(&mut self) { + if self.enqueued_window_content_changed { return; } - send_window_content_changed(self.env, self.callback_class, self.host); - self.sent_window_content_changed = true; + enqueue_window_content_changed(self.events); + self.enqueued_window_content_changed = true; } } -impl TreeChangeHandler for AdapterChangeHandler<'_, '_, '_, '_> { +impl TreeChangeHandler for AdapterChangeHandler<'_> { fn node_added(&mut self, _node: &Node) { - self.send_window_content_changed_if_needed(); + self.enqueue_window_content_changed_if_needed(); // TODO: live regions? } fn node_updated(&mut self, old_node: &Node, new_node: &Node) { - self.send_window_content_changed_if_needed(); + self.enqueue_window_content_changed_if_needed(); if filter(new_node) != FilterResult::Include { return; } @@ -114,27 +223,11 @@ impl TreeChangeHandler for AdapterChangeHandler<'_, '_, '_, '_> { let new_text = new_wrapper.text(); if old_text != new_text { let id = self.node_id_map.get_or_create_java_id(new_node); - let old_text = self - .env - .new_string(old_text.unwrap_or_else(String::new)) - .unwrap(); - let new_text = self - .env - .new_string(new_text.clone().unwrap_or_else(String::new)) - .unwrap(); - self.env - .call_static_method( - self.callback_class, - "sendTextChanged", - "(Landroid/view/View;ILjava/lang/String;Ljava/lang/String;)V", - &[ - self.host.into(), - id.into(), - (&old_text).into(), - (&new_text).into(), - ], - ) - .unwrap(); + self.events.push(QueuedEvent::TextChanged { + virtual_view_id: id, + old: old_text, + new: new_text.clone(), + }); } if old_node.raw_text_selection() != new_node.raw_text_selection() || (new_node.raw_text_selection().is_some() @@ -143,21 +236,12 @@ impl TreeChangeHandler for AdapterChangeHandler<'_, '_, '_, '_> { if let Some((start, end)) = new_wrapper.text_selection() { if let Some(text) = new_text { let id = self.node_id_map.get_or_create_java_id(new_node); - let text = self.env.new_string(text).unwrap(); - self.env - .call_static_method( - self.callback_class, - "sendTextSelectionChanged", - "(Landroid/view/View;ILjava/lang/String;II)V", - &[ - self.host.into(), - id.into(), - (&text).into(), - (start as jint).into(), - (end as jint).into(), - ], - ) - .unwrap(); + self.events.push(QueuedEvent::TextSelectionChanged { + virtual_view_id: id, + text, + start: start as jint, + end: end as jint, + }); } } } @@ -166,18 +250,12 @@ impl TreeChangeHandler for AdapterChangeHandler<'_, '_, '_, '_> { fn focus_moved(&mut self, _old_node: Option<&Node>, new_node: Option<&Node>) { if let Some(new_node) = new_node { - send_focus_event_if_applicable( - self.env, - self.callback_class, - self.host, - self.node_id_map, - new_node, - ); + enqueue_focus_event_if_applicable(self.events, self.node_id_map, new_node); } } fn node_removed(&mut self, _node: &Node) { - self.send_window_content_changed_if_needed(); + self.enqueue_window_content_changed_if_needed(); // TODO: other events? } } @@ -227,14 +305,12 @@ impl State { } fn update_tree( - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, + events: &mut Vec, node_id_map: &mut NodeIdMap, tree: &mut Tree, update: TreeUpdate, ) { - let mut handler = AdapterChangeHandler::new(env, callback_class, host, node_id_map); + let mut handler = AdapterChangeHandler::new(events, node_id_map); tree.update_and_process_changes(update, &mut handler); } @@ -246,19 +322,6 @@ fn update_tree( /// glue code. For a higher-level implementation built on this type, see /// [`InjectingAdapter`]. /// -/// Several of this type's functions have a `callback_class` parameter. -/// The reference implementation of the duck-typed contract for this Java class -/// is `dev.accesskit.android.Delegate`, the source code for which is in the -/// `java` directory of this crate. The methods that are called from native -/// code are all marked `public static`, and so far, all of them that are -/// called by this type (rather than [`InjectingAdapter`]) are for sending -/// events. Other implementations may differ by, for example, sending those -/// events synchronously rather than posting them to the UI thread for -/// asynchronous handling. -/// -/// Several of this type's functions have a `host` parameter. This is always -/// a Java object whose class must derive from `android.view.View`. -/// /// [`InjectingAdapter`]: crate::InjectingAdapter #[derive(Debug, Default)] pub struct Adapter { @@ -272,43 +335,43 @@ impl Adapter { /// [`ActivationHandler::request_initial_tree`] initially returned `None`, /// the [`TreeUpdate`] returned by the provided function must contain /// a full tree. + /// + /// If a [`QueuedEvents`] instance is returned, the caller must call + /// [`QueuedEvents::raise`] on it. + /// + /// This method may be safely called on any thread, but refer to + /// [`QueuedEvents::raise`] for restrictions on the context in which + /// it should be called. pub fn update_if_active( &mut self, update_factory: impl FnOnce() -> TreeUpdate, - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, - ) { + ) -> Option { match &mut self.state { - State::Inactive => (), + State::Inactive => None, State::Placeholder(_) => { let tree = Tree::new(update_factory(), true); - send_window_content_changed(env, callback_class, host); + let mut events = Vec::new(); + enqueue_window_content_changed(&mut events); let state = tree.state(); if let Some(focus) = state.focus() { - send_focus_event_if_applicable( - env, - callback_class, - host, - &mut self.node_id_map, - &focus, - ); + enqueue_focus_event_if_applicable(&mut events, &mut self.node_id_map, &focus); } self.state = State::Active(tree); + Some(QueuedEvents(events)) } State::Active(tree) => { - update_tree( - env, - callback_class, - host, - &mut self.node_id_map, - tree, - update_factory(), - ); + let mut events = Vec::new(); + update_tree(&mut events, &mut self.node_id_map, tree, update_factory()); + Some(QueuedEvents(events)) } } } + /// Populate an `AccessibilityNodeInfo` with information about the + /// AccessKit node identified by the given virtual view ID. + /// + /// The `host` paramter is the Android view for this adapter. + /// It must be an instance of `android.view.View` or a subclass. #[allow(clippy::too_many_arguments)] pub fn populate_node_info( &mut self, @@ -371,23 +434,28 @@ impl Adapter { self.node_id_map.get_or_create_java_id(&node) } + /// Perform the specified accessibility action. + /// + /// If a [`QueuedEvents`] instance is returned, the caller must call + /// [`QueuedEvents::raise`] on it, and the Java `performAction` method + /// must return `true`. Otherwise, the Java `performAction` method + /// must either handle the action some other way or return false. + /// + /// This method may be safely called on any thread, but refer to + /// [`QueuedEvents::raise`] for restrictions on the context in which + /// it should be called. pub fn perform_action( &mut self, action_handler: &mut H, virtual_view_id: jint, action: jint, - ) -> bool { - let Some(tree) = self.state.get_full_tree() else { - return false; - }; + ) -> Option { + let tree = self.state.get_full_tree()?; let tree_state = tree.state(); let target = if virtual_view_id == HOST_VIEW_ID { tree_state.root_id() } else { - let Some(accesskit_id) = self.node_id_map.get_accesskit_id(virtual_view_id) else { - return false; - }; - accesskit_id + self.node_id_map.get_accesskit_id(virtual_view_id)? }; let request = match action { ACTION_CLICK => ActionRequest { @@ -408,19 +476,24 @@ impl Adapter { data: None, }, _ => { - return false; + return None; } }; action_handler.do_action(request); - true + let mut events = Vec::new(); + if action == ACTION_CLICK { + events.push(QueuedEvent::Simple { + virtual_view_id, + event_type: EVENT_VIEW_CLICKED, + }); + } + Some(QueuedEvents(events)) } fn set_text_selection_common( &mut self, action_handler: &mut H, - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, + events: &mut Vec, virtual_view_id: jint, selection_factory: F, ) -> Option @@ -454,14 +527,7 @@ impl Adapter { tree: None, focus: tree_state.focus_id_in_tree(), }; - update_tree( - env, - callback_class, - host, - &mut self.node_id_map, - tree, - update, - ); + update_tree(events, &mut self.node_id_map, tree, update); let request = ActionRequest { target, action: Action::SetTextSelection, @@ -471,72 +537,78 @@ impl Adapter { Some(extra) } - #[allow(clippy::too_many_arguments)] + /// Set the text selection of the specified virtual view to the specified + /// endpoints. + /// + /// If a [`QueuedEvents`] instance is returned, the caller must call + /// [`QueuedEvents::raise`] on it, and the Java `performAction` method + /// must return `true`. Otherwise, the Java `performAction` method + /// must either handle the action some other way or return false. + /// + /// This method may be safely called on any thread, but refer to + /// [`QueuedEvents::raise`] for restrictions on the context in which + /// it should be called. pub fn set_text_selection( &mut self, action_handler: &mut H, - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, virtual_view_id: jint, anchor: jint, focus: jint, - ) -> bool { - self.set_text_selection_common( - action_handler, - env, - callback_class, - host, - virtual_view_id, - |node| { - let anchor = usize::try_from(anchor).ok()?; - let anchor = node.text_position_from_global_utf16_index(anchor)?; - let focus = usize::try_from(focus).ok()?; - let focus = node.text_position_from_global_utf16_index(focus)?; - Some((anchor, focus, ())) - }, - ) - .is_some() + ) -> Option { + let mut events = Vec::new(); + self.set_text_selection_common(action_handler, &mut events, virtual_view_id, |node| { + let anchor = usize::try_from(anchor).ok()?; + let anchor = node.text_position_from_global_utf16_index(anchor)?; + let focus = usize::try_from(focus).ok()?; + let focus = node.text_position_from_global_utf16_index(focus)?; + Some((anchor, focus, ())) + })?; + Some(QueuedEvents(events)) } + /// Collapse the text selection of the specified virtual view. + /// + /// If a [`QueuedEvents`] instance is returned, the caller must call + /// [`QueuedEvents::raise`] on it, and the Java `performAction` method + /// must return `true`. Otherwise, the Java `performAction` method + /// must either handle the action some other way or return false. + /// + /// This method may be safely called on any thread, but refer to + /// [`QueuedEvents::raise`] for restrictions on the context in which + /// it should be called. pub fn collapse_text_selection( &mut self, action_handler: &mut H, - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, virtual_view_id: jint, - ) -> bool { - self.set_text_selection_common( - action_handler, - env, - callback_class, - host, - virtual_view_id, - |node| node.text_selection_focus().map(|pos| (pos, pos, ())), - ) - .is_some() + ) -> Option { + let mut events = Vec::new(); + self.set_text_selection_common(action_handler, &mut events, virtual_view_id, |node| { + node.text_selection_focus().map(|pos| (pos, pos, ())) + })?; + Some(QueuedEvents(events)) } - #[allow(clippy::too_many_arguments)] + /// Traverse the text in the specified virtual view. + /// + /// If a [`QueuedEvents`] instance is returned, the caller must call + /// [`QueuedEvents::raise`] on it, and the Java `performAction` method + /// must return `true`. Otherwise, the Java `performAction` method + /// must either handle the action some other way or return false. + /// + /// This method may be safely called on any thread, but refer to + /// [`QueuedEvents::raise`] for restrictions on the context in which + /// it should be called. pub fn traverse_text( &mut self, action_handler: &mut H, - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, virtual_view_id: jint, granularity: jint, forward: bool, extend_selection: bool, - ) -> bool { - let Some((segment_start, segment_end)) = self.set_text_selection_common( - action_handler, - env, - callback_class, - host, - virtual_view_id, - |node| { + ) -> Option { + let mut events = Vec::new(); + let (segment_start, segment_end) = + self.set_text_selection_common(action_handler, &mut events, virtual_view_id, |node| { let current = node.text_selection_focus().unwrap_or_else(|| { let range = node.document_range(); if forward { @@ -668,24 +740,14 @@ impl Adapter { segment_end.to_global_utf16_index(), ), )) - }, - ) else { - return false; - }; - env.call_static_method( - callback_class, - "sendTextTraversed", - "(Landroid/view/View;IIZII)V", - &[ - host.into(), - virtual_view_id.into(), - granularity.into(), - forward.into(), - (segment_start as jint).into(), - (segment_end as jint).into(), - ], - ) - .unwrap(); - true + })?; + events.push(QueuedEvent::TextTraversed { + virtual_view_id, + granularity, + forward, + segment_start: segment_start as jint, + segment_end: segment_end as jint, + }); + Some(QueuedEvents(events)) } } diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index ebe399be7..ba4c52d89 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -28,7 +28,10 @@ use std::{ }, }; -use crate::{adapter::Adapter, util::*}; +use crate::{ + adapter::{Adapter, QueuedEvents}, + util::*, +}; struct InnerInjectingAdapter { adapter: Adapter, @@ -76,63 +79,36 @@ impl InnerInjectingAdapter { .virtual_view_at_point(&mut *self.activation_handler, x, y) } - fn perform_action(&mut self, virtual_view_id: jint, action: jint) -> bool { + fn perform_action(&mut self, virtual_view_id: jint, action: jint) -> Option { self.adapter .perform_action(&mut *self.action_handler, virtual_view_id, action) } fn set_text_selection( &mut self, - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, virtual_view_id: jint, anchor: jint, focus: jint, - ) -> bool { - self.adapter.set_text_selection( - &mut *self.action_handler, - env, - callback_class, - host, - virtual_view_id, - anchor, - focus, - ) + ) -> Option { + self.adapter + .set_text_selection(&mut *self.action_handler, virtual_view_id, anchor, focus) } - fn collapse_text_selection( - &mut self, - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, - virtual_view_id: jint, - ) -> bool { - self.adapter.collapse_text_selection( - &mut *self.action_handler, - env, - callback_class, - host, - virtual_view_id, - ) + fn collapse_text_selection(&mut self, virtual_view_id: jint) -> Option { + self.adapter + .collapse_text_selection(&mut *self.action_handler, virtual_view_id) } #[allow(clippy::too_many_arguments)] fn traverse_text( &mut self, - env: &mut JNIEnv, - callback_class: &JClass, - host: &JObject, virtual_view_id: jint, granularity: jint, forward: bool, extend_selection: bool, - ) -> bool { + ) -> Option { self.adapter.traverse_text( &mut *self.action_handler, - env, - callback_class, - host, virtual_view_id, granularity, forward, @@ -204,9 +180,10 @@ extern "system" fn get_virtual_view_at_point( } extern "system" fn perform_action( - _env: JNIEnv, - _class: JClass, + mut env: JNIEnv, + class: JClass, adapter_handle: jlong, + host: JObject, virtual_view_id: jint, action: jint, ) -> jboolean { @@ -214,7 +191,8 @@ extern "system" fn perform_action( return JNI_FALSE; }; let mut inner_adapter = inner_adapter.lock().unwrap(); - if inner_adapter.perform_action(virtual_view_id, action) { + if let Some(events) = inner_adapter.perform_action(virtual_view_id, action) { + events.raise(&mut env, &class, &host); JNI_TRUE } else { JNI_FALSE @@ -234,7 +212,8 @@ extern "system" fn set_text_selection( return JNI_FALSE; }; let mut inner_adapter = inner_adapter.lock().unwrap(); - if inner_adapter.set_text_selection(&mut env, &class, &host, virtual_view_id, anchor, focus) { + if let Some(events) = inner_adapter.set_text_selection(virtual_view_id, anchor, focus) { + events.raise(&mut env, &class, &host); JNI_TRUE } else { JNI_FALSE @@ -252,7 +231,8 @@ extern "system" fn collapse_text_selection( return JNI_FALSE; }; let mut inner_adapter = inner_adapter.lock().unwrap(); - if inner_adapter.collapse_text_selection(&mut env, &class, &host, virtual_view_id) { + if let Some(events) = inner_adapter.collapse_text_selection(virtual_view_id) { + events.raise(&mut env, &class, &host); JNI_TRUE } else { JNI_FALSE @@ -273,15 +253,13 @@ extern "system" fn traverse_text( return JNI_FALSE; }; let mut inner_adapter = inner_adapter.lock().unwrap(); - if inner_adapter.traverse_text( - &mut env, - &class, - &host, + if let Some(events) = inner_adapter.traverse_text( virtual_view_id, granularity, forward == JNI_TRUE, extend_selection == JNI_TRUE, ) { + events.raise(&mut env, &class, &host); JNI_TRUE } else { JNI_FALSE @@ -337,7 +315,7 @@ fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { }, NativeMethod { name: "performAction".into(), - sig: "(JII)Z".into(), + sig: "(JLandroid/view/View;II)Z".into(), fn_ptr: perform_action as *mut c_void, }, NativeMethod { @@ -436,12 +414,15 @@ impl InjectingAdapter { let Some(host) = self.host.upgrade_local(&env).unwrap() else { return; }; - self.inner.lock().unwrap().adapter.update_if_active( - update_factory, - &mut env, - self.delegate_class, - &host, - ); + if let Some(events) = self + .inner + .lock() + .unwrap() + .adapter + .update_if_active(update_factory) + { + events.raise(&mut env, self.delegate_class, &host); + } } } diff --git a/platforms/android/src/lib.rs b/platforms/android/src/lib.rs index 0e9af77f5..180c97b43 100644 --- a/platforms/android/src/lib.rs +++ b/platforms/android/src/lib.rs @@ -8,7 +8,7 @@ mod node; mod util; mod adapter; -pub use adapter::Adapter; +pub use adapter::{Adapter, QueuedEvents}; mod inject; pub use inject::InjectingAdapter; diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index b20b78fe1..191579c89 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -13,6 +13,7 @@ pub(crate) const ACTION_CLICK: jint = 1 << 4; pub(crate) const ACTION_NEXT_AT_MOVEMENT_GRANULARITY: jint = 1 << 8; pub(crate) const ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: jint = 1 << 9; pub(crate) const ACTION_SET_SELECTION: jint = 1 << 17; +pub(crate) const EVENT_VIEW_CLICKED: jint = 1; pub(crate) const EVENT_VIEW_FOCUSED: jint = 1 << 3; pub(crate) const EVENT_WINDOW_CONTENT_CHANGED: jint = 1 << 11; pub(crate) const HOST_VIEW_ID: jint = -1; From 10f4bd1344b98ebb2baf969e62afdd767f5ff1d1 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 27 Apr 2025 13:43:55 +0200 Subject: [PATCH 21/24] chore: Update the main README regarding Android (#557) --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 07919cc4a..bf3673623 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The interaction between the provider (toolkit or application) and the platform a One notable consequence of this design is that only the platform adapter needs to retain a complete accessibility tree in memory. That means that this design is suitable for immediate-mode GUI toolkits, as long as they can provide a stable ID for each UI element. -The platform adapters are written primarily in Rust. We've chosen Rust for its combination of reliability and efficiency, including safe concurrency, which is especially important in modern software. Some future adapters will need to be partially written in another language, such as Java for the Android adapter. +The platform adapters are written primarily in Rust. We've chosen Rust for its combination of reliability and efficiency, including safe concurrency, which is especially important in modern software. Some future adapters may need to be partially written in another language. The current released platform adapters are all at rough feature parity. They don't yet support all types of UI elements or all of the properties in the schema, but they have enough functionality to make non-trivial applications accessible, including support for both single-line and multi-line text input controls. They don't yet support rich text or hypertext. @@ -43,7 +43,6 @@ The following platform adapters are currently available: #### Planned adapters -* Android * iOS * web (for applications that render their own UI elements to a canvas) From 7ac5911b11f3d6b8b777b91e6476e7073f6b0e4a Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sun, 4 May 2025 06:13:38 -0500 Subject: [PATCH 22/24] refactor!: Simplify the core Android adapter API (#558) --- Cargo.lock | 1 - platforms/android/Cargo.toml | 2 - platforms/android/classes.dex | Bin 10028 -> 3108 bytes .../java/dev/accesskit/android/Delegate.java | 357 +------------- platforms/android/src/action.rs | 69 +++ platforms/android/src/adapter.rs | 454 +++++++++-------- platforms/android/src/event.rs | 323 ++++++++++++ platforms/android/src/inject.rs | 460 +++++++++--------- platforms/android/src/lib.rs | 7 +- platforms/android/src/node.rs | 161 +++--- platforms/android/src/util.rs | 80 ++- platforms/winit/src/platform_impl/android.rs | 3 +- 12 files changed, 1053 insertions(+), 864 deletions(-) create mode 100644 platforms/android/src/action.rs create mode 100644 platforms/android/src/event.rs diff --git a/Cargo.lock b/Cargo.lock index bdd6066e8..d69049a20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,7 +36,6 @@ dependencies = [ "accesskit_consumer", "jni", "log", - "once_cell", ] [[package]] diff --git a/platforms/android/Cargo.toml b/platforms/android/Cargo.toml index 832dcca94..b17d763f6 100644 --- a/platforms/android/Cargo.toml +++ b/platforms/android/Cargo.toml @@ -19,5 +19,3 @@ accesskit = { version = "0.18.0", path = "../../common" } accesskit_consumer = { version = "0.27.0", path = "../../consumer" } jni = "0.21.1" log = "0.4.17" -once_cell = "1.17.1" - diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index f0b648ed071282fbdf8c77750ddec1fee4e2084b..23dbaef5755e7181fcb957ef5de6a0e98b7112ad 100644 GIT binary patch literal 3108 zcmb7GYiJx*6h3!$vYUNx9@^TM>o(P-O|qL!`Y;=`k0dl<`=ALy)Beb0ce0sgGqc&* z-BiR9v|zC+EwqAA5Ts~P3Kp$Dv{Z#ESPF&W55i7A@C7!0k{ZU z0=@*k0=z}M7fM7ZH?0IgSqN!S z6r3fa6&#^S^nfEYdEY*8_FY@t)ei5+_!@_^&02@2!Jh=@SVzI54xa#j%Haj@n8O#q zgNSS!`awkYEMy3oZ%^EgkhBZFYvXMdX^L&%2vAyj9h zBR5Iuryj^KD!}?M=B<}fp%y7s+Th5ir3}(DPM`gR5fS?dBZ77*-P9#zh>}tUs9VaQ zb005lK|it>Cpxl^H9#GBeYRl}yvN1I#e0A+UKhStzaRQf9X;<4-W$AkBF=nJy5Z;;RqW zyRdG&J~y2>4+@12DlX)~(QzL9!?O!L{)nrABFTWP&7-=;MV$-zasIzx62}vRuR_9u z+#As6AJMOl&v`dLZ9c3uuZK(O?n1}U&L!v*RZ?9bxz@HZa`zdgQF@Mo{d!){ zX(he$uvXDX43ZckF-*#Enw0bqsp(;~N;*xhbb6SY(wdnqT1GZ*+3}raGn>~_v}$p% zV(9bnJyyxE%z=t-mQvLAux<z6UzXYRHtdNFO-CEe7E zsU-o(d$GS1B_E!tE%hIN=x%$htUhE;St;7`zvk>OS`{P90jy7FwS2{xiEE~5l{8K@ zK450@mTj21J$h-{%BE=DGDbrt64;g3Y&%73m(h}3Ad`& zyjH80uK!?@Oi}b-{G_Nc&C4FoYi2IKZ}PC7!ICvE89PumO>L4FS&We~HcYND>;g1t z662(f4-KWsJ&r9hI9QeFyT+mQRikf7Bod3fYY9&-;oVDk54gWNcQCKzYzk^wtx(d7 zyHSL^POCFT9lLeeMJ4Z)VP*%dOxdP2Ilc6VQEe5;E493Kq=LR6+ZRbSwOO6~rar$* z%jYMx%nW&~>JFrUL*%}uU_mcVS;g5M8R^7TESuym|4B z64il~b+V2g?certjBliy*;sEZs~?HA$1>Jz!N|+4)iH|=D;1OF98}Yq-I1BrGc$I1 z){gZ~X?a_3kIfophgL9Ry^% literal 10028 zcmb7K4RllId4BJeELpN_%d+Kv%=Hf-euRx}h`}PUF*XPbOl09;I~~Z<#TJMwS(1%O z){Oi#q%@>7rComLO4`t-4cijRy0bI3(=Kh=ou*rtWj#4NJ!k34X}hznquI`u_OO#Z z?{_~5nI>mc@Z9J9-uM09@BM!7y|Uu^lC!I4GrjfIpZv|->l>;MymH{Z_m6JO*xxz- zdL-$4aKjp+G!ZRL^m;(zGq;-PG9=cw1Db`X7knAq3~sg&&46!$OTjKDd|+^u5slOn zjRNC93V0TH4frWg)0J6YI;5_gZ;M>3r zz}rZ)4j2X&fxiUa1=ckYjR7g(Dc}n5L*Umy3-a9o90AS%mx0%Te*yj%s9r&|2G|M& zfB|495CrxDVc;MT1rk6SSO6XX9s(W)P6KCw^S}k*67WaB72r>SKL@@8yaoIi_yzE5 zz}7&~P`Z+6HLwZT4g`UNz+qqk z_!w{wcoujC_&RV6_&M-P;8#G2MzjL(0bRfrU;x+++zT8AGQbk>Fz|8UCEyQ%Zvt-s z{{-9s9IaSSz%XzS$O319F9ELt-vIs=_$lyTfZ9gX2y6uI2F8FXAPFo1XMm@Hi@?`_ zYrtOv{|Hq58MR|0uf*u;Ps1TOD>=az%nH)DZ=t0EEBQbF(riIBn$^> z08%3vnmQn_09!EX4way3MqF%u9R{BvRleKi}WGLyumF1`&5Ii0DoAv3(1tRoyDgHR~{SA2mgf} zfxvpsgY{l1ABJ2ji4mIc$?M=iD#lQ;-8vJ_mWblrKWQTgsOq-y`L#kn`%LW_{}E)K=T?9}I{=oiLq8Ee(sQ0J{NuI6%=7QT>iC>s2fYBU#)@RFDX>Z}iLy#x0ehw1i`o5xD7VlT1)KI- zME?VnYo#BTza+{n^ku=OUmZr`^C;KBeo?U5E|*`F`b(0z-FocM=LMU74e*;pxdHi4 z307$yY(Czz%YX`}`{g(xIFEAl^C7{j=}Ez6{BoRT&!Jo)$H7CI z7!ME14?|y#(~-xgT9#{YN^-eImc7{FT=vRxE!GE@Yh}5PR)}()EZ5_ZW_vwaJS;gX zcr85$Za_`8H(-UB+)SgQypckJ*VA6X7TPCxC5;Jgq5Xna(7l37DJa5FtB z`7yyO>0^=~7u-UpB|jl}1$|ud8NnOq8NnOr0=NlxARd=yj3;vo@Daf(ErM6#jx!DB z^PfM>0G|bX&VP3m=DCBl=Gn|#Y<~y#HfgV6BR<9U_h9dlcCTq(W_Pc~y-nI{KfvyT zJudA?Nqmaq55WGgwBslfAHJ{fhx0!O`wPr`dHZ8F58N37=_b;MZ3vKqHHyYaP!%k(r zN8#4orjJ$$sRH?$eL98u+}~-XgeFxxOR2ZUL{RhD`>LBRfx%+CGfck_c27lf|9<3cS8hN!IGwnny&o6tL9ilw%ZbR9n`0Q+*hu=3*id=!G+kq%9 z)j)!Rwbi)8c^V9D4Sk9tSL$fqy#tmqHFC9{JkFgZeJ{R#UJEKaO9JK}V3 zi>@kH;h{S8_@6ne&@On!Z zuDC7d4Si7P zn#iObxs-^qi=4_<#X>%8b3?PR9TUqEEcVFrX3R1k)${eFTsmRf#UqKRMYgiYIkY!0 zLf^Z{7Ch%;FLI1#M5|Uv>1o0~tMg$sd60Tz8SikF+bS)Jl5fEtSk}T$^v7A&^KvP% zujl5xHca1x%qv3l6_OWo#(VB%SEcY`pOgkE>I>pYR#O zx;|-}kBnhYo3`Jqx4%%2&p2LB1NnLby9@RBoZ%dH<=eCIoz9F|iY&R_e%MC9hMU); zlX#vz@2(f`Up{~Mz9#Bkvkv1Op+#jf+(f?i@$p7@mk4jeZDY324~)`K+G!8(hriu} zI(Cf8DYG64tyUsRHf0hqJ++mb7}gh$2cLmFX3U%u?@ZpCNAi8r_V5l9PEY zjC+9je#bS4j-d|gCl%=7YR>pA|8dM6Pb1GYN1BH}zsIHBd>VRgOu8Fr(%nQ;o<^Au zdzx`Mw{+K5aZlit!g?Ne*7FGQy&^v!JdZ*@Fd@$7{QUA<2z}(B(C6of*97ZN*!U^k zT#HMktKD-A7=Dr*fdS>XBSCZnd|hrEy$r-af>{4#bq-zFP`-bcNKPnOUY&J&LnHlpcr2Rr(wr)y(oml;5P+6dpu;D(H16 z{+s?&0R^T5Cy%~4>c8?tVB_h)(ZDo)Uxj+F|Mcm0|7)k)=qXh+p>ErwPW_$#iRW5E zEg}D`dSVWj^2wHCXIhRyIyiUw=+uL1dd1Y#8M=WRHCnk_R2;n;2yKT7LCIUlz$;Vr zn)!S4ss0?#%T%FOer+_MX8M0T>YpCnzJ+#vea$bk~rtsx8$pdQYTSW~XM=rOBl6fAQW!A8bPcvpd!^~%KE;m5K zWxi)Kw@PjUb3U96$2Rllh-PeyXf1CuGhrL&mA9#QQY$XEB0Bpxpyjrj$>Li(LAF4` zNaXIO#=)UTaBSb9!J(ny@$uk}U?>=wJTx*kbYPr(d0TkbfxW}~B8P?#Muzw8JTyKW z8a8X~7#j->5AOR-&%I+4!=lQr@Zi1!p}{am4epCj>+Lab1>?}{(*EHtdU@WF*vx%8RGLbu(Pq?4jKfra&*qvI`GoeHldFYwq1VmdL zufHhaf^vT}Biip=9yD6qbmy`}WWpnNTwz}-t_O`Hspb6{jB)S#sr$E%(@$G|t2+BL zsl`N`2cc=2swI3y6+wt$a!3kOQ>0EI!W37Y4wBPc zCmox*x~S}yTvS+Q>4sarO0nEir0*?~Hy6oUisZYN$=!vReMS1MkY!3E$>?mBJZ66K zI2uXC7P3?xjYrctJ+m9DJgJi<8jq6$7jtuyErux`%#k;iN+zS}tRBJPIF6H7?0qWZ zV>6mCvZMM*YKmoaO!r{%!IM8Z$Z;fL#LXt_XZ74Nr~JmE=G!e?J-_+MA*{TR$O&;r zDwWiurYuiwDu+I3q!)5#OEyu=Q8hS}iizW5%osLt8e`T3p_# z$UetMd@xS7gu#oSoC#wwnoPtoY*d@bE*llm1s$`I$mT^`Qa5IEb7VC*;Zj3CA&zpg zrDk{*$euE9B2=E%Ge=UH<8l_==~Q|liAD<3VND}zvS;+z$rz^HmeG$RHCmD3-JHb? z8(0BF_b#&?OJ>0!PZq|Z)bVsu&*^c~;9#98Ru#k`?xYzbnxslr@p$6AFlN!Ey4w^5 z&-%Qfu*C9Jv=6fs>WYTBl}ILvQO=^BhJ2-hs^E>5K4cLhx#gDp=%bS`i1k;_Vmz*6 z92T%1xVS^dfQ#)xUKGWOG-Qt>C)0YCTu_f>QpbZvTwfBUU`{`7MyQ2tFJ|C4Zsof& zQ3JD;!6O3?cJ8((1FC?oc;RArb73)mG96BIJhhOC>EwpEe9JmnlQMH)>qG+2CrLe$ zI*`>fWXsJZvK?LI%E>_#+n@@Iv}5@}VP_SlmC9H^*H1(GGRM2qlrXj=5?s8sQL)8JPy-F6 z9(rhJ--B&4*yzZ%ZCe}0zHLKW4C^<69f8Bde;kL5Fw88>!h|jCkIm_^W7&n{*|u#* zqRFhjq3w9W@JG{$wr!g>wDIafMbzop*t4;_%a7-U_0d!|y}9k9q*f_kRhkw2S+o*D zV@P$Y2d#9X#Vp=y(zrt-|Fx>t15(RhHi1%eT1rPOEYTZ%Y8P;<&I% zah}s|mA`@V^;L@FvDJ$6rPYe#ZSYO-b)VuqzZSd}yiU|b{h;!eR-)W&<@cmF+Ht{$ z-<|#e@{a(PuNTSxS|q<)B>$>N#+M0&x^^k&R&iwW-H&-KFbCbK+w|+D{>j_)eyM-v zHhrhmzj<zp>%?uiU2R?@?HPP3ln;AO7BjKRgcnVZIEc0UiT%PyCL|0`3L)w`BaT z&wuOV-;r~Uww)1j`;7GD)ihI|MtuZ jncv67TtMbH{P#@@{8`Vr+o0!n^Y3yn=s8#Z&D;M0RPL|l diff --git a/platforms/android/java/dev/accesskit/android/Delegate.java b/platforms/android/java/dev/accesskit/android/Delegate.java index 79498355b..99b4eb845 100644 --- a/platforms/android/java/dev/accesskit/android/Delegate.java +++ b/platforms/android/java/dev/accesskit/android/Delegate.java @@ -3,381 +3,68 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. -// Derived from the Flutter engine. -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE.chromium file. - package dev.accesskit.android; import android.os.Bundle; import android.view.MotionEvent; import android.view.View; -import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityNodeProvider; public final class Delegate extends View.AccessibilityDelegate implements View.OnHoverListener { private final long adapterHandle; - private int accessibilityFocus = AccessibilityNodeProvider.HOST_VIEW_ID; - private int hoverId = AccessibilityNodeProvider.HOST_VIEW_ID; private Delegate(long adapterHandle) { super(); this.adapterHandle = adapterHandle; } - public static void inject(final View host, final long adapterHandle) { - host.post( - new Runnable() { - @Override - public void run() { - if (host.getAccessibilityDelegate() != null) { - throw new IllegalStateException( - "host already has an accessibility delegate"); - } - Delegate delegate = new Delegate(adapterHandle); - host.setAccessibilityDelegate(delegate); - host.setOnHoverListener(delegate); - } - }); - } + private static native void runCallback(View host, long handle); - public static void remove(final View host) { - host.post( - new Runnable() { - @Override - public void run() { - View.AccessibilityDelegate delegate = host.getAccessibilityDelegate(); - if (delegate != null && delegate instanceof Delegate) { - host.setAccessibilityDelegate(null); - host.setOnHoverListener(null); - } - } - }); - } - - private static AccessibilityEvent newEvent(View host, int virtualViewId, int type) { - AccessibilityEvent e = AccessibilityEvent.obtain(type); - e.setPackageName(host.getContext().getPackageName()); - if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) { - e.setSource(host); - } else { - e.setSource(host, virtualViewId); - } - return e; - } + private static native AccessibilityNodeInfo createAccessibilityNodeInfo( + long adapterHandle, View host, int virtualViewId); - private static void sendCompletedEvent(View host, AccessibilityEvent e) { - host.getParent().requestSendAccessibilityEvent(host, e); - } + private static native AccessibilityNodeInfo findFocus( + long adapterHandle, View host, int focusType); - private static void sendEventInternal(View host, int virtualViewId, int type) { - AccessibilityEvent e = newEvent(host, virtualViewId, type); - if (type == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { - e.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); - } - sendCompletedEvent(host, e); - } + private static native boolean performAction( + long adapterHandle, View host, int virtualViewId, int action, Bundle arguments); - public static void sendEvent(final View host, final int virtualViewId, final int type) { - host.post( - new Runnable() { - @Override - public void run() { - sendEventInternal(host, virtualViewId, type); - } - }); - } + private static native boolean onHoverEvent( + long adapterHandle, View host, int action, float x, float y); - private static void sendTextChangedInternal( - View host, int virtualViewId, String oldValue, String newValue) { - int i; - for (i = 0; i < oldValue.length() && i < newValue.length(); ++i) { - if (oldValue.charAt(i) != newValue.charAt(i)) { - break; - } - } - if (i >= oldValue.length() && i >= newValue.length()) { - return; // Text did not change - } - AccessibilityEvent e = - newEvent(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); - e.setBeforeText(oldValue); - e.getText().add(newValue); - int firstDifference = i; - e.setFromIndex(firstDifference); - int oldIndex = oldValue.length() - 1; - int newIndex = newValue.length() - 1; - while (oldIndex >= firstDifference && newIndex >= firstDifference) { - if (oldValue.charAt(oldIndex) != newValue.charAt(newIndex)) { - break; + public static Runnable newCallback(final View host, final long handle) { + return new Runnable() { + @Override + public void run() { + runCallback(host, handle); } - --oldIndex; - --newIndex; - } - e.setRemovedCount(oldIndex - firstDifference + 1); - e.setAddedCount(newIndex - firstDifference + 1); - sendCompletedEvent(host, e); - } - - public static void sendTextChanged( - final View host, - final int virtualViewId, - final String oldValue, - final String newValue) { - host.post( - new Runnable() { - @Override - public void run() { - sendTextChangedInternal(host, virtualViewId, oldValue, newValue); - } - }); - } - - private static void sendTextSelectionChangedInternal( - View host, int virtualViewId, String text, int start, int end) { - AccessibilityEvent e = - newEvent(host, virtualViewId, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); - e.getText().add(text); - e.setFromIndex(start); - e.setToIndex(end); - e.setItemCount(text.length()); - sendCompletedEvent(host, e); - } - - public static void sendTextSelectionChanged( - final View host, - final int virtualViewId, - final String text, - final int start, - final int end) { - host.post( - new Runnable() { - @Override - public void run() { - sendTextSelectionChangedInternal(host, virtualViewId, text, start, end); - } - }); - } - - private static void sendTextTraversedInternal( - View host, - int virtualViewId, - int granularity, - boolean forward, - int segmentStart, - int segmentEnd) { - AccessibilityEvent e = - newEvent( - host, - virtualViewId, - AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY); - e.setMovementGranularity(granularity); - e.setAction( - forward - ? AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY - : AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); - e.setFromIndex(segmentStart); - e.setToIndex(segmentEnd); - sendCompletedEvent(host, e); - } - - public static void sendTextTraversed( - final View host, - final int virtualViewId, - final int granularity, - final boolean forward, - final int segmentStart, - final int segmentEnd) { - host.post( - new Runnable() { - @Override - public void run() { - sendTextTraversedInternal( - host, - virtualViewId, - granularity, - forward, - segmentStart, - segmentEnd); - } - }); + }; } - private static native boolean populateNodeInfo( - long adapterHandle, - View host, - int screenX, - int screenY, - int virtualViewId, - AccessibilityNodeInfo nodeInfo); - - private static native int getInputFocus(long adapterHandle); - - private static native int getVirtualViewAtPoint(long adapterHandle, float x, float y); - - private static native boolean performAction( - long adapterHandle, View host, int virtualViewId, int action); - - private static native boolean setTextSelection( - long adapterHandle, View host, int virtualViewId, int anchor, int focus); - - private static native boolean collapseTextSelection( - long adapterHandle, View host, int virtualViewId); - - private static native boolean traverseText( - long adapterHandle, - View host, - int virtualViewId, - int granularity, - boolean forward, - boolean extendSelection); - @Override public AccessibilityNodeProvider getAccessibilityNodeProvider(final View host) { return new AccessibilityNodeProvider() { @Override public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { - int[] location = new int[2]; - host.getLocationOnScreen(location); - AccessibilityNodeInfo nodeInfo; - if (virtualViewId == HOST_VIEW_ID) { - nodeInfo = AccessibilityNodeInfo.obtain(host); - } else { - nodeInfo = AccessibilityNodeInfo.obtain(host, virtualViewId); - } - nodeInfo.setPackageName(host.getContext().getPackageName()); - nodeInfo.setVisibleToUser(true); - if (!populateNodeInfo( - adapterHandle, host, location[0], location[1], virtualViewId, nodeInfo)) { - nodeInfo.recycle(); - return null; - } - if (virtualViewId == accessibilityFocus) { - nodeInfo.setAccessibilityFocused(true); - nodeInfo.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS); - } else { - nodeInfo.setAccessibilityFocused(false); - nodeInfo.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); - } - return nodeInfo; + return Delegate.createAccessibilityNodeInfo(adapterHandle, host, virtualViewId); } @Override - public boolean performAction(int virtualViewId, int action, Bundle arguments) { - switch (action) { - case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: - accessibilityFocus = virtualViewId; - host.invalidate(); - sendEventInternal( - host, - virtualViewId, - AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); - return true; - case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: - if (accessibilityFocus == virtualViewId) { - accessibilityFocus = AccessibilityNodeProvider.HOST_VIEW_ID; - } - host.invalidate(); - sendEventInternal( - host, - virtualViewId, - AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); - return true; - case AccessibilityNodeInfo.ACTION_SET_SELECTION: - if (!(arguments != null - && arguments.containsKey( - AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT) - && arguments.containsKey( - AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT))) { - return Delegate.collapseTextSelection( - adapterHandle, host, virtualViewId); - } - int anchor = - arguments.getInt( - AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT); - int focus = - arguments.getInt( - AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT); - return Delegate.setTextSelection( - adapterHandle, host, virtualViewId, anchor, focus); - case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: - case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: - int granularity = - arguments.getInt( - AccessibilityNodeInfo - .ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); - boolean forward = - (action - == AccessibilityNodeInfo - .ACTION_NEXT_AT_MOVEMENT_GRANULARITY); - boolean extendSelection = - arguments.getBoolean( - AccessibilityNodeInfo - .ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN); - return Delegate.traverseText( - adapterHandle, - host, - virtualViewId, - granularity, - forward, - extendSelection); - } - return Delegate.performAction(adapterHandle, host, virtualViewId, action); + public AccessibilityNodeInfo findFocus(int focusType) { + return Delegate.findFocus(adapterHandle, host, focusType); } @Override - public AccessibilityNodeInfo findFocus(int focusType) { - switch (focusType) { - case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: - { - AccessibilityNodeInfo result = - createAccessibilityNodeInfo(accessibilityFocus); - if (result != null && result.isAccessibilityFocused()) { - return result; - } - break; - } - case AccessibilityNodeInfo.FOCUS_INPUT: - { - AccessibilityNodeInfo result = - createAccessibilityNodeInfo(getInputFocus(adapterHandle)); - if (result != null && result.isFocused()) { - return result; - } - break; - } - } - return null; + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return Delegate.performAction( + adapterHandle, host, virtualViewId, action, arguments); } }; } @Override public boolean onHover(View v, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_HOVER_ENTER: - case MotionEvent.ACTION_HOVER_MOVE: - int newId = getVirtualViewAtPoint(adapterHandle, event.getX(), event.getY()); - if (newId != hoverId) { - if (newId != AccessibilityNodeProvider.HOST_VIEW_ID) { - sendEventInternal(v, newId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); - } - if (hoverId != AccessibilityNodeProvider.HOST_VIEW_ID) { - sendEventInternal(v, hoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); - } - hoverId = newId; - } - break; - case MotionEvent.ACTION_HOVER_EXIT: - if (hoverId != AccessibilityNodeProvider.HOST_VIEW_ID) { - sendEventInternal(v, hoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); - hoverId = AccessibilityNodeProvider.HOST_VIEW_ID; - } - break; - } - return true; + return onHoverEvent(adapterHandle, v, event.getAction(), event.getX(), event.getY()); } } diff --git a/platforms/android/src/action.rs b/platforms/android/src/action.rs new file mode 100644 index 000000000..be4ac717e --- /dev/null +++ b/platforms/android/src/action.rs @@ -0,0 +1,69 @@ +// Copyright 2025 The AccessKit Authors. All rights reserved. +// Licensed under the Apache License, Version 2.0 (found in +// the LICENSE-APACHE file) or the MIT license (found in +// the LICENSE-MIT file), at your option. + +use jni::{objects::JObject, sys::jint, JNIEnv}; + +use crate::util::*; + +pub(crate) enum PlatformActionInner { + Simple { + action: jint, + }, + SetTextSelection { + anchor: jint, + focus: jint, + }, + CollapseTextSelection, + TraverseText { + granularity: jint, + forward: bool, + extend_selection: bool, + }, +} + +pub struct PlatformAction(pub(crate) PlatformActionInner); + +impl PlatformAction { + pub fn from_java(env: &mut JNIEnv, action: jint, arguments: &JObject) -> Option { + match action { + ACTION_SET_SELECTION => { + if !(!arguments.is_null() + && bundle_contains_key(env, arguments, ACTION_ARGUMENT_SELECTION_START_INT) + && bundle_contains_key(env, arguments, ACTION_ARGUMENT_SELECTION_END_INT)) + { + return Some(Self(PlatformActionInner::CollapseTextSelection)); + } + let anchor = bundle_get_int(env, arguments, ACTION_ARGUMENT_SELECTION_START_INT); + let focus = bundle_get_int(env, arguments, ACTION_ARGUMENT_SELECTION_END_INT); + Some(Self(PlatformActionInner::SetTextSelection { + anchor, + focus, + })) + } + ACTION_NEXT_AT_MOVEMENT_GRANULARITY | ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY => { + if arguments.is_null() + || !bundle_contains_key( + env, + arguments, + ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, + ) + { + return None; + } + let granularity = + bundle_get_int(env, arguments, ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); + let forward = action == ACTION_NEXT_AT_MOVEMENT_GRANULARITY; + let extend_selection = + bundle_get_bool(env, arguments, ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN); + Some(Self(PlatformActionInner::TraverseText { + granularity, + forward, + extend_selection, + })) + } + _ => Some(Self(PlatformActionInner::Simple { action })), + } + } +} diff --git a/platforms/android/src/adapter.rs b/platforms/android/src/adapter.rs index 0de31b70f..f92d238a1 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -14,154 +14,22 @@ use accesskit::{ }; use accesskit_consumer::{FilterResult, Node, TextPosition, Tree, TreeChangeHandler}; use jni::{ - errors::Result, - objects::{JClass, JObject}, + objects::JObject, sys::{jfloat, jint}, JNIEnv, }; -use crate::{filters::filter, node::NodeWrapper, util::*}; - -enum QueuedEvent { - Simple { - virtual_view_id: jint, - event_type: jint, - }, - TextChanged { - virtual_view_id: jint, - old: Option, - new: Option, - }, - TextSelectionChanged { - virtual_view_id: jint, - text: String, - start: jint, - end: jint, - }, - TextTraversed { - virtual_view_id: jint, - granularity: jint, - forward: bool, - segment_start: jint, - segment_end: jint, - }, -} - -/// Events generated by a tree update or accessibility action. -#[must_use = "events must be explicitly raised"] -pub struct QueuedEvents(Vec); - -impl QueuedEvents { - /// Raise all queued events. - /// - /// The `callback_class` parameter refers to a Java class with methods - /// that actually send the events. The reference implementation of the - /// contract for this class is `dev.accesskit.android.Delegate`, the - /// source code for which is in the `java` directory of this crate. - /// The methods of that class that are called by this function are all - /// marked `public static` and have names starting with `send`. - /// - /// Whether it is safe to call this function on a thread other than the - /// UI thread depends on the implementation of the `send` methods in - /// `callback_class`. If these methods dispatch events asynchronously - /// to the UI thread, as the implementations in - /// `dev.accesskit.android.Delegate` do, then it is safe to call this - /// function on any thread without concern for reentrancy. If they - /// synchronously call the Android framework to send the events, - /// then this function must be called on the UI thread, while not holding - /// any locks required by the host view's implementations of Android - /// framework callbacks. - /// - /// The `host` paramter is the Android view for the adapter that - /// returned this struct. It must be an instance of `android.view.View` - /// or a subclass. - pub fn raise(self, env: &mut JNIEnv, callback_class: &JClass, host: &JObject) { - for event in self.0 { - match event { - QueuedEvent::Simple { - virtual_view_id, - event_type, - } => { - env.call_static_method( - callback_class, - "sendEvent", - "(Landroid/view/View;II)V", - &[host.into(), virtual_view_id.into(), event_type.into()], - ) - .unwrap(); - } - QueuedEvent::TextChanged { - virtual_view_id, - old, - new, - } => { - let old = env.new_string(old.unwrap_or_else(String::new)).unwrap(); - let new = env.new_string(new.unwrap_or_else(String::new)).unwrap(); - env.call_static_method( - callback_class, - "sendTextChanged", - "(Landroid/view/View;ILjava/lang/String;Ljava/lang/String;)V", - &[ - host.into(), - virtual_view_id.into(), - (&old).into(), - (&new).into(), - ], - ) - .unwrap(); - } - QueuedEvent::TextSelectionChanged { - virtual_view_id, - text, - start, - end, - } => { - let text = env.new_string(text).unwrap(); - env.call_static_method( - callback_class, - "sendTextSelectionChanged", - "(Landroid/view/View;ILjava/lang/String;II)V", - &[ - host.into(), - virtual_view_id.into(), - (&text).into(), - start.into(), - end.into(), - ], - ) - .unwrap(); - } - QueuedEvent::TextTraversed { - virtual_view_id, - granularity, - forward, - segment_start, - segment_end, - } => { - env.call_static_method( - callback_class, - "sendTextTraversed", - "(Landroid/view/View;IIZII)V", - &[ - host.into(), - virtual_view_id.into(), - granularity.into(), - forward.into(), - segment_start.into(), - segment_end.into(), - ], - ) - .unwrap(); - } - } - } - } -} +use crate::{ + action::{PlatformAction, PlatformActionInner}, + event::{QueuedEvent, QueuedEvents}, + filters::filter, + node::{add_action, NodeWrapper}, + util::*, +}; fn enqueue_window_content_changed(events: &mut Vec) { - events.push(QueuedEvent::Simple { + events.push(QueuedEvent::WindowContentChanged { virtual_view_id: HOST_VIEW_ID, - event_type: EVENT_WINDOW_CONTENT_CHANGED, }); } @@ -225,8 +93,8 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> { let id = self.node_id_map.get_or_create_java_id(new_node); self.events.push(QueuedEvent::TextChanged { virtual_view_id: id, - old: old_text, - new: new_text.clone(), + old: old_text.unwrap_or_else(String::new), + new: new_text.clone().unwrap_or_else(String::new), }); } if old_node.raw_text_selection() != new_node.raw_text_selection() @@ -327,6 +195,8 @@ fn update_tree( pub struct Adapter { node_id_map: NodeIdMap, state: State, + accessibility_focus: Option, + hover_target: Option, } impl Adapter { @@ -367,84 +237,111 @@ impl Adapter { } } - /// Populate an `AccessibilityNodeInfo` with information about the - /// AccessKit node identified by the given virtual view ID. + /// Create an `AccessibilityNodeInfo` for the AccessKit node + /// corresponding to the given virtual view ID. Returns null if + /// there is no such node. /// - /// The `host` paramter is the Android view for this adapter. + /// The `host` parameter is the Android view for this adapter. /// It must be an instance of `android.view.View` or a subclass. - #[allow(clippy::too_many_arguments)] - pub fn populate_node_info( + pub fn create_accessibility_node_info<'local, H: ActivationHandler + ?Sized>( &mut self, activation_handler: &mut H, - env: &mut JNIEnv, + env: &mut JNIEnv<'local>, host: &JObject, - host_screen_x: jint, - host_screen_y: jint, virtual_view_id: jint, - jni_node: &JObject, - ) -> Result { + ) -> JObject<'local> { let tree = self.state.get_or_init_tree(activation_handler); let tree_state = tree.state(); let node = if virtual_view_id == HOST_VIEW_ID { tree_state.root() } else { let Some(accesskit_id) = self.node_id_map.get_accesskit_id(virtual_view_id) else { - return Ok(false); + return JObject::null(); }; let Some(node) = tree_state.node_by_id(accesskit_id) else { - return Ok(false); + return JObject::null(); }; node }; + let node_info_class = env + .find_class("android/view/accessibility/AccessibilityNodeInfo") + .unwrap(); + let node_info = env + .call_static_method( + &node_info_class, + "obtain", + "(Landroid/view/View;I)Landroid/view/accessibility/AccessibilityNodeInfo;", + &[host.into(), virtual_view_id.into()], + ) + .unwrap() + .l() + .unwrap(); + + let package_name = get_package_name(env, host); + env.call_method( + &node_info, + "setPackageName", + "(Ljava/lang/CharSequence;)V", + &[(&package_name).into()], + ) + .unwrap(); + let wrapper = NodeWrapper(&node); - wrapper.populate_node_info( + wrapper.populate_node_info(env, host, &mut self.node_id_map, &node_info); + + let is_accessibility_focus = self.accessibility_focus == Some(virtual_view_id); + env.call_method( + &node_info, + "setAccessibilityFocused", + "(Z)V", + &[is_accessibility_focus.into()], + ) + .unwrap(); + add_action( env, - host, - host_screen_x, - host_screen_y, - &mut self.node_id_map, - jni_node, - )?; - Ok(true) - } + &node_info, + if is_accessibility_focus { + ACTION_CLEAR_ACCESSIBILITY_FOCUS + } else { + ACTION_ACCESSIBILITY_FOCUS + }, + ); - pub fn input_focus( - &mut self, - activation_handler: &mut H, - ) -> jint { - let tree = self.state.get_or_init_tree(activation_handler); - let tree_state = tree.state(); - let node = tree_state.focus_in_tree(); - self.node_id_map.get_or_create_java_id(&node) + node_info } - pub fn virtual_view_at_point( + /// Create an `AccessibilityNodeInfo` for the AccessKit node + /// with the given focus type. Returns null if there is no such node. + /// + /// The `host` parameter is the Android view for this adapter. + /// It must be an instance of `android.view.View` or a subclass. + pub fn find_focus<'local, H: ActivationHandler + ?Sized>( &mut self, activation_handler: &mut H, - x: jfloat, - y: jfloat, - ) -> jint { - let tree = self.state.get_or_init_tree(activation_handler); - let tree_state = tree.state(); - let root = tree_state.root(); - let point = Point::new(x.into(), y.into()); - let point = root.transform().inverse() * point; - let node = root.node_at_point(point, &filter).unwrap_or(root); - self.node_id_map.get_or_create_java_id(&node) + env: &mut JNIEnv<'local>, + host: &JObject, + focus_type: jint, + ) -> JObject<'local> { + let virtual_view_id = match focus_type { + FOCUS_INPUT => { + let tree = self.state.get_or_init_tree(activation_handler); + let tree_state = tree.state(); + let node = tree_state.focus_in_tree(); + self.node_id_map.get_or_create_java_id(&node) + } + FOCUS_ACCESSIBILITY => { + let Some(id) = self.accessibility_focus else { + return JObject::null(); + }; + id + } + _ => return JObject::null(), + }; + self.create_accessibility_node_info(activation_handler, env, host, virtual_view_id) } - /// Perform the specified accessibility action. - /// - /// If a [`QueuedEvents`] instance is returned, the caller must call - /// [`QueuedEvents::raise`] on it, and the Java `performAction` method - /// must return `true`. Otherwise, the Java `performAction` method - /// must either handle the action some other way or return false. - /// - /// This method may be safely called on any thread, but refer to - /// [`QueuedEvents::raise`] for restrictions on the context in which - /// it should be called. - pub fn perform_action( + fn perform_simple_action( &mut self, action_handler: &mut H, virtual_view_id: jint, @@ -457,6 +354,7 @@ impl Adapter { } else { self.node_id_map.get_accesskit_id(virtual_view_id)? }; + let mut events = Vec::new(); let request = match action { ACTION_CLICK => ActionRequest { action: { @@ -475,12 +373,31 @@ impl Adapter { target, data: None, }, + ACTION_ACCESSIBILITY_FOCUS => { + self.accessibility_focus = Some(virtual_view_id); + events.push(QueuedEvent::InvalidateHost); + events.push(QueuedEvent::Simple { + virtual_view_id, + event_type: EVENT_VIEW_ACCESSIBILITY_FOCUSED, + }); + return Some(QueuedEvents(events)); + } + ACTION_CLEAR_ACCESSIBILITY_FOCUS => { + if self.accessibility_focus == Some(virtual_view_id) { + self.accessibility_focus = None; + } + events.push(QueuedEvent::InvalidateHost); + events.push(QueuedEvent::Simple { + virtual_view_id, + event_type: EVENT_VIEW_ACCESSIBILITY_FOCUS_CLEARED, + }); + return Some(QueuedEvents(events)); + } _ => { return None; } }; action_handler.do_action(request); - let mut events = Vec::new(); if action == ACTION_CLICK { events.push(QueuedEvent::Simple { virtual_view_id, @@ -537,18 +454,7 @@ impl Adapter { Some(extra) } - /// Set the text selection of the specified virtual view to the specified - /// endpoints. - /// - /// If a [`QueuedEvents`] instance is returned, the caller must call - /// [`QueuedEvents::raise`] on it, and the Java `performAction` method - /// must return `true`. Otherwise, the Java `performAction` method - /// must either handle the action some other way or return false. - /// - /// This method may be safely called on any thread, but refer to - /// [`QueuedEvents::raise`] for restrictions on the context in which - /// it should be called. - pub fn set_text_selection( + fn set_text_selection( &mut self, action_handler: &mut H, virtual_view_id: jint, @@ -566,17 +472,7 @@ impl Adapter { Some(QueuedEvents(events)) } - /// Collapse the text selection of the specified virtual view. - /// - /// If a [`QueuedEvents`] instance is returned, the caller must call - /// [`QueuedEvents::raise`] on it, and the Java `performAction` method - /// must return `true`. Otherwise, the Java `performAction` method - /// must either handle the action some other way or return false. - /// - /// This method may be safely called on any thread, but refer to - /// [`QueuedEvents::raise`] for restrictions on the context in which - /// it should be called. - pub fn collapse_text_selection( + fn collapse_text_selection( &mut self, action_handler: &mut H, virtual_view_id: jint, @@ -588,17 +484,7 @@ impl Adapter { Some(QueuedEvents(events)) } - /// Traverse the text in the specified virtual view. - /// - /// If a [`QueuedEvents`] instance is returned, the caller must call - /// [`QueuedEvents::raise`] on it, and the Java `performAction` method - /// must return `true`. Otherwise, the Java `performAction` method - /// must either handle the action some other way or return false. - /// - /// This method may be safely called on any thread, but refer to - /// [`QueuedEvents::raise`] for restrictions on the context in which - /// it should be called. - pub fn traverse_text( + fn traverse_text( &mut self, action_handler: &mut H, virtual_view_id: jint, @@ -750,4 +636,114 @@ impl Adapter { }); Some(QueuedEvents(events)) } + + /// Perform the specified accessibility action. + /// + /// If a [`QueuedEvents`] instance is returned, the caller must call + /// [`QueuedEvents::raise`] on it, and the Java `performAction` method + /// must return `true`. Otherwise, the Java `performAction` method + /// must either handle the action some other way or return `false`. + /// + /// This method may be safely called on any thread, but refer to + /// [`QueuedEvents::raise`] for restrictions on the context in which + /// it should be called. + pub fn perform_action( + &mut self, + action_handler: &mut H, + virtual_view_id: jint, + action: &PlatformAction, + ) -> Option { + match action.0 { + PlatformActionInner::Simple { action } => { + self.perform_simple_action(action_handler, virtual_view_id, action) + } + PlatformActionInner::SetTextSelection { anchor, focus } => { + self.set_text_selection(action_handler, virtual_view_id, anchor, focus) + } + PlatformActionInner::CollapseTextSelection => { + self.collapse_text_selection(action_handler, virtual_view_id) + } + PlatformActionInner::TraverseText { + granularity, + forward, + extend_selection, + } => self.traverse_text( + action_handler, + virtual_view_id, + granularity, + forward, + extend_selection, + ), + } + } + + fn virtual_view_at_point( + &mut self, + activation_handler: &mut H, + x: jfloat, + y: jfloat, + ) -> Option { + let tree = self.state.get_or_init_tree(activation_handler); + let tree_state = tree.state(); + let root = tree_state.root(); + let point = Point::new(x.into(), y.into()); + let point = root.transform().inverse() * point; + let node = root.node_at_point(point, &filter)?; + Some(self.node_id_map.get_or_create_java_id(&node)) + } + + /// Handle the provided hover event. + /// + /// The `action`, `x`, and `y` parameters must be retrieved from + /// the corresponding properties on an Android motion event. These + /// parameters are passed individually so you can use either a Java + /// or NDK event. + /// + /// If a [`QueuedEvents`] instance is returned, the caller must call + /// [`QueuedEvents::raise`] on it, and if using Java, the event handler + /// must return `true`. Otherwise, if using Java, the event handler + /// must either handle the event some other way or return `false`. + /// + /// This method may be safely called on any thread, but refer to + /// [`QueuedEvents::raise`] for restrictions on the context in which + /// it should be called. + pub fn on_hover_event( + &mut self, + activation_handler: &mut H, + action: jint, + x: jfloat, + y: jfloat, + ) -> Option { + let mut events = Vec::new(); + match action { + MOTION_ACTION_HOVER_ENTER | MOTION_ACTION_HOVER_MOVE => { + let new_id = self.virtual_view_at_point(activation_handler, x, y); + if self.hover_target != new_id { + if let Some(virtual_view_id) = new_id { + events.push(QueuedEvent::Simple { + virtual_view_id, + event_type: EVENT_VIEW_HOVER_ENTER, + }); + } + if let Some(virtual_view_id) = self.hover_target { + events.push(QueuedEvent::Simple { + virtual_view_id, + event_type: EVENT_VIEW_HOVER_EXIT, + }); + } + self.hover_target = new_id; + } + } + MOTION_ACTION_HOVER_EXIT => { + if let Some(virtual_view_id) = self.hover_target.take() { + events.push(QueuedEvent::Simple { + virtual_view_id, + event_type: EVENT_VIEW_HOVER_EXIT, + }); + } + } + _ => return None, + } + Some(QueuedEvents(events)) + } } diff --git a/platforms/android/src/event.rs b/platforms/android/src/event.rs new file mode 100644 index 000000000..326202f99 --- /dev/null +++ b/platforms/android/src/event.rs @@ -0,0 +1,323 @@ +// Copyright 2025 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 the Flutter engine. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE.chromium file. + +use jni::{objects::JObject, sys::jint, JNIEnv}; + +use crate::util::*; + +fn new_event<'local>( + env: &mut JNIEnv<'local>, + host: &JObject, + virtual_view_id: jint, + event_type: jint, +) -> JObject<'local> { + let event_class = env + .find_class("android/view/accessibility/AccessibilityEvent") + .unwrap(); + let event = env + .call_static_method( + &event_class, + "obtain", + "(I)Landroid/view/accessibility/AccessibilityEvent;", + &[event_type.into()], + ) + .unwrap() + .l() + .unwrap(); + let package_name = get_package_name(env, host); + env.call_method( + &event, + "setPackageName", + "(Ljava/lang/CharSequence;)V", + &[(&package_name).into()], + ) + .unwrap(); + env.call_method( + &event, + "setSource", + "(Landroid/view/View;I)V", + &[host.into(), virtual_view_id.into()], + ) + .unwrap(); + event +} + +fn send_completed_event(env: &mut JNIEnv, host: &JObject, event: JObject) { + let parent = env + .call_method(host, "getParent", "()Landroid/view/ViewParent;", &[]) + .unwrap() + .l() + .unwrap(); + env.call_method( + &parent, + "requestSendAccessibilityEvent", + "(Landroid/view/View;Landroid/view/accessibility/AccessibilityEvent;)Z", + &[host.into(), (&event).into()], + ) + .unwrap(); +} + +fn send_simple_event(env: &mut JNIEnv, host: &JObject, virtual_view_id: jint, event_type: jint) { + let event = new_event(env, host, virtual_view_id, event_type); + send_completed_event(env, host, event); +} + +fn send_window_content_changed(env: &mut JNIEnv, host: &JObject, virtual_view_id: jint) { + let event = new_event(env, host, virtual_view_id, EVENT_WINDOW_CONTENT_CHANGED); + env.call_method( + &event, + "setContentChangeTypes", + "(I)V", + &[CONTENT_CHANGE_TYPE_SUBTREE.into()], + ) + .unwrap(); + send_completed_event(env, host, event); +} + +fn send_text_changed( + env: &mut JNIEnv, + host: &JObject, + virtual_view_id: jint, + old: String, + new: String, +) { + let old_u16 = old.encode_utf16().collect::>(); + let new_u16 = new.encode_utf16().collect::>(); + let mut i = 0usize; + while i < old_u16.len() && i < new_u16.len() { + if old_u16[i] != new_u16[i] { + break; + } + i += 1; + } + if i == old_u16.len() && i == new_u16.len() { + // The text didn't change. + return; + } + let event = new_event(env, host, virtual_view_id, EVENT_VIEW_TEXT_CHANGED); + let old = env.new_string(old).unwrap(); + env.call_method( + &event, + "setBeforeText", + "(Ljava/lang/CharSequence;)V", + &[(&old).into()], + ) + .unwrap(); + let text_list = env + .call_method(&event, "getText", "()Ljava/util/List;", &[]) + .unwrap() + .l() + .unwrap(); + let new = env.new_string(new).unwrap(); + env.call_method(&text_list, "add", "(Ljava/lang/Object;)Z", &[(&new).into()]) + .unwrap(); + // Note: This algorithm, translated from code in Flutter, assumes + // that the indices are signed. + let first_difference = i as jint; + env.call_method(&event, "setFromIndex", "(I)V", &[first_difference.into()]) + .unwrap(); + let mut old_index = (old_u16.len() - 1) as jint; + let mut new_index = (new_u16.len() - 1) as jint; + while old_index >= first_difference && new_index >= first_difference { + if old_u16[old_index as usize] != new_u16[new_index as usize] { + break; + } + old_index -= 1; + new_index -= 1; + } + env.call_method( + &event, + "setRemovedCount", + "(I)V", + &[(old_index - first_difference + 1).into()], + ) + .unwrap(); + env.call_method( + &event, + "setAddedCount", + "(I)V", + &[(new_index - first_difference + 1).into()], + ) + .unwrap(); + send_completed_event(env, host, event); +} + +fn send_text_selection_changed( + env: &mut JNIEnv, + host: &JObject, + virtual_view_id: jint, + text: String, + start: jint, + end: jint, +) { + let text_u16_len = text.encode_utf16().count(); + let event = new_event( + env, + host, + virtual_view_id, + EVENT_VIEW_TEXT_SELECTION_CHANGED, + ); + let text_list = env + .call_method(&event, "getText", "()Ljava/util/List;", &[]) + .unwrap() + .l() + .unwrap(); + let text = env.new_string(text).unwrap(); + env.call_method( + &text_list, + "add", + "(Ljava/lang/Object;)Z", + &[(&text).into()], + ) + .unwrap(); + env.call_method(&event, "setFromIndex", "(I)V", &[(start as jint).into()]) + .unwrap(); + env.call_method(&event, "setToIndex", "(I)V", &[(end as jint).into()]) + .unwrap(); + env.call_method( + &event, + "setItemCount", + "(I)V", + &[(text_u16_len as jint).into()], + ) + .unwrap(); + send_completed_event(env, host, event); +} + +fn send_text_traversed( + env: &mut JNIEnv, + host: &JObject, + virtual_view_id: jint, + granularity: jint, + forward: bool, + segment_start: jint, + segment_end: jint, +) { + let event = new_event( + env, + host, + virtual_view_id, + EVENT_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, + ); + env.call_method( + &event, + "setMovementGranularity", + "(I)V", + &[granularity.into()], + ) + .unwrap(); + let action = if forward { + ACTION_NEXT_AT_MOVEMENT_GRANULARITY + } else { + ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY + }; + env.call_method(&event, "setAction", "(I)V", &[action.into()]) + .unwrap(); + env.call_method(&event, "setFromIndex", "(I)V", &[segment_start.into()]) + .unwrap(); + env.call_method(&event, "setToIndex", "(I)V", &[segment_end.into()]) + .unwrap(); + send_completed_event(env, host, event); +} + +pub(crate) enum QueuedEvent { + Simple { + virtual_view_id: jint, + event_type: jint, + }, + WindowContentChanged { + virtual_view_id: jint, + }, + TextChanged { + virtual_view_id: jint, + old: String, + new: String, + }, + TextSelectionChanged { + virtual_view_id: jint, + text: String, + start: jint, + end: jint, + }, + TextTraversed { + virtual_view_id: jint, + granularity: jint, + forward: bool, + segment_start: jint, + segment_end: jint, + }, + InvalidateHost, +} + +/// Events generated by a tree update or accessibility action. +#[must_use = "events must be explicitly raised"] +pub struct QueuedEvents(pub(crate) Vec); + +impl QueuedEvents { + /// Raise all queued events. + /// + /// The `host` parameter is the Android view for the adapter that + /// returned this struct. It must be an instance of `android.view.View` + /// or a subclass. + /// + /// This function must be called on the Android UI thread, while not holding + /// any locks required by the host view's implementations of Android + /// framework callbacks. + pub fn raise(self, env: &mut JNIEnv, host: &JObject) { + for event in self.0 { + match event { + QueuedEvent::Simple { + virtual_view_id, + event_type, + } => { + send_simple_event(env, host, virtual_view_id, event_type); + } + QueuedEvent::WindowContentChanged { virtual_view_id } => { + send_window_content_changed(env, host, virtual_view_id); + } + QueuedEvent::TextChanged { + virtual_view_id, + old, + new, + } => { + send_text_changed(env, host, virtual_view_id, old, new); + } + QueuedEvent::TextSelectionChanged { + virtual_view_id, + text, + start, + end, + } => { + send_text_selection_changed(env, host, virtual_view_id, text, start, end); + } + QueuedEvent::TextTraversed { + virtual_view_id, + granularity, + forward, + segment_start, + segment_end, + } => { + send_text_traversed( + env, + host, + virtual_view_id, + granularity, + forward, + segment_start, + segment_end, + ); + } + QueuedEvent::InvalidateHost => { + env.call_method(host, "invalidate", "()V", &[]).unwrap(); + } + } + } + } +} diff --git a/platforms/android/src/inject.rs b/platforms/android/src/inject.rs index ba4c52d89..6d5c32a8f 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -17,21 +17,17 @@ use jni::{ JNIEnv, JavaVM, NativeMethod, }; use log::debug; -use once_cell::sync::OnceCell; use std::{ collections::BTreeMap, ffi::c_void, fmt::{Debug, Formatter}, sync::{ atomic::{AtomicI64, Ordering}, - Arc, Mutex, Weak, + Arc, Mutex, OnceLock, Weak, }, }; -use crate::{ - adapter::{Adapter, QueuedEvents}, - util::*, -}; +use crate::{action::PlatformAction, adapter::Adapter, event::QueuedEvents}; struct InnerInjectingAdapter { adapter: Adapter, @@ -50,70 +46,42 @@ impl Debug for InnerInjectingAdapter { } impl InnerInjectingAdapter { - fn populate_node_info( + fn create_accessibility_node_info<'local>( &mut self, - env: &mut JNIEnv, + env: &mut JNIEnv<'local>, host: &JObject, - host_screen_x: jint, - host_screen_y: jint, virtual_view_id: jint, - jni_node: &JObject, - ) -> Result { - self.adapter.populate_node_info( + ) -> JObject<'local> { + self.adapter.create_accessibility_node_info( &mut *self.activation_handler, env, host, - host_screen_x, - host_screen_y, virtual_view_id, - jni_node, ) } - fn input_focus(&mut self) -> jint { - self.adapter.input_focus(&mut *self.activation_handler) - } - - fn virtual_view_at_point(&mut self, x: jfloat, y: jfloat) -> jint { - self.adapter - .virtual_view_at_point(&mut *self.activation_handler, x, y) - } - - fn perform_action(&mut self, virtual_view_id: jint, action: jint) -> Option { + fn find_focus<'local>( + &mut self, + env: &mut JNIEnv<'local>, + host: &JObject, + focus_type: jint, + ) -> JObject<'local> { self.adapter - .perform_action(&mut *self.action_handler, virtual_view_id, action) + .find_focus(&mut *self.activation_handler, env, host, focus_type) } - fn set_text_selection( + fn perform_action( &mut self, virtual_view_id: jint, - anchor: jint, - focus: jint, + action: &PlatformAction, ) -> Option { self.adapter - .set_text_selection(&mut *self.action_handler, virtual_view_id, anchor, focus) + .perform_action(&mut *self.action_handler, virtual_view_id, action) } - fn collapse_text_selection(&mut self, virtual_view_id: jint) -> Option { + fn on_hover_event(&mut self, action: jint, x: jfloat, y: jfloat) -> Option { self.adapter - .collapse_text_selection(&mut *self.action_handler, virtual_view_id) - } - - #[allow(clippy::too_many_arguments)] - fn traverse_text( - &mut self, - virtual_view_id: jint, - granularity: jint, - forward: bool, - extend_selection: bool, - ) -> Option { - self.adapter.traverse_text( - &mut *self.action_handler, - virtual_view_id, - granularity, - forward, - extend_selection, - ) + .on_hover_event(&mut *self.activation_handler, action, x, y) } } @@ -126,218 +94,200 @@ fn inner_adapter_from_handle(handle: jlong) -> Option jboolean { - let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { - return JNI_FALSE; - }; - let mut inner_adapter = inner_adapter.lock().unwrap(); - if inner_adapter - .populate_node_info( - &mut env, - &host, - host_screen_x, - host_screen_y, - virtual_view_id, - &node_info, +static NEXT_CALLBACK_HANDLE: AtomicI64 = AtomicI64::new(0); +#[allow(clippy::type_complexity)] +static CALLBACK_MAP: Mutex< + BTreeMap>, +> = Mutex::new(BTreeMap::new()); + +fn post_to_ui_thread( + env: &mut JNIEnv, + delegate_class: &JClass, + host: &JObject, + callback: impl FnOnce(&mut JNIEnv, &JClass, &JObject) + Send + 'static, +) { + let handle = NEXT_CALLBACK_HANDLE.fetch_add(1, Ordering::Relaxed); + CALLBACK_MAP + .lock() + .unwrap() + .insert(handle, Box::new(callback)); + let runnable = env + .call_static_method( + delegate_class, + "newCallback", + "(Landroid/view/View;J)Ljava/lang/Runnable;", + &[host.into(), handle.into()], ) .unwrap() - { - JNI_TRUE - } else { - JNI_FALSE - } + .l() + .unwrap(); + env.call_method( + host, + "post", + "(Ljava/lang/Runnable;)Z", + &[(&runnable).into()], + ) + .unwrap(); } -extern "system" fn get_input_focus(_env: JNIEnv, _class: JClass, adapter_handle: jlong) -> jint { - let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { - return HOST_VIEW_ID; +extern "system" fn run_callback<'local>( + mut env: JNIEnv<'local>, + class: JClass<'local>, + host: JObject<'local>, + handle: jlong, +) { + let Some(callback) = CALLBACK_MAP.lock().unwrap().remove(&handle) else { + return; }; - let mut inner_adapter = inner_adapter.lock().unwrap(); - inner_adapter.input_focus() + callback(&mut env, &class, &host); } -extern "system" fn get_virtual_view_at_point( - _env: JNIEnv, - _class: JClass, +extern "system" fn create_accessibility_node_info<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, adapter_handle: jlong, - x: jfloat, - y: jfloat, -) -> jint { + host: JObject<'local>, + virtual_view_id: jint, +) -> JObject<'local> { let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { - return HOST_VIEW_ID; + return JObject::null(); }; let mut inner_adapter = inner_adapter.lock().unwrap(); - inner_adapter.virtual_view_at_point(x, y) + inner_adapter.create_accessibility_node_info(&mut env, &host, virtual_view_id) } -extern "system" fn perform_action( - mut env: JNIEnv, - class: JClass, +extern "system" fn find_focus<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, adapter_handle: jlong, - host: JObject, - virtual_view_id: jint, - action: jint, -) -> jboolean { + host: JObject<'local>, + focus_type: jint, +) -> JObject<'local> { let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { - return JNI_FALSE; + return JObject::null(); }; let mut inner_adapter = inner_adapter.lock().unwrap(); - if let Some(events) = inner_adapter.perform_action(virtual_view_id, action) { - events.raise(&mut env, &class, &host); - JNI_TRUE - } else { - JNI_FALSE - } + inner_adapter.find_focus(&mut env, &host, focus_type) } -extern "system" fn set_text_selection( - mut env: JNIEnv, - class: JClass, +extern "system" fn perform_action<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, adapter_handle: jlong, - host: JObject, + host: JObject<'local>, virtual_view_id: jint, - anchor: jint, - focus: jint, + action: jint, + arguments: JObject<'local>, ) -> jboolean { - let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { + let Some(action) = PlatformAction::from_java(&mut env, action, &arguments) else { return JNI_FALSE; }; - let mut inner_adapter = inner_adapter.lock().unwrap(); - if let Some(events) = inner_adapter.set_text_selection(virtual_view_id, anchor, focus) { - events.raise(&mut env, &class, &host); - JNI_TRUE - } else { - JNI_FALSE - } -} - -extern "system" fn collapse_text_selection( - mut env: JNIEnv, - class: JClass, - adapter_handle: jlong, - host: JObject, - virtual_view_id: jint, -) -> jboolean { let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { return JNI_FALSE; }; let mut inner_adapter = inner_adapter.lock().unwrap(); - if let Some(events) = inner_adapter.collapse_text_selection(virtual_view_id) { - events.raise(&mut env, &class, &host); - JNI_TRUE - } else { - JNI_FALSE - } + let Some(events) = inner_adapter.perform_action(virtual_view_id, &action) else { + return JNI_FALSE; + }; + drop(inner_adapter); + events.raise(&mut env, &host); + JNI_TRUE } -extern "system" fn traverse_text( - mut env: JNIEnv, - class: JClass, +extern "system" fn on_hover_event<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, adapter_handle: jlong, - host: JObject, - virtual_view_id: jint, - granularity: jint, - forward: jboolean, - extend_selection: jboolean, + host: JObject<'local>, + action: jint, + x: jfloat, + y: jfloat, ) -> jboolean { let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else { return JNI_FALSE; }; let mut inner_adapter = inner_adapter.lock().unwrap(); - if let Some(events) = inner_adapter.traverse_text( - virtual_view_id, - granularity, - forward == JNI_TRUE, - extend_selection == JNI_TRUE, - ) { - events.raise(&mut env, &class, &host); - JNI_TRUE - } else { - JNI_FALSE - } + let Some(events) = inner_adapter.on_hover_event(action, x, y) else { + return JNI_FALSE; + }; + drop(inner_adapter); + events.raise(&mut env, &host); + JNI_TRUE } -fn delegate_class(env: &mut JNIEnv) -> Result<&'static JClass<'static>> { - static CLASS: OnceCell = OnceCell::new(); - let global = CLASS.get_or_try_init(|| { +fn delegate_class(env: &mut JNIEnv) -> &'static JClass<'static> { + static CLASS: OnceLock = OnceLock::new(); + let global = CLASS.get_or_init(|| { #[cfg(feature = "embedded-dex")] let class = { - let dex_class_loader_class = env.find_class("dalvik/system/InMemoryDexClassLoader")?; + let dex_class_loader_class = env + .find_class("dalvik/system/InMemoryDexClassLoader") + .unwrap(); let dex_bytes = include_bytes!("../classes.dex"); let dex_buffer = unsafe { env.new_direct_byte_buffer(dex_bytes.as_ptr() as *mut u8, dex_bytes.len()) - }?; - let dex_class_loader = env.new_object( - &dex_class_loader_class, - "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V", - &[(&dex_buffer).into(), (&JObject::null()).into()], - )?; - let class_name = env.new_string("dev.accesskit.android.Delegate")?; + } + .unwrap(); + let dex_class_loader = env + .new_object( + &dex_class_loader_class, + "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V", + &[(&dex_buffer).into(), (&JObject::null()).into()], + ) + .unwrap(); + let class_name = env.new_string("dev.accesskit.android.Delegate").unwrap(); let class_obj = env .call_method( &dex_class_loader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", &[(&class_name).into()], - )? - .l()?; + ) + .unwrap() + .l() + .unwrap(); JClass::from(class_obj) }; #[cfg(not(feature = "embedded-dex"))] - let class = env.find_class("dev/accesskit/android/Delegate")?; + let class = env.find_class("dev/accesskit/android/Delegate").unwrap(); env.register_native_methods( &class, &[ NativeMethod { - name: "populateNodeInfo".into(), - sig: "(JLandroid/view/View;IIILandroid/view/accessibility/AccessibilityNodeInfo;)Z" - .into(), - fn_ptr: populate_node_info as *mut c_void, + name: "runCallback".into(), + sig: "(Landroid/view/View;J)V".into(), + fn_ptr: run_callback as *mut c_void, }, NativeMethod { - name: "getInputFocus".into(), - sig: "(J)I".into(), - fn_ptr: get_input_focus as *mut c_void, + name: "createAccessibilityNodeInfo".into(), + sig: + "(JLandroid/view/View;I)Landroid/view/accessibility/AccessibilityNodeInfo;" + .into(), + fn_ptr: create_accessibility_node_info as *mut c_void, }, NativeMethod { - name: "getVirtualViewAtPoint".into(), - sig: "(JFF)I".into(), - fn_ptr: get_virtual_view_at_point as *mut c_void, + name: "findFocus".into(), + sig: + "(JLandroid/view/View;I)Landroid/view/accessibility/AccessibilityNodeInfo;" + .into(), + fn_ptr: find_focus as *mut c_void, }, NativeMethod { name: "performAction".into(), - sig: "(JLandroid/view/View;II)Z".into(), + sig: "(JLandroid/view/View;IILandroid/os/Bundle;)Z".into(), fn_ptr: perform_action as *mut c_void, }, NativeMethod { - name: "setTextSelection".into(), - sig: "(JLandroid/view/View;III)Z".into(), - fn_ptr: set_text_selection as *mut c_void, - }, - NativeMethod { - name: "collapseTextSelection".into(), - sig: "(JLandroid/view/View;I)Z".into(), - fn_ptr: collapse_text_selection as *mut c_void, - }, - NativeMethod { - name: "traverseText".into(), - sig: "(JLandroid/view/View;IIZZ)Z".into(), - fn_ptr: traverse_text as *mut c_void, + name: "onHoverEvent".into(), + sig: "(JLandroid/view/View;IFF)Z".into(), + fn_ptr: on_hover_event as *mut c_void, }, ], - )?; - env.new_global_ref(class) - })?; - Ok(global.as_obj().into()) + ) + .unwrap(); + env.new_global_ref(class).unwrap() + }); + global.as_obj().into() } /// High-level AccessKit Android adapter that injects itself into an Android @@ -374,10 +324,10 @@ impl Debug for InjectingAdapter { impl InjectingAdapter { pub fn new( env: &mut JNIEnv, - host_view: &JObject, + host: &JObject, activation_handler: impl 'static + ActivationHandler + Send, action_handler: impl 'static + ActionHandler + Send, - ) -> Result { + ) -> Self { let inner = Arc::new(Mutex::new(InnerInjectingAdapter { adapter: Adapter::default(), activation_handler: Box::new(activation_handler), @@ -388,20 +338,51 @@ impl InjectingAdapter { .lock() .unwrap() .insert(handle, Arc::downgrade(&inner)); - let delegate_class = delegate_class(env)?; - env.call_static_method( + let delegate_class = delegate_class(env); + post_to_ui_thread( + env, delegate_class, - "inject", - "(Landroid/view/View;J)V", - &[host_view.into(), handle.into()], - )?; - Ok(Self { - vm: env.get_java_vm()?, + host, + move |env, delegate_class, host| { + let prev_delegate = env + .call_method( + host, + "getAccessibilityDelegate", + "()Landroid/view/View$AccessibilityDelegate;", + &[], + ) + .unwrap() + .l() + .unwrap(); + if !prev_delegate.is_null() { + panic!("host already has an accessibility delegate"); + } + let delegate = env + .new_object(delegate_class, "(J)V", &[handle.into()]) + .unwrap(); + env.call_method( + host, + "setAccessibilityDelegate", + "(Landroid/view/View$AccessibilityDelegate;)V", + &[(&delegate).into()], + ) + .unwrap(); + env.call_method( + host, + "setOnHoverListener", + "(Landroid/view/View$OnHoverListener;)V", + &[(&delegate).into()], + ) + .unwrap(); + }, + ); + Self { + vm: env.get_java_vm().unwrap(), delegate_class, - host: env.new_weak_ref(host_view)?.unwrap(), + host: env.new_weak_ref(host).unwrap().unwrap(), handle, inner, - }) + } } /// If and only if the tree has been initialized, call the provided function @@ -414,40 +395,69 @@ impl InjectingAdapter { let Some(host) = self.host.upgrade_local(&env).unwrap() else { return; }; - if let Some(events) = self - .inner - .lock() - .unwrap() - .adapter - .update_if_active(update_factory) - { - events.raise(&mut env, self.delegate_class, &host); - } + let mut inner = self.inner.lock().unwrap(); + let Some(events) = inner.adapter.update_if_active(update_factory) else { + return; + }; + drop(inner); + post_to_ui_thread( + &mut env, + self.delegate_class, + &host, + |env, _delegate_class, host| { + events.raise(env, host); + }, + ); } } impl Drop for InjectingAdapter { fn drop(&mut self) { - fn drop_impl(env: &mut JNIEnv, host: &WeakRef) -> Result<()> { + fn drop_impl(env: &mut JNIEnv, delegate_class: &JClass, host: &WeakRef) -> Result<()> { let Some(host) = host.upgrade_local(env)? else { return Ok(()); }; - let delegate_class = delegate_class(env)?; - env.call_static_method( - delegate_class, - "remove", - "(Landroid/view/View;)V", - &[(&host).into()], - )?; + post_to_ui_thread(env, delegate_class, &host, |env, delegate_class, host| { + let prev_delegate = env + .call_method( + host, + "getAccessibilityDelegate", + "()Landroid/view/View$AccessibilityDelegate;", + &[], + ) + .unwrap() + .l() + .unwrap(); + if prev_delegate.is_null() + && !env.is_instance_of(&prev_delegate, delegate_class).unwrap() + { + return; + } + let null = JObject::null(); + env.call_method( + host, + "setAccessibilityDelegate", + "(Landroid/view/View$AccessibilityDelegate;)V", + &[(&null).into()], + ) + .unwrap(); + env.call_method( + host, + "setOnHoverListener", + "(Landroid/view/View$OnHoverListener;)V", + &[(&null).into()], + ) + .unwrap(); + }); Ok(()) } let res = match self.vm.get_env() { - Ok(mut env) => drop_impl(&mut env, &self.host), + Ok(mut env) => drop_impl(&mut env, self.delegate_class, &self.host), Err(_) => self .vm .attach_current_thread() - .and_then(|mut env| drop_impl(&mut env, &self.host)), + .and_then(|mut env| drop_impl(&mut env, self.delegate_class, &self.host)), }; if let Err(err) = res { diff --git a/platforms/android/src/lib.rs b/platforms/android/src/lib.rs index 180c97b43..e1d201b63 100644 --- a/platforms/android/src/lib.rs +++ b/platforms/android/src/lib.rs @@ -7,9 +7,12 @@ mod filters; mod node; mod util; +mod action; +pub use action::PlatformAction; mod adapter; -pub use adapter::{Adapter, QueuedEvents}; - +pub use adapter::Adapter; +mod event; +pub use event::QueuedEvents; mod inject; pub use inject::InjectingAdapter; diff --git a/platforms/android/src/node.rs b/platforms/android/src/node.rs index d480be448..c1fbc1e12 100644 --- a/platforms/android/src/node.rs +++ b/platforms/android/src/node.rs @@ -10,10 +10,20 @@ use accesskit::{Live, Role, Toggled}; use accesskit_consumer::Node; -use jni::{errors::Result, objects::JObject, sys::jint, JNIEnv}; +use jni::{objects::JObject, sys::jint, JNIEnv}; use crate::{filters::filter, util::*}; +pub(crate) fn add_action(env: &mut JNIEnv, node_info: &JObject, action: jint) { + // Note: We're using the deprecated addAction signature. + // But this one is much easier to call from JNI since it uses + // a simple integer constant. Revisit if Android ever gets strict + // about prohibiting deprecated methods for applications targeting + // newer SDKs. + env.call_method(node_info, "addAction", "(I)V", &[action.into()]) + .unwrap(); +} + pub(crate) struct NodeWrapper<'a>(pub(crate) &'a Node<'a>); impl NodeWrapper<'_> { @@ -136,146 +146,163 @@ impl NodeWrapper<'_> { &self, env: &mut JNIEnv, host: &JObject, - host_screen_x: jint, - host_screen_y: jint, id_map: &mut NodeIdMap, - jni_node: &JObject, - ) -> Result<()> { + node_info: &JObject, + ) { for child in self.0.filtered_children(&filter) { env.call_method( - jni_node, + node_info, "addChild", "(Landroid/view/View;I)V", &[host.into(), id_map.get_or_create_java_id(&child).into()], - )?; + ) + .unwrap(); } if let Some(parent) = self.0.filtered_parent(&filter) { if parent.is_root() { env.call_method( - jni_node, + node_info, "setParent", "(Landroid/view/View;)V", &[host.into()], - )?; + ) + .unwrap(); } else { env.call_method( - jni_node, + node_info, "setParent", "(Landroid/view/View;I)V", &[host.into(), id_map.get_or_create_java_id(&parent).into()], - )?; + ) + .unwrap(); } } if let Some(rect) = self.0.bounding_box() { - let android_rect_class = env.find_class("android/graphics/Rect")?; - let android_rect = env.new_object( - &android_rect_class, - "(IIII)V", - &[ - ((rect.x0 as jint) + host_screen_x).into(), - ((rect.y0 as jint) + host_screen_y).into(), - ((rect.x1 as jint) + host_screen_x).into(), - ((rect.y1 as jint) + host_screen_y).into(), - ], - )?; + let location = env.new_int_array(2).unwrap(); + env.call_method(host, "getLocationOnScreen", "([I)V", &[(&location).into()]) + .unwrap(); + let mut location_buf = [0; 2]; + env.get_int_array_region(&location, 0, &mut location_buf) + .unwrap(); + let host_screen_x = location_buf[0]; + let host_screen_y = location_buf[1]; + let android_rect_class = env.find_class("android/graphics/Rect").unwrap(); + let android_rect = env + .new_object( + &android_rect_class, + "(IIII)V", + &[ + ((rect.x0 as jint) + host_screen_x).into(), + ((rect.y0 as jint) + host_screen_y).into(), + ((rect.x1 as jint) + host_screen_x).into(), + ((rect.y1 as jint) + host_screen_y).into(), + ], + ) + .unwrap(); env.call_method( - jni_node, + node_info, "setBoundsInScreen", "(Landroid/graphics/Rect;)V", &[(&android_rect).into()], - )?; + ) + .unwrap(); } if self.is_checkable() { - env.call_method(jni_node, "setCheckable", "(Z)V", &[true.into()])?; - env.call_method(jni_node, "setChecked", "(Z)V", &[self.is_checked().into()])?; + env.call_method(node_info, "setCheckable", "(Z)V", &[true.into()]) + .unwrap(); + env.call_method(node_info, "setChecked", "(Z)V", &[self.is_checked().into()]) + .unwrap(); } env.call_method( - jni_node, + node_info, "setEditable", "(Z)V", &[self.is_editable().into()], - )?; - env.call_method(jni_node, "setEnabled", "(Z)V", &[self.is_enabled().into()])?; + ) + .unwrap(); + env.call_method(node_info, "setEnabled", "(Z)V", &[self.is_enabled().into()]) + .unwrap(); env.call_method( - jni_node, + node_info, "setFocusable", "(Z)V", &[self.is_focusable().into()], - )?; - env.call_method(jni_node, "setFocused", "(Z)V", &[self.is_focused().into()])?; + ) + .unwrap(); + env.call_method(node_info, "setFocused", "(Z)V", &[self.is_focused().into()]) + .unwrap(); env.call_method( - jni_node, + node_info, "setPassword", "(Z)V", &[self.is_password().into()], - )?; + ) + .unwrap(); env.call_method( - jni_node, + node_info, "setSelected", "(Z)V", &[self.is_selected().into()], - )?; + ) + .unwrap(); + // TBD: When, if ever, should the visible-to-user property be false? + env.call_method(node_info, "setVisibleToUser", "(Z)V", &[true.into()]) + .unwrap(); if let Some(desc) = self.content_description() { - let desc = env.new_string(desc)?; + let desc = env.new_string(desc).unwrap(); env.call_method( - jni_node, + node_info, "setContentDescription", "(Ljava/lang/CharSequence;)V", &[(&desc).into()], - )?; + ) + .unwrap(); } if let Some(text) = self.text() { - let text = env.new_string(text)?; + let text = env.new_string(text).unwrap(); env.call_method( - jni_node, + node_info, "setText", "(Ljava/lang/CharSequence;)V", &[(&text).into()], - )?; + ) + .unwrap(); } if let Some((start, end)) = self.text_selection() { env.call_method( - jni_node, + node_info, "setTextSelection", "(II)V", &[(start as jint).into(), (end as jint).into()], - )?; + ) + .unwrap(); } - let class_name = env.new_string(self.class_name())?; + let class_name = env.new_string(self.class_name()).unwrap(); env.call_method( - jni_node, + node_info, "setClassName", "(Ljava/lang/CharSequence;)V", &[(&class_name).into()], - )?; - - fn add_action(env: &mut JNIEnv, jni_node: &JObject, action: jint) -> Result<()> { - // Note: We're using the deprecated addAction signature. - // But this one is much easier to call from JNI since it uses - // a simple integer constant. Revisit if Android ever gets strict - // about prohibiting deprecated methods for applications targeting - // newer SDKs. - env.call_method(jni_node, "addAction", "(I)V", &[action.into()])?; - Ok(()) - } + ) + .unwrap(); let can_focus = self.0.is_focusable() && !self.0.is_focused(); if self.0.is_clickable() || can_focus { - add_action(env, jni_node, ACTION_CLICK)?; + add_action(env, node_info, ACTION_CLICK); } if can_focus { - add_action(env, jni_node, ACTION_FOCUS)?; + add_action(env, node_info, ACTION_FOCUS); } if self.0.supports_text_ranges() { - add_action(env, jni_node, ACTION_SET_SELECTION)?; - add_action(env, jni_node, ACTION_NEXT_AT_MOVEMENT_GRANULARITY)?; - add_action(env, jni_node, ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY)?; + add_action(env, node_info, ACTION_SET_SELECTION); + add_action(env, node_info, ACTION_NEXT_AT_MOVEMENT_GRANULARITY); + add_action(env, node_info, ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); env.call_method( - jni_node, + node_info, "setMovementGranularities", "(I)V", &[(MOVEMENT_GRANULARITY_CHARACTER @@ -283,7 +310,8 @@ impl NodeWrapper<'_> { | MOVEMENT_GRANULARITY_LINE | MOVEMENT_GRANULARITY_PARAGRAPH) .into()], - )?; + ) + .unwrap(); } let live = match self.0.live() { @@ -291,8 +319,7 @@ impl NodeWrapper<'_> { Live::Polite => LIVE_REGION_POLITE, Live::Assertive => LIVE_REGION_ASSERTIVE, }; - env.call_method(jni_node, "setLiveRegion", "(I)V", &[live.into()])?; - - Ok(()) + env.call_method(node_info, "setLiveRegion", "(I)V", &[live.into()]) + .unwrap(); } } diff --git a/platforms/android/src/util.rs b/platforms/android/src/util.rs index 191579c89..fa0845fed 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -5,21 +5,50 @@ use accesskit::NodeId; use accesskit_consumer::Node; -use jni::sys::jint; +use jni::{objects::JObject, sys::jint, JNIEnv}; use std::collections::HashMap; pub(crate) const ACTION_FOCUS: jint = 1 << 0; pub(crate) const ACTION_CLICK: jint = 1 << 4; +pub(crate) const ACTION_ACCESSIBILITY_FOCUS: jint = 1 << 6; +pub(crate) const ACTION_CLEAR_ACCESSIBILITY_FOCUS: jint = 1 << 7; pub(crate) const ACTION_NEXT_AT_MOVEMENT_GRANULARITY: jint = 1 << 8; pub(crate) const ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: jint = 1 << 9; pub(crate) const ACTION_SET_SELECTION: jint = 1 << 17; + +pub(crate) const ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT: &str = + "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT"; +pub(crate) const ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN: &str = + "ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN"; +pub(crate) const ACTION_ARGUMENT_SELECTION_START_INT: &str = "ACTION_ARGUMENT_SELECTION_START_INT"; +pub(crate) const ACTION_ARGUMENT_SELECTION_END_INT: &str = "ACTION_ARGUMENT_SELECTION_END_INT"; + +pub(crate) const CONTENT_CHANGE_TYPE_SUBTREE: jint = 1 << 0; + pub(crate) const EVENT_VIEW_CLICKED: jint = 1; pub(crate) const EVENT_VIEW_FOCUSED: jint = 1 << 3; +pub(crate) const EVENT_VIEW_TEXT_CHANGED: jint = 1 << 4; +pub(crate) const EVENT_VIEW_HOVER_ENTER: jint = 1 << 7; +pub(crate) const EVENT_VIEW_HOVER_EXIT: jint = 1 << 8; +pub(crate) const EVENT_VIEW_TEXT_SELECTION_CHANGED: jint = 1 << 13; +pub(crate) const EVENT_VIEW_ACCESSIBILITY_FOCUSED: jint = 1 << 15; +pub(crate) const EVENT_VIEW_ACCESSIBILITY_FOCUS_CLEARED: jint = 1 << 16; +pub(crate) const EVENT_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: jint = 1 << 17; pub(crate) const EVENT_WINDOW_CONTENT_CHANGED: jint = 1 << 11; + +pub(crate) const FOCUS_INPUT: jint = 1; +pub(crate) const FOCUS_ACCESSIBILITY: jint = 2; + pub(crate) const HOST_VIEW_ID: jint = -1; + pub(crate) const LIVE_REGION_NONE: jint = 0; pub(crate) const LIVE_REGION_POLITE: jint = 1; pub(crate) const LIVE_REGION_ASSERTIVE: jint = 2; + +pub(crate) const MOTION_ACTION_HOVER_MOVE: jint = 7; +pub(crate) const MOTION_ACTION_HOVER_ENTER: jint = 9; +pub(crate) const MOTION_ACTION_HOVER_EXIT: jint = 10; + pub(crate) const MOVEMENT_GRANULARITY_CHARACTER: jint = 1 << 0; pub(crate) const MOVEMENT_GRANULARITY_WORD: jint = 1 << 1; pub(crate) const MOVEMENT_GRANULARITY_LINE: jint = 1 << 2; @@ -52,3 +81,52 @@ impl NodeIdMap { java_id } } + +pub(crate) fn bundle_contains_key(env: &mut JNIEnv, bundle: &JObject, key: &str) -> bool { + let key = env.new_string(key).unwrap(); + env.call_method( + bundle, + "containsKey", + "(Ljava/lang/String;)Z", + &[(&key).into()], + ) + .unwrap() + .z() + .unwrap() +} + +pub(crate) fn bundle_get_int(env: &mut JNIEnv, bundle: &JObject, key: &str) -> jint { + let key = env.new_string(key).unwrap(); + env.call_method(bundle, "getInt", "(Ljava/lang/String;)I", &[(&key).into()]) + .unwrap() + .i() + .unwrap() +} + +pub(crate) fn bundle_get_bool(env: &mut JNIEnv, bundle: &JObject, key: &str) -> bool { + let key = env.new_string(key).unwrap(); + env.call_method( + bundle, + "getBoolean", + "(Ljava/lang/String;)Z", + &[(&key).into()], + ) + .unwrap() + .z() + .unwrap() +} + +pub(crate) fn get_package_name<'local>( + env: &mut JNIEnv<'local>, + view: &JObject, +) -> JObject<'local> { + let context = env + .call_method(view, "getContext", "()Landroid/content/Context;", &[]) + .unwrap() + .l() + .unwrap(); + env.call_method(&context, "getPackageName", "()Ljava/lang/String;", &[]) + .unwrap() + .l() + .unwrap() +} diff --git a/platforms/winit/src/platform_impl/android.rs b/platforms/winit/src/platform_impl/android.rs index 6de4310f9..c3d402371 100644 --- a/platforms/winit/src/platform_impl/android.rs +++ b/platforms/winit/src/platform_impl/android.rs @@ -37,8 +37,7 @@ impl Adapter { .unwrap() .l() .unwrap(); - let adapter = - InjectingAdapter::new(&mut env, &view, activation_handler, action_handler).unwrap(); + let adapter = InjectingAdapter::new(&mut env, &view, activation_handler, action_handler); Self { adapter } } From b0cf01a26ded03d722818a193fa6902f69bbc102 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Tue, 6 May 2025 23:36:56 +0200 Subject: [PATCH 23/24] fix: Mention caveats with window bounds under Wayland (#559) --- platforms/unix/src/adapter.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/platforms/unix/src/adapter.rs b/platforms/unix/src/adapter.rs index c56e61225..a7c44d581 100644 --- a/platforms/unix/src/adapter.rs +++ b/platforms/unix/src/adapter.rs @@ -144,6 +144,13 @@ impl Adapter { let _ = self.messages.send(message); } + /// Set the bounds of the top-level window. The outer bounds contain any + /// window decoration and borders. + /// + /// # Caveats + /// + /// Since an application can not get the position of its window under + /// Wayland, calling this method only makes sense under X11. pub fn set_root_window_bounds(&mut self, outer: Rect, inner: Rect) { let new_bounds = WindowBounds::new(outer, inner); let mut state = self.state.lock().unwrap(); From daf5fafd63c42f62cc824a1f72e17aff1d102108 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 7 May 2025 16:09:17 -0500 Subject: [PATCH 24/24] chore: release main (#538) --- .release-please-manifest.json | 2 +- Cargo.lock | 16 +++++++-------- common/CHANGELOG.md | 21 +++++++++++++++++++ common/Cargo.toml | 2 +- consumer/CHANGELOG.md | 32 +++++++++++++++++++++++++++++ consumer/Cargo.toml | 4 ++-- platforms/android/CHANGELOG.md | 29 ++++++++++++++++++++++++++ platforms/android/Cargo.toml | 6 +++--- platforms/atspi-common/CHANGELOG.md | 29 ++++++++++++++++++++++++++ platforms/atspi-common/Cargo.toml | 6 +++--- platforms/macos/CHANGELOG.md | 29 ++++++++++++++++++++++++++ platforms/macos/Cargo.toml | 6 +++--- platforms/unix/CHANGELOG.md | 25 ++++++++++++++++++++++ platforms/unix/Cargo.toml | 6 +++--- platforms/windows/CHANGELOG.md | 25 ++++++++++++++++++++++ platforms/windows/Cargo.toml | 6 +++--- platforms/winit/CHANGELOG.md | 24 ++++++++++++++++++++++ platforms/winit/Cargo.toml | 12 +++++------ 18 files changed, 247 insertions(+), 33 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ca6dcb312..0a317a360 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{"common":"0.18.0","consumer":"0.27.0","platforms/macos":"0.19.0","platforms/windows":"0.26.0","platforms/winit":"0.26.0","platforms/unix":"0.14.0","platforms/atspi-common":"0.11.0","platforms/android":"0.1.1"} \ No newline at end of file +{"common":"0.19.0","consumer":"0.28.0","platforms/macos":"0.20.0","platforms/windows":"0.27.0","platforms/winit":"0.27.0","platforms/unix":"0.15.0","platforms/atspi-common":"0.12.0","platforms/android":"0.2.0"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d69049a20..22005b798 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,7 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "accesskit" -version = "0.18.0" +version = "0.19.0" dependencies = [ "enumn", "pyo3", @@ -30,7 +30,7 @@ dependencies = [ [[package]] name = "accesskit_android" -version = "0.1.1" +version = "0.2.0" dependencies = [ "accesskit", "accesskit_consumer", @@ -40,7 +40,7 @@ dependencies = [ [[package]] name = "accesskit_atspi_common" -version = "0.11.0" +version = "0.12.0" dependencies = [ "accesskit", "accesskit_consumer", @@ -52,7 +52,7 @@ dependencies = [ [[package]] name = "accesskit_consumer" -version = "0.27.0" +version = "0.28.0" dependencies = [ "accesskit", "hashbrown", @@ -60,7 +60,7 @@ dependencies = [ [[package]] name = "accesskit_macos" -version = "0.19.0" +version = "0.20.0" dependencies = [ "accesskit", "accesskit_consumer", @@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.14.0" +version = "0.15.0" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -90,7 +90,7 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.26.0" +version = "0.27.0" dependencies = [ "accesskit", "accesskit_consumer", @@ -105,7 +105,7 @@ dependencies = [ [[package]] name = "accesskit_winit" -version = "0.26.0" +version = "0.27.0" dependencies = [ "accesskit", "accesskit_android", diff --git a/common/CHANGELOG.md b/common/CHANGELOG.md index a1351d016..7cf2922dd 100644 --- a/common/CHANGELOG.md +++ b/common/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [0.19.0](https://github.com/AccessKit/accesskit/compare/accesskit-v0.18.0...accesskit-v0.19.0) (2025-05-06) + + +### ⚠ BREAKING CHANGES + +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) +* Drop unused `Node::is_linked` ([#545](https://github.com/AccessKit/accesskit/issues/545)) +* Drop `FrozenNode` ([#496](https://github.com/AccessKit/accesskit/issues/496)) + +### Bug Fixes + +* Improve `NodeId`'s debug representation ([#547](https://github.com/AccessKit/accesskit/issues/547)) ([a47bca1](https://github.com/AccessKit/accesskit/commit/a47bca1e376de7b0a22a7dfe6c23dedad315c449)) +* Update pyo3 to 0.24 ([#544](https://github.com/AccessKit/accesskit/issues/544)) ([6338e45](https://github.com/AccessKit/accesskit/commit/6338e45097662bf39994e19a09054c20cb2ee782)) + + +### Code Refactoring + +* Drop `FrozenNode` ([#496](https://github.com/AccessKit/accesskit/issues/496)) ([f8c0d0a](https://github.com/AccessKit/accesskit/commit/f8c0d0a6fc9613cf1a2a6d8cfba11ebc892dfeb8)) +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) ([56abf17](https://github.com/AccessKit/accesskit/commit/56abf17356e4c7f13f64aaeaca6a63c8f7ede553)) +* Drop unused `Node::is_linked` ([#545](https://github.com/AccessKit/accesskit/issues/545)) ([3aab4ac](https://github.com/AccessKit/accesskit/commit/3aab4ac6f0193b8a06d7962f933582a4dbdf0c98)) + ## [0.18.0](https://github.com/AccessKit/accesskit/compare/accesskit-v0.17.1...accesskit-v0.18.0) (2025-03-06) diff --git a/common/Cargo.toml b/common/Cargo.toml index 5247f1db6..d1ab5bc15 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "accesskit" -version = "0.18.0" +version = "0.19.0" authors.workspace = true license.workspace = true description = "UI accessibility infrastructure across platforms" diff --git a/consumer/CHANGELOG.md b/consumer/CHANGELOG.md index 0f5547e6c..e1ea2fab5 100644 --- a/consumer/CHANGELOG.md +++ b/consumer/CHANGELOG.md @@ -24,6 +24,38 @@ * dependencies * accesskit bumped from 0.16.2 to 0.16.3 +## [0.28.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.27.0...accesskit_consumer-v0.28.0) (2025-05-06) + + +### ⚠ BREAKING CHANGES + +* Drop unused `Node::is_linked` ([#545](https://github.com/AccessKit/accesskit/issues/545)) +* Drop `FrozenNode` ([#496](https://github.com/AccessKit/accesskit/issues/496)) +* Replace `immutable-chunkmap` with dual tree states ([#495](https://github.com/AccessKit/accesskit/issues/495)) + +### Features + +* Expose tabs in consumer and atspi-common ([b1fb5b3](https://github.com/AccessKit/accesskit/commit/b1fb5b3de12c001e34021263038b66a6e3a7dd1e)) + + +### Bug Fixes + +* Improve `NodeId`'s debug representation ([#547](https://github.com/AccessKit/accesskit/issues/547)) ([a47bca1](https://github.com/AccessKit/accesskit/commit/a47bca1e376de7b0a22a7dfe6c23dedad315c449)) + + +### Code Refactoring + +* Drop `FrozenNode` ([#496](https://github.com/AccessKit/accesskit/issues/496)) ([f8c0d0a](https://github.com/AccessKit/accesskit/commit/f8c0d0a6fc9613cf1a2a6d8cfba11ebc892dfeb8)) +* Drop unused `Node::is_linked` ([#545](https://github.com/AccessKit/accesskit/issues/545)) ([3aab4ac](https://github.com/AccessKit/accesskit/commit/3aab4ac6f0193b8a06d7962f933582a4dbdf0c98)) +* Replace `immutable-chunkmap` with dual tree states ([#495](https://github.com/AccessKit/accesskit/issues/495)) ([a74dbfc](https://github.com/AccessKit/accesskit/commit/a74dbfcd2d30f9fbec781db811243ec070cbf8c5)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * accesskit bumped from 0.18.0 to 0.19.0 + ## [0.27.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.26.0...accesskit_consumer-v0.27.0) (2025-03-06) diff --git a/consumer/Cargo.toml b/consumer/Cargo.toml index 66165ded6..4f18ab95c 100644 --- a/consumer/Cargo.toml +++ b/consumer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "accesskit_consumer" -version = "0.27.0" +version = "0.28.0" authors.workspace = true license.workspace = true description = "AccessKit consumer library (internal)" @@ -12,5 +12,5 @@ edition.workspace = true rust-version.workspace = true [dependencies] -accesskit = { version = "0.18.0", path = "../common" } +accesskit = { version = "0.19.0", path = "../common" } hashbrown = { version = "0.15", default-features = false, features = ["default-hasher"] } diff --git a/platforms/android/CHANGELOG.md b/platforms/android/CHANGELOG.md index 111c513ca..c300c19d8 100644 --- a/platforms/android/CHANGELOG.md +++ b/platforms/android/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## [0.2.0](https://github.com/AccessKit/accesskit/compare/accesskit_android-v0.1.1...accesskit_android-v0.2.0) (2025-05-06) + + +### ⚠ BREAKING CHANGES + +* Simplify the core Android adapter API ([#558](https://github.com/AccessKit/accesskit/issues/558)) +* Use the queued-events pattern in the Android adapter ([#555](https://github.com/AccessKit/accesskit/issues/555)) +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) + +### Bug Fixes + +* Fix Android adapter after dropping `FrozenNode` ([#553](https://github.com/AccessKit/accesskit/issues/553)) ([735cb7e](https://github.com/AccessKit/accesskit/commit/735cb7e292b87e7660586a924954689e4894dcea)) +* Return text content from multiline inputs ([#552](https://github.com/AccessKit/accesskit/issues/552)) ([4b74090](https://github.com/AccessKit/accesskit/commit/4b74090dc0b848747296b4a66d3bbe3cef96fc56)) + + +### Code Refactoring + +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) ([56abf17](https://github.com/AccessKit/accesskit/commit/56abf17356e4c7f13f64aaeaca6a63c8f7ede553)) +* Simplify the core Android adapter API ([#558](https://github.com/AccessKit/accesskit/issues/558)) ([7ac5911](https://github.com/AccessKit/accesskit/commit/7ac5911b11f3d6b8b777b91e6476e7073f6b0e4a)) +* Use the queued-events pattern in the Android adapter ([#555](https://github.com/AccessKit/accesskit/issues/555)) ([0316518](https://github.com/AccessKit/accesskit/commit/0316518b94cf1bc9755e67f0cf48e37c096975fa)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * accesskit bumped from 0.18.0 to 0.19.0 + * accesskit_consumer bumped from 0.27.0 to 0.28.0 + ## [0.1.1](https://github.com/AccessKit/accesskit/compare/accesskit_android-v0.1.0...accesskit_android-v0.1.1) (2025-03-17) diff --git a/platforms/android/Cargo.toml b/platforms/android/Cargo.toml index b17d763f6..2a6dc335c 100644 --- a/platforms/android/Cargo.toml +++ b/platforms/android/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "accesskit_android" -version = "0.1.1" +version = "0.2.0" authors.workspace = true license.workspace = true description = "AccessKit UI accessibility infrastructure: Android adapter" @@ -15,7 +15,7 @@ rust-version.workspace = true embedded-dex = [] [dependencies] -accesskit = { version = "0.18.0", path = "../../common" } -accesskit_consumer = { version = "0.27.0", path = "../../consumer" } +accesskit = { version = "0.19.0", path = "../../common" } +accesskit_consumer = { version = "0.28.0", path = "../../consumer" } jni = "0.21.1" log = "0.4.17" diff --git a/platforms/atspi-common/CHANGELOG.md b/platforms/atspi-common/CHANGELOG.md index 15961da23..2c397ed5d 100644 --- a/platforms/atspi-common/CHANGELOG.md +++ b/platforms/atspi-common/CHANGELOG.md @@ -24,6 +24,35 @@ * accesskit bumped from 0.17.0 to 0.17.1 * accesskit_consumer bumped from 0.25.0 to 0.26.0 +## [0.12.0](https://github.com/AccessKit/accesskit/compare/accesskit_atspi_common-v0.11.0...accesskit_atspi_common-v0.12.0) (2025-05-06) + + +### ⚠ BREAKING CHANGES + +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) + +### Features + +* Expose tabs in consumer and atspi-common ([b1fb5b3](https://github.com/AccessKit/accesskit/commit/b1fb5b3de12c001e34021263038b66a6e3a7dd1e)) + + +### Bug Fixes + +* Fix a compilation error in atspi-common `Event::new` ([#537](https://github.com/AccessKit/accesskit/issues/537)) ([23b4d8d](https://github.com/AccessKit/accesskit/commit/23b4d8d49fed378899855a40e63aff10e829f6e8)) + + +### Code Refactoring + +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) ([56abf17](https://github.com/AccessKit/accesskit/commit/56abf17356e4c7f13f64aaeaca6a63c8f7ede553)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * accesskit bumped from 0.18.0 to 0.19.0 + * accesskit_consumer bumped from 0.27.0 to 0.28.0 + ## [0.11.0](https://github.com/AccessKit/accesskit/compare/accesskit_atspi_common-v0.10.1...accesskit_atspi_common-v0.11.0) (2025-03-06) diff --git a/platforms/atspi-common/Cargo.toml b/platforms/atspi-common/Cargo.toml index 2feaff99b..760644079 100644 --- a/platforms/atspi-common/Cargo.toml +++ b/platforms/atspi-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "accesskit_atspi_common" -version = "0.11.0" +version = "0.12.0" authors.workspace = true license.workspace = true description = "AccessKit UI accessibility infrastructure: core AT-SPI translation layer" @@ -15,8 +15,8 @@ rust-version.workspace = true simplified-api = [] [dependencies] -accesskit = { version = "0.18.0", path = "../../common" } -accesskit_consumer = { version = "0.27.0", path = "../../consumer" } +accesskit = { version = "0.19.0", path = "../../common" } +accesskit_consumer = { version = "0.28.0", path = "../../consumer" } atspi-common = { version = "0.9", default-features = false } serde = "1.0" thiserror = "1.0" diff --git a/platforms/macos/CHANGELOG.md b/platforms/macos/CHANGELOG.md index 398d49ae7..c6d29af36 100644 --- a/platforms/macos/CHANGELOG.md +++ b/platforms/macos/CHANGELOG.md @@ -37,6 +37,35 @@ * accesskit bumped from 0.16.2 to 0.16.3 * accesskit_consumer bumped from 0.24.2 to 0.24.3 +## [0.20.0](https://github.com/AccessKit/accesskit/compare/accesskit_macos-v0.19.0...accesskit_macos-v0.20.0) (2025-05-06) + + +### ⚠ BREAKING CHANGES + +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) + +### Features + +* Expose tabs in consumer and atspi-common ([b1fb5b3](https://github.com/AccessKit/accesskit/commit/b1fb5b3de12c001e34021263038b66a6e3a7dd1e)) + + +### Bug Fixes + +* Expose tabs in the platform adapters ([341a11b](https://github.com/AccessKit/accesskit/commit/341a11bca2c8a29682c11ddcfe91fa58776ea11d)) + + +### Code Refactoring + +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) ([56abf17](https://github.com/AccessKit/accesskit/commit/56abf17356e4c7f13f64aaeaca6a63c8f7ede553)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * accesskit bumped from 0.18.0 to 0.19.0 + * accesskit_consumer bumped from 0.27.0 to 0.28.0 + ## [0.19.0](https://github.com/AccessKit/accesskit/compare/accesskit_macos-v0.18.1...accesskit_macos-v0.19.0) (2025-03-06) diff --git a/platforms/macos/Cargo.toml b/platforms/macos/Cargo.toml index 3f54087c0..e2b4905a3 100644 --- a/platforms/macos/Cargo.toml +++ b/platforms/macos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "accesskit_macos" -version = "0.19.0" +version = "0.20.0" authors.workspace = true license.workspace = true description = "AccessKit UI accessibility infrastructure: macOS adapter" @@ -15,8 +15,8 @@ rust-version.workspace = true default-target = "x86_64-apple-darwin" [dependencies] -accesskit = { version = "0.18.0", path = "../../common" } -accesskit_consumer = { version = "0.27.0", path = "../../consumer" } +accesskit = { version = "0.19.0", path = "../../common" } +accesskit_consumer = { version = "0.28.0", path = "../../consumer" } hashbrown = { version = "0.15", default-features = false, features = ["default-hasher"] } objc2 = "0.5.1" objc2-foundation = { version = "0.2.0", features = [ diff --git a/platforms/unix/CHANGELOG.md b/platforms/unix/CHANGELOG.md index 7dab284ec..848f79c49 100644 --- a/platforms/unix/CHANGELOG.md +++ b/platforms/unix/CHANGELOG.md @@ -68,6 +68,31 @@ * accesskit bumped from 0.17.0 to 0.17.1 * accesskit_atspi_common bumped from 0.10.0 to 0.10.1 +## [0.15.0](https://github.com/AccessKit/accesskit/compare/accesskit_unix-v0.14.0...accesskit_unix-v0.15.0) (2025-05-06) + + +### ⚠ BREAKING CHANGES + +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) + +### Bug Fixes + +* Expose tabs in the platform adapters ([341a11b](https://github.com/AccessKit/accesskit/commit/341a11bca2c8a29682c11ddcfe91fa58776ea11d)) +* Mention caveats with window bounds under Wayland ([#559](https://github.com/AccessKit/accesskit/issues/559)) ([b0cf01a](https://github.com/AccessKit/accesskit/commit/b0cf01a26ded03d722818a193fa6902f69bbc102)) + + +### Code Refactoring + +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) ([56abf17](https://github.com/AccessKit/accesskit/commit/56abf17356e4c7f13f64aaeaca6a63c8f7ede553)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * accesskit bumped from 0.18.0 to 0.19.0 + * accesskit_atspi_common bumped from 0.11.0 to 0.12.0 + ## [0.14.0](https://github.com/AccessKit/accesskit/compare/accesskit_unix-v0.13.1...accesskit_unix-v0.14.0) (2025-03-06) diff --git a/platforms/unix/Cargo.toml b/platforms/unix/Cargo.toml index c7d4193d7..c11b49255 100644 --- a/platforms/unix/Cargo.toml +++ b/platforms/unix/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "accesskit_unix" -version = "0.14.0" +version = "0.15.0" authors.workspace = true license.workspace = true description = "AccessKit UI accessibility infrastructure: Linux adapter" @@ -17,8 +17,8 @@ async-io = ["dep:async-channel", "dep:async-executor", "dep:async-task", "dep:fu tokio = ["dep:tokio", "dep:tokio-stream"] [dependencies] -accesskit = { version = "0.18.0", path = "../../common" } -accesskit_atspi_common = { version = "0.11.0", path = "../atspi-common" } +accesskit = { version = "0.19.0", path = "../../common" } +accesskit_atspi_common = { version = "0.12.0", path = "../atspi-common" } atspi = { version = "0.25", default-features = false, features = ["async-std"] } futures-lite = "2.3" serde = "1.0" diff --git a/platforms/windows/CHANGELOG.md b/platforms/windows/CHANGELOG.md index 7f30ea399..6c76639e4 100644 --- a/platforms/windows/CHANGELOG.md +++ b/platforms/windows/CHANGELOG.md @@ -38,6 +38,31 @@ * accesskit bumped from 0.16.2 to 0.16.3 * accesskit_consumer bumped from 0.24.2 to 0.24.3 +## [0.27.0](https://github.com/AccessKit/accesskit/compare/accesskit_windows-v0.26.0...accesskit_windows-v0.27.0) (2025-05-06) + + +### ⚠ BREAKING CHANGES + +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) + +### Bug Fixes + +* Expose tabs in the platform adapters ([341a11b](https://github.com/AccessKit/accesskit/commit/341a11bca2c8a29682c11ddcfe91fa58776ea11d)) +* Update windows-rs to 0.61 ([#541](https://github.com/AccessKit/accesskit/issues/541)) ([2f86c45](https://github.com/AccessKit/accesskit/commit/2f86c453a776956ca36c06c9689be22323646421)) + + +### Code Refactoring + +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) ([56abf17](https://github.com/AccessKit/accesskit/commit/56abf17356e4c7f13f64aaeaca6a63c8f7ede553)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * accesskit bumped from 0.18.0 to 0.19.0 + * accesskit_consumer bumped from 0.27.0 to 0.28.0 + ## [0.26.0](https://github.com/AccessKit/accesskit/compare/accesskit_windows-v0.25.0...accesskit_windows-v0.26.0) (2025-03-17) diff --git a/platforms/windows/Cargo.toml b/platforms/windows/Cargo.toml index 05eb4de35..72eeb5a98 100644 --- a/platforms/windows/Cargo.toml +++ b/platforms/windows/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "accesskit_windows" -version = "0.26.0" +version = "0.27.0" authors.workspace = true license.workspace = true description = "AccessKit UI accessibility infrastructure: Windows adapter" @@ -16,8 +16,8 @@ default-target = "x86_64-pc-windows-msvc" targets = [] [dependencies] -accesskit = { version = "0.18.0", path = "../../common" } -accesskit_consumer = { version = "0.27.0", path = "../../consumer" } +accesskit = { version = "0.19.0", path = "../../common" } +accesskit_consumer = { version = "0.28.0", path = "../../consumer" } hashbrown = { version = "0.15", default-features = false, features = ["default-hasher"] } static_assertions = "1.1.0" windows-core = "0.61.0" diff --git a/platforms/winit/CHANGELOG.md b/platforms/winit/CHANGELOG.md index d656c8fd0..81403cc62 100644 --- a/platforms/winit/CHANGELOG.md +++ b/platforms/winit/CHANGELOG.md @@ -147,6 +147,30 @@ * accesskit_macos bumped from 0.18.0 to 0.18.1 * accesskit_unix bumped from 0.13.0 to 0.13.1 +## [0.27.0](https://github.com/AccessKit/accesskit/compare/accesskit_winit-v0.26.0...accesskit_winit-v0.27.0) (2025-05-06) + + +### ⚠ BREAKING CHANGES + +* Simplify the core Android adapter API ([#558](https://github.com/AccessKit/accesskit/issues/558)) +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) + +### Code Refactoring + +* Drop redundant `HasPopup::True` ([#550](https://github.com/AccessKit/accesskit/issues/550)) ([56abf17](https://github.com/AccessKit/accesskit/commit/56abf17356e4c7f13f64aaeaca6a63c8f7ede553)) +* Simplify the core Android adapter API ([#558](https://github.com/AccessKit/accesskit/issues/558)) ([7ac5911](https://github.com/AccessKit/accesskit/commit/7ac5911b11f3d6b8b777b91e6476e7073f6b0e4a)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * accesskit bumped from 0.18.0 to 0.19.0 + * accesskit_windows bumped from 0.26.0 to 0.27.0 + * accesskit_macos bumped from 0.19.0 to 0.20.0 + * accesskit_unix bumped from 0.14.0 to 0.15.0 + * accesskit_android bumped from 0.1.1 to 0.2.0 + ## [0.26.0](https://github.com/AccessKit/accesskit/compare/accesskit_winit-v0.25.0...accesskit_winit-v0.26.0) (2025-03-17) diff --git a/platforms/winit/Cargo.toml b/platforms/winit/Cargo.toml index 3fa56672b..80b5bd504 100644 --- a/platforms/winit/Cargo.toml +++ b/platforms/winit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "accesskit_winit" -version = "0.26.0" +version = "0.27.0" authors.workspace = true license = "Apache-2.0" description = "AccessKit UI accessibility infrastructure: winit adapter" @@ -19,22 +19,22 @@ async-io = ["accesskit_unix/async-io"] tokio = ["accesskit_unix/tokio"] [dependencies] -accesskit = { version = "0.18.0", path = "../../common" } +accesskit = { version = "0.19.0", path = "../../common" } winit = { version = "0.30.5", default-features = false } rwh_05 = { package = "raw-window-handle", version = "0.5", features = ["std"], optional = true } rwh_06 = { package = "raw-window-handle", version = "0.6.2", features = ["std"], optional = true } [target.'cfg(target_os = "windows")'.dependencies] -accesskit_windows = { version = "0.26.0", path = "../windows" } +accesskit_windows = { version = "0.27.0", path = "../windows" } [target.'cfg(target_os = "macos")'.dependencies] -accesskit_macos = { version = "0.19.0", path = "../macos" } +accesskit_macos = { version = "0.20.0", path = "../macos" } [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] -accesskit_unix = { version = "0.14.0", path = "../unix", optional = true, default-features = false } +accesskit_unix = { version = "0.15.0", path = "../unix", optional = true, default-features = false } [target.'cfg(target_os = "android")'.dependencies] -accesskit_android = { version = "0.1.1", path = "../android", optional = true, features = ["embedded-dex"] } +accesskit_android = { version = "0.2.0", path = "../android", optional = true, features = ["embedded-dex"] } [dev-dependencies.winit] version = "0.30.5"