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: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc26698f6..9e78c0857 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 }} @@ -53,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: @@ -91,11 +114,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..09cfe2487 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -2,11 +2,17 @@ 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 + - uses: googleapis/release-please-action@v4 with: - command: manifest + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} 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 d6a6b5e47..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,18 +30,17 @@ dependencies = [ [[package]] name = "accesskit_android" -version = "0.1.1" +version = "0.2.0" dependencies = [ "accesskit", "accesskit_consumer", "jni", "log", - "once_cell", ] [[package]] name = "accesskit_atspi_common" -version = "0.11.0" +version = "0.12.0" dependencies = [ "accesskit", "accesskit_consumer", @@ -53,16 +52,15 @@ dependencies = [ [[package]] name = "accesskit_consumer" -version = "0.27.0" +version = "0.28.0" dependencies = [ "accesskit", "hashbrown", - "immutable-chunkmap", ] [[package]] name = "accesskit_macos" -version = "0.19.0" +version = "0.20.0" dependencies = [ "accesskit", "accesskit_consumer", @@ -74,7 +72,7 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.14.0" +version = "0.15.0" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -92,7 +90,7 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.26.0" +version = "0.27.0" dependencies = [ "accesskit", "accesskit_consumer", @@ -107,7 +105,7 @@ dependencies = [ [[package]] name = "accesskit_winit" -version = "0.26.0" +version = "0.27.0" dependencies = [ "accesskit", "accesskit_android", @@ -141,7 +139,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 +251,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 0.38.44", "slab", "tracing", "windows-sys 0.59.0", @@ -285,7 +283,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix", + "rustix 0.38.44", "tracing", ] @@ -312,7 +310,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 0.38.44", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -473,7 +471,7 @@ dependencies = [ "bitflags 2.8.0", "log", "polling", - "rustix", + "rustix 0.38.44", "slab", "thiserror", ] @@ -485,7 +483,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 +559,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 +709,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 +716,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 +730,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 +810,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 +848,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" @@ -864,15 +858,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" @@ -969,6 +954,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 +1010,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 +1403,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]] @@ -1451,9 +1442,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", @@ -1469,9 +1460,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", @@ -1479,9 +1470,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", @@ -1489,9 +1480,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", @@ -1501,9 +1492,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", @@ -1540,6 +1531,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 +1573,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 +1750,7 @@ dependencies = [ "libc", "log", "memmap2", - "rustix", + "rustix 0.38.44", "thiserror", "wayland-backend", "wayland-client", @@ -1796,21 +1806,21 @@ 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" -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]] @@ -1860,9 +1870,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", @@ -1875,9 +1885,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", @@ -1886,9 +1896,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", @@ -1903,13 +1913,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 +2010,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 +2098,7 @@ checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" dependencies = [ "cc", "downcast-rs", - "rustix", + "rustix 0.38.44", "scoped-tls", "smallvec", "wayland-sys", @@ -2092,7 +2111,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 +2133,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", ] @@ -2233,32 +2252,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", @@ -2267,9 +2308,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", @@ -2277,40 +2318,46 @@ 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-strings" -version = "0.1.0" +name = "windows-result" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link", ] [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-strings" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-targets 0.42.2", + "windows-link", ] [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.42.2", ] [[package]] @@ -2542,7 +2589,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 +2611,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 +2649,7 @@ dependencies = [ "libc", "libloading", "once_cell", - "rustix", + "rustix 0.38.44", "x11rb-protocol", ] @@ -2679,7 +2726,7 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.59.0", - "winnow 0.7.3", + "winnow", "xdg-home", "zbus_macros", "zbus_names", @@ -2733,7 +2780,7 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow 0.7.3", + "winnow", "zvariant", ] @@ -2780,7 +2827,7 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "winnow 0.7.3", + "winnow", "zvariant_derive", "zvariant_utils", ] @@ -2809,5 +2856,5 @@ dependencies = [ "serde", "static_assertions", "syn", - "winnow 0.7.3", + "winnow", ] 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) 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 9dddb678a..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" @@ -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 } diff --git a/common/src/lib.rs b/common/src/lib.rs index f2d232994..7ff45c67c 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, @@ -625,7 +624,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 +644,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 @@ -696,7 +701,6 @@ pub struct TextSelection { #[repr(u8)] enum Flag { Hidden, - Linked, Multiselectable, Required, Visited, @@ -884,23 +888,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, @@ -936,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; @@ -949,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] } } @@ -974,31 +954,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] @@ -1021,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()); + } + })* } } @@ -1029,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, } })* } @@ -1043,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(), + _ => &[], } })* } @@ -1057,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, } })* } @@ -1096,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); } })* } @@ -1109,13 +1085,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,12 +1137,42 @@ 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,)*] } } + $(#[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()); + } + })* } } @@ -1195,12 +1194,32 @@ 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,)*] } } + $(#[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()); + } + })* } } @@ -1210,12 +1229,32 @@ 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,)*] } } + $(#[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()); + } + })* } } @@ -1225,12 +1264,32 @@ 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,)*] } } + $(#[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()); + } + })* } } @@ -1240,12 +1299,32 @@ 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,)*] } } + $(#[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()); + } + })* } } @@ -1255,12 +1334,32 @@ 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,)*] } } + $(#[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()); + } + })* } } @@ -1270,12 +1369,32 @@ 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,)*] } } + $(#[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()); + } + })* } } @@ -1285,12 +1404,34 @@ 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,)*] } } + $(#[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()); + } + })* } } @@ -1300,12 +1441,36 @@ 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,)*] } } + $(#[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()); + } + })* } } @@ -1315,37 +1480,44 @@ 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,)*] } } + $(#[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)),+) => { - 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,)*] } - } + ($($(#[$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] @@ -1358,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()); + } + })* } } @@ -1371,38 +1567,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 +1576,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 @@ -1444,7 +1599,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), @@ -1761,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! { @@ -1806,49 +1960,166 @@ 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,] } } -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 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()); + } } -impl fmt::Debug for FrozenNode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut fmt = f.debug_struct("FrozenNode"); +#[cfg(test)] +mod bounds { + use super::{Node, Rect, Role}; - fmt.field("role", &self.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()); + } +} - let supported_actions = action_mask_to_action_vec(self.actions); - if !supported_actions.is_empty() { - fmt.field("actions", &supported_actions); - } +#[cfg(test)] +mod text_selection { + use super::{Node, NodeId, Role, TextPosition, TextSelection}; - 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); + #[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()); + } +} - let custom_actions = self.custom_actions(); - if !custom_actions.is_empty() { - fmt.field("custom_actions", &custom_actions); - } +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) +} - fmt.finish() +#[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()); } } @@ -2416,9 +2687,28 @@ pub trait DeactivationHandler { #[cfg(test)] mod tests { use super::*; + use alloc::format; + + #[test] + fn u64_should_be_convertible_to_node_id() { + assert_eq!(NodeId::from(0u64), NodeId(0)); + assert_eq!(NodeId::from(1u64), NodeId(1)); + } #[test] - fn action_n() { + 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)); @@ -2450,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!( @@ -2487,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); + } } 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 743cc6cfc..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,6 +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"] } -immutable-chunkmap = "2.0.6" 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 = { diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 2c1214381..5154ac121 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -9,12 +9,11 @@ // 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}, - 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)] @@ -408,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 } @@ -692,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/consumer/src/tree.rs b/consumer/src/tree.rs index c45ad7fd6..371582410 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -3,17 +3,16 @@ // 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 alloc::{sync::Arc, vec}; +use accesskit::{Node as NodeData, NodeId, Tree as TreeData, TreeUpdate}; +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,11 +27,11 @@ struct InternalChanges { impl State { fn validate_global(&self) { - if self.nodes.get_key(&self.data.root).is_none() { - panic!("Root id #{} is not in the node list", self.data.root.0); + if !self.nodes.contains_key(&self.data.root) { + panic!("Root ID {:?} is not in the node list", self.data.root); } - if self.nodes.get_key(&self.focus).is_none() { - panic!("Focused id #{} is not in the node list", self.focus.0); + if !self.nodes.contains_key(&self.focus) { + panic!("Focused ID {:?} is not in the node list", self.focus); } } @@ -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,32 +63,33 @@ 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); } } 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()); 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); 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 +105,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 +114,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); } @@ -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; @@ -147,14 +147,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 +240,7 @@ pub trait ChangeHandler { #[derive(Debug)] pub struct Tree { state: State, + next_state: State, } impl Tree { @@ -248,17 +249,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 +267,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 +278,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 +318,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 { @@ -352,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, " ...")?; 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" 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 ab5863951..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,8 +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" -once_cell = "1.17.1" diff --git a/platforms/android/classes.dex b/platforms/android/classes.dex index 8685c5d7c..23dbaef57 100644 Binary files a/platforms/android/classes.dex and b/platforms/android/classes.dex differ diff --git a/platforms/android/java/dev/accesskit/android/Delegate.java b/platforms/android/java/dev/accesskit/android/Delegate.java index 3abca62c0..99b4eb845 100644 --- a/platforms/android/java/dev/accesskit/android/Delegate.java +++ b/platforms/android/java/dev/accesskit/android/Delegate.java @@ -3,389 +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, 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); - } - if (!Delegate.performAction(adapterHandle, virtualViewId, action)) { - return false; - } - switch (action) { - case AccessibilityNodeInfo.ACTION_CLICK: - sendEventInternal( - host, virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED); - break; - } - return true; + 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 a80a3679e..f92d238a1 100644 --- a/platforms/android/src/adapter.rs +++ b/platforms/android/src/adapter.rs @@ -14,44 +14,27 @@ 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::*}; - -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(); -} +use crate::{ + action::{PlatformAction, PlatformActionInner}, + event::{QueuedEvent, QueuedEvents}, + filters::filter, + node::{add_action, NodeWrapper}, + util::*, +}; -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::WindowContentChanged { + virtual_view_id: HOST_VIEW_ID, + }); } -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 +42,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 +91,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.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() || (new_node.raw_text_selection().is_some() @@ -143,21 +104,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 +118,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 +173,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,24 +190,13 @@ 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 { node_id_map: NodeIdMap, state: State, + accessibility_focus: Option, + hover_target: Option, } impl Adapter { @@ -272,123 +205,156 @@ 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)) } } } - #[allow(clippy::too_many_arguments)] - pub fn populate_node_info( + /// Create an `AccessibilityNodeInfo` for the AccessKit node + /// corresponding to the given virtual view ID. 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 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) } - pub fn perform_action( + fn perform_simple_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 mut events = Vec::new(); let request = match action { ACTION_CLICK => ActionRequest { action: { @@ -407,20 +373,44 @@ 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 false; + return None; } }; action_handler.do_action(request); - true + 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 @@ -447,21 +437,14 @@ 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)], 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 +454,47 @@ impl Adapter { Some(extra) } - #[allow(clippy::too_many_arguments)] - pub fn set_text_selection( + 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)) } - pub fn collapse_text_selection( + 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)] - pub fn traverse_text( + 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 +626,124 @@ 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)) + } + + /// 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 ebe399be7..6d5c32a8f 100644 --- a/platforms/android/src/inject.rs +++ b/platforms/android/src/inject.rs @@ -17,18 +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, util::*}; +use crate::{action::PlatformAction, adapter::Adapter, event::QueuedEvents}; struct InnerInjectingAdapter { adapter: Adapter, @@ -47,97 +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) -> bool { - self.adapter - .perform_action(&mut *self.action_handler, virtual_view_id, action) - } - - fn set_text_selection( + fn find_focus<'local>( &mut self, - env: &mut JNIEnv, - callback_class: &JClass, + env: &mut JNIEnv<'local>, 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, - ) + focus_type: jint, + ) -> JObject<'local> { + self.adapter + .find_focus(&mut *self.activation_handler, env, host, focus_type) } - fn collapse_text_selection( + fn perform_action( &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, - ) + action: &PlatformAction, + ) -> Option { + self.adapter + .perform_action(&mut *self.action_handler, virtual_view_id, action) } - #[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 { - self.adapter.traverse_text( - &mut *self.action_handler, - env, - callback_class, - host, - virtual_view_id, - granularity, - forward, - extend_selection, - ) + fn on_hover_event(&mut self, action: jint, x: jfloat, y: jfloat) -> Option { + self.adapter + .on_hover_event(&mut *self.activation_handler, action, x, y) } } @@ -150,216 +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( - _env: JNIEnv, - _class: JClass, +extern "system" fn find_focus<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, adapter_handle: jlong, - 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 inner_adapter.perform_action(virtual_view_id, action) { - 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 inner_adapter.set_text_selection(&mut env, &class, &host, virtual_view_id, anchor, focus) { - 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 inner_adapter.collapse_text_selection(&mut env, &class, &host, virtual_view_id) { - 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 inner_adapter.traverse_text( - &mut env, - &class, - &host, - virtual_view_id, - granularity, - forward == JNI_TRUE, - extend_selection == JNI_TRUE, - ) { - 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: "(JII)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 @@ -396,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), @@ -410,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 @@ -436,37 +395,69 @@ impl InjectingAdapter { let Some(host) = self.host.upgrade_local(&env).unwrap() else { return; }; - self.inner.lock().unwrap().adapter.update_if_active( - update_factory, + 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 0e9af77f5..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; - +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 be68aefaf..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<'_> { @@ -67,7 +77,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)> { @@ -132,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 @@ -279,7 +310,8 @@ impl NodeWrapper<'_> { | MOVEMENT_GRANULARITY_LINE | MOVEMENT_GRANULARITY_PARAGRAPH) .into()], - )?; + ) + .unwrap(); } let live = match self.0.live() { @@ -287,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 b20b78fe1..fa0845fed 100644 --- a/platforms/android/src/util.rs +++ b/platforms/android/src/util.rs @@ -5,20 +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; @@ -51,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/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 8b21ead00..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,9 +15,10 @@ 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" zvariant = { version = "5.4", default-features = false } + 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 8c914b72f..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), @@ -618,7 +627,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, 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 9a10a2105..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 = [ @@ -34,3 +34,4 @@ objc2-app-kit = { version = "0.2.0", features = [ "NSView", "NSWindow", ] } + 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 16c51aeba..13e1f0f8c 100644 --- a/platforms/macos/src/node.rs +++ b/platforms/macos/src/node.rs @@ -324,6 +324,11 @@ impl NodeWrapper<'_> { if let Some(toggled) = self.0.toggled() { 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() { return Some(Value::String(value)); } @@ -340,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 { @@ -414,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 @@ -832,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) { @@ -855,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 @@ -870,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 @@ -886,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, @@ -899,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| { @@ -936,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) @@ -958,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/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 5418af9cf..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" @@ -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/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(); 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/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 ca8498a9a..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,16 +16,15 @@ 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.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", @@ -41,3 +40,4 @@ features = [ once_cell = "1.13.0" scopeguard = "1.1.0" winit = "0.30" + 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/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 994cc08f4..e2ff22bd6 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::{ @@ -442,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); } @@ -688,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(); + } _ => (), } } @@ -768,8 +783,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()) @@ -823,6 +837,7 @@ macro_rules! properties { fn enqueue_simple_property_changes( &self, queue: &mut Vec, + platform_node: &PlatformNode, element: &IRawElementProviderSimple, old: &NodeWrapper, ) { @@ -839,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(), + ); + } } } }; @@ -850,15 +891,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 +1071,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..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::{ @@ -38,7 +39,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) } } @@ -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() { @@ -199,8 +221,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 +270,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 { 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 6d318ac7b..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,24 +19,25 @@ 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" default-features = false features = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] + 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 } }