diff --git a/Cargo.lock b/Cargo.lock index dce6e95ee..71cc73336 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,20 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "accesskit_unix" +version = "0.1.0" +dependencies = [ + "accesskit", + "accesskit_consumer", + "async-channel", + "atspi", + "futures-lite", + "parking_lot", + "serde", + "zbus", +] + [[package]] name = "accesskit_windows" version = "0.10.2" @@ -52,6 +66,7 @@ version = "0.7.3" dependencies = [ "accesskit", "accesskit_macos", + "accesskit_unix", "accesskit_windows", "parking_lot", "winit", @@ -63,6 +78,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "arrayref" version = "0.3.6" @@ -81,6 +105,132 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd" +[[package]] +name = "async-broadcast" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" +dependencies = [ + "event-listener", + "futures-core", + "parking_lot", +] + +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +dependencies = [ + "async-lock", + "autocfg", + "concurrent-queue", + "futures-lite", + "libc", + "log", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "windows-sys 0.42.0", +] + +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", +] + +[[package]] +name = "async-recursion" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-task" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" + +[[package]] +name = "async-trait" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atspi" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab84c09a770065868da0d713f1f4b35af85d96530a868f1c1a6c249178379187" +dependencies = [ + "async-recursion", + "async-trait", + "atspi-macros", + "enumflags2", + "futures-lite", + "serde", + "tracing", + "zbus", + "zbus_names", +] + +[[package]] +name = "atspi-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ebc5a6f61f6996eca56a4cece7b3fe7da3b86f0473c7b71ab44e229f3acce4" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "zbus", + "zbus_names", + "zvariant", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -99,6 +249,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + [[package]] name = "block-sys" version = "0.1.0-beta.1" @@ -130,6 +289,12 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "calloop" version = "0.10.1" @@ -195,6 +360,15 @@ dependencies = [ "objc", ] +[[package]] +name = "concurrent-queue" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -248,6 +422,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -257,6 +440,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossfont" version = "0.5.0" @@ -280,6 +472,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cty" version = "0.2.2" @@ -321,6 +523,47 @@ dependencies = [ "syn", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -362,6 +605,27 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" +[[package]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "enumset" version = "1.0.8" @@ -384,6 +648,12 @@ dependencies = [ "syn", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "expat-sys" version = "2.1.6" @@ -394,6 +664,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + [[package]] name = "flate2" version = "1.0.24" @@ -474,6 +753,86 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-io" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "ident_case" version = "1.0.1" @@ -532,9 +891,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libloading" @@ -622,7 +981,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -708,6 +1067,20 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nom" version = "7.1.1" @@ -780,6 +1153,22 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +[[package]] +name = "ordered-stream" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ca8c99d73c6e92ac1358f9f692c22c0bfd9c4701fa086f5d365c0d4ea818ea" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.12.1" @@ -800,7 +1189,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -815,6 +1204,18 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.25" @@ -833,34 +1234,85 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "windows-sys 0.42.0", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro-crate" -version = "1.1.3" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" dependencies = [ + "once_cell", "thiserror", "toml", ] [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "raw-window-handle" version = "0.4.3" @@ -881,13 +1333,50 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ryu" version = "1.0.5" @@ -961,6 +1450,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-xml-rs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + [[package]] name = "serde_derive" version = "1.0.126" @@ -994,6 +1495,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "servo-fontconfig" version = "0.5.1" @@ -1015,6 +1527,26 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + [[package]] name = "slotmap" version = "1.0.6" @@ -1049,6 +1581,22 @@ dependencies = [ "wayland-protocols", ] +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" @@ -1057,13 +1605,27 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.73" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", ] [[package]] @@ -1121,10 +1683,58 @@ dependencies = [ ] [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "vec_map" @@ -1138,6 +1748,12 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1281,6 +1897,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1343,6 +1968,21 @@ dependencies = [ "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.0" @@ -1444,7 +2084,7 @@ dependencies = [ "wayland-client", "wayland-protocols", "web-sys", - "windows-sys", + "windows-sys 0.36.1", "x11-dl", ] @@ -1482,3 +2122,93 @@ name = "xml-rs" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "zbus" +version = "3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938ea6da98c75c2c37a86007bd17fd8e208cbec24e086108c87ece98e9edec0d" +dependencies = [ + "async-broadcast", + "async-channel", + "async-executor", + "async-io", + "async-lock", + "async-recursion", + "async-task", + "async-trait", + "byteorder", + "derivative", + "dirs", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.25.1", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde-xml-rs", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "zbus_names" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c737644108627748a660d038974160e0cbb62605536091bdfa28fd7f64d43c8" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zvariant" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8c89c183461e11867ded456db252eae90874bc6769b7adbea464caa777e51" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index e15971ba5..35433b258 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "common", "consumer", "platforms/macos", + "platforms/unix", "platforms/windows", "platforms/winit", ] diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 4e8133701..c371a2011 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -103,17 +103,21 @@ impl<'a> Node<'a> { (self.tree_state.node_by_id(*parent).unwrap(), *index) }) } +} +impl NodeState { pub fn child_ids( &self, ) -> impl DoubleEndedIterator + ExactSizeIterator + FusedIterator - + 'a { - let data = &self.state.data; + + '_ { + let data = &self.data; data.children.iter().copied() } +} +impl<'a> Node<'a> { pub fn children( &self, ) -> impl DoubleEndedIterator> @@ -121,7 +125,8 @@ impl<'a> Node<'a> { + FusedIterator> + 'a { let state = self.tree_state; - self.child_ids() + self.state + .child_ids() .map(move |id| state.node_by_id(id).unwrap()) } @@ -267,23 +272,31 @@ impl<'a> Node<'a> { }; parent_transform * self.direct_transform() } +} + +impl NodeState { + pub fn raw_bounds(&self) -> Option { + self.data().bounds + } +} +impl<'a> Node<'a> { pub fn has_bounds(&self) -> bool { - self.data().bounds.is_some() + self.state.raw_bounds().is_some() } /// Returns the node's transformed bounding box relative to the tree's /// container (e.g. window). pub fn bounding_box(&self) -> Option { - self.data() - .bounds + self.state + .raw_bounds() .as_ref() .map(|rect| self.transform().transform_rect_bbox(*rect)) } pub(crate) fn bounding_box_in_coordinate_space(&self, other: &Node) -> Option { - self.data() - .bounds + self.state + .raw_bounds() .as_ref() .map(|rect| self.relative_transform(other).transform_rect_bbox(*rect)) } @@ -307,7 +320,7 @@ impl<'a> Node<'a> { } if filter_result == FilterResult::Include { - if let Some(rect) = &self.data().bounds { + if let Some(rect) = &self.state.raw_bounds() { if rect.contains(point) { return Some((*self, point)); } @@ -413,6 +426,10 @@ impl NodeState { self.data().multiline } + pub fn is_protected(&self) -> bool { + self.data().protected + } + pub fn default_action_verb(&self) -> Option { self.data().default_action_verb } diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 95c9a69a6..197ed116f 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -3,6 +3,7 @@ // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. +use accesskit::kurbo::Point; use accesskit::{ Action, ActionData, ActionHandler, ActionRequest, Live, Node as NodeData, NodeId, TextSelection, Tree as TreeData, TreeUpdate, @@ -386,6 +387,14 @@ impl Tree { }) } + pub fn scroll_to_point(&self, target: NodeId, point: Point) { + self.action_handler.do_action(ActionRequest { + action: Action::ScrollToPoint, + target, + data: Some(ActionData::ScrollToPoint(point)), + }) + } + pub fn select_text_range(&self, range: &TextRange) { let selection = TextSelection { anchor: range.start.downgrade(), diff --git a/platforms/unix/Cargo.toml b/platforms/unix/Cargo.toml new file mode 100644 index 000000000..839af5671 --- /dev/null +++ b/platforms/unix/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "accesskit_unix" +version = "0.1.0" +authors = ["Arnold Loubriat "] +license = "MIT/Apache-2.0" +description = "AccessKit UI accessibility infrastructure: Linux adapter" +categories = ["gui"] +keywords = ["gui", "ui", "accessibility"] +repository = "https://github.com/AccessKit/accesskit" +readme = "README.md" +edition = "2021" + +[dependencies] +accesskit = { version = "0.8.1", path = "../../common" } +accesskit_consumer = { version = "0.11.0", path = "../../consumer" } +async-channel = "1.8.0" +atspi = "0.8.7" +futures-lite = "1.12.0" +parking_lot = "0.12.1" +serde = "1.0" +zbus = "3.6" diff --git a/platforms/unix/README.md b/platforms/unix/README.md new file mode 100644 index 000000000..116f59780 --- /dev/null +++ b/platforms/unix/README.md @@ -0,0 +1,3 @@ +# AccessKit Unix adapter + +This is the Unix adapter for [AccessKit](https://accesskit.dev/). It exposes an AccessKit accessibility tree through the AT-SPI protocol. diff --git a/platforms/unix/src/adapter.rs b/platforms/unix/src/adapter.rs new file mode 100644 index 000000000..d9fca322d --- /dev/null +++ b/platforms/unix/src/adapter.rs @@ -0,0 +1,323 @@ +// Copyright 2022 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 crate::{ + atspi::{ + interfaces::{ + AccessibleInterface, ActionInterface, ComponentInterface, Event, ObjectEvent, + ValueInterface, WindowEvent, + }, + Bus, ObjectId, ACCESSIBLE_PATH_PREFIX, + }, + node::{filter, filter_detached, NodeWrapper, PlatformNode, PlatformRootNode}, + util::{AppContext, WindowBounds}, +}; +use accesskit::{kurbo::Rect, ActionHandler, NodeId, Role, TreeUpdate}; +use accesskit_consumer::{DetachedNode, FilterResult, Node, Tree, TreeChangeHandler, TreeState}; +use async_channel::{Receiver, Sender}; +use atspi::{Interface, InterfaceSet, State}; +use futures_lite::StreamExt; +use parking_lot::RwLock; +use std::sync::Arc; +use zbus::Task; + +pub struct Adapter { + atspi_bus: Bus, + _event_task: Task<()>, + events: Sender, + _app_context: Arc>, + root_window_bounds: Arc>, + tree: Arc, +} + +impl Adapter { + /// Create a new Unix adapter. + pub fn new( + app_name: String, + toolkit_name: String, + toolkit_version: String, + initial_state: impl 'static + FnOnce() -> TreeUpdate, + action_handler: Box, + ) -> Option { + let mut atspi_bus = Bus::a11y_bus()?; + let (event_sender, event_receiver) = async_channel::unbounded(); + let atspi_bus_copy = atspi_bus.clone(); + let event_task = atspi_bus.connection().inner().executor().spawn( + async move { + handle_events(atspi_bus_copy, event_receiver).await; + }, + "accesskit_event_task", + ); + let tree = Arc::new(Tree::new(initial_state(), action_handler)); + let app_context = Arc::new(RwLock::new(AppContext::new( + app_name, + toolkit_name, + toolkit_version, + ))); + atspi_bus + .register_root_node(PlatformRootNode::new(&app_context, &tree)) + .ok()?; + let adapter = Adapter { + atspi_bus, + _event_task: event_task, + events: event_sender, + _app_context: app_context, + root_window_bounds: Arc::new(RwLock::new(WindowBounds::default())), + tree, + }; + adapter.register_tree(); + Some(adapter) + } + + fn register_tree(&self) { + let reader = self.tree.read(); + let mut objects_to_add = Vec::new(); + + fn add_children(node: Node<'_>, to_add: &mut Vec) { + for child in node.filtered_children(&filter) { + to_add.push(child.id()); + add_children(child, to_add); + } + } + + objects_to_add.push(reader.root().id()); + add_children(reader.root(), &mut objects_to_add); + for id in objects_to_add { + let interfaces = NodeWrapper::Node(&reader.node_by_id(id).unwrap()).interfaces(); + self.register_interfaces(&self.tree, id, interfaces) + .unwrap(); + } + } + + fn register_interfaces( + &self, + tree: &Arc, + id: NodeId, + new_interfaces: InterfaceSet, + ) -> zbus::Result { + let path = format!("{}{}", ACCESSIBLE_PATH_PREFIX, ObjectId::from(id).as_str()); + if new_interfaces.contains(Interface::Accessible) { + self.atspi_bus.register_interface( + &path, + AccessibleInterface::new( + self.atspi_bus.unique_name().to_owned(), + PlatformNode::new(tree, id), + ), + )?; + } + if new_interfaces.contains(Interface::Action) { + self.atspi_bus + .register_interface(&path, ActionInterface::new(PlatformNode::new(tree, id)))?; + } + if new_interfaces.contains(Interface::Component) { + self.atspi_bus.register_interface( + &path, + ComponentInterface::new(PlatformNode::new(tree, id), &self.root_window_bounds), + )?; + } + if new_interfaces.contains(Interface::Value) { + self.atspi_bus + .register_interface(&path, ValueInterface::new(PlatformNode::new(tree, id)))?; + } + Ok(true) + } + + fn unregister_interfaces( + &self, + id: &ObjectId, + old_interfaces: InterfaceSet, + ) -> zbus::Result { + let path = format!("{}{}", ACCESSIBLE_PATH_PREFIX, id.as_str()); + if old_interfaces.contains(Interface::Accessible) { + self.atspi_bus + .unregister_interface::>(&path)?; + } + if old_interfaces.contains(Interface::Action) { + self.atspi_bus + .unregister_interface::(&path)?; + } + if old_interfaces.contains(Interface::Component) { + self.atspi_bus + .unregister_interface::(&path)?; + } + if old_interfaces.contains(Interface::Value) { + self.atspi_bus + .unregister_interface::(&path)?; + } + Ok(true) + } + + pub fn set_root_window_bounds(&self, outer: Rect, inner: Rect) { + let mut bounds = self.root_window_bounds.write(); + bounds.outer = outer; + bounds.inner = inner; + } + + /// Apply the provided update to the tree. + pub fn update(&self, update: TreeUpdate) { + struct Handler<'a> { + adapter: &'a Adapter, + tree: &'a Arc, + } + impl Handler<'_> { + fn add_node(&mut self, node: &Node) { + let interfaces = NodeWrapper::Node(node).interfaces(); + self.adapter + .register_interfaces(self.tree, node.id(), interfaces) + .unwrap(); + } + fn remove_node(&mut self, node: &DetachedNode) { + let node = NodeWrapper::DetachedNode(node); + self.adapter + .events + .send_blocking(Event::Object { + target: node.id(), + event: ObjectEvent::StateChanged(State::Defunct, true), + }) + .unwrap(); + self.adapter + .unregister_interfaces(&node.id(), node.interfaces()) + .unwrap(); + } + } + impl TreeChangeHandler for Handler<'_> { + fn node_added(&mut self, node: &Node) { + if filter(node) == FilterResult::Include { + self.add_node(node); + } + } + fn node_updated(&mut self, old_node: &DetachedNode, new_node: &Node) { + let filter_old = filter_detached(old_node); + let filter_new = filter(new_node); + if filter_new != filter_old { + if filter_new == FilterResult::Include { + self.add_node(new_node); + } else if filter_old == FilterResult::Include { + self.remove_node(old_node); + } + } else if filter_new == FilterResult::Include { + let old_wrapper = NodeWrapper::DetachedNode(old_node); + let new_wrapper = NodeWrapper::Node(new_node); + let old_interfaces = old_wrapper.interfaces(); + let new_interfaces = new_wrapper.interfaces(); + let kept_interfaces = old_interfaces & new_interfaces; + self.adapter + .unregister_interfaces(&new_wrapper.id(), old_interfaces ^ kept_interfaces) + .unwrap(); + self.adapter + .register_interfaces( + self.tree, + new_node.id(), + new_interfaces ^ kept_interfaces, + ) + .unwrap(); + new_wrapper.notify_changes( + &self.adapter.root_window_bounds.read(), + &self.adapter.events, + &old_wrapper, + ); + } + } + fn focus_moved(&mut self, old_node: Option<&DetachedNode>, new_node: Option<&Node>) { + if let Some(root_window) = root_window(&self.tree.read()) { + if old_node.is_none() && new_node.is_some() { + self.adapter.window_activated( + &NodeWrapper::Node(&root_window), + &self.adapter.events, + ); + } else if old_node.is_some() && new_node.is_none() { + self.adapter.window_deactivated( + &NodeWrapper::Node(&root_window), + &self.adapter.events, + ); + } + } + if let Some(node) = new_node.map(NodeWrapper::Node) { + self.adapter + .events + .send_blocking(Event::Object { + target: node.id(), + event: ObjectEvent::StateChanged(State::Focused, true), + }) + .unwrap(); + } + if let Some(node) = old_node.map(NodeWrapper::DetachedNode) { + self.adapter + .events + .send_blocking(Event::Object { + target: node.id(), + event: ObjectEvent::StateChanged(State::Focused, false), + }) + .unwrap(); + } + } + fn node_removed(&mut self, node: &DetachedNode, _: &TreeState) { + if filter_detached(node) == FilterResult::Include { + self.remove_node(node); + } + } + } + let mut handler = Handler { + adapter: self, + tree: &self.tree, + }; + self.tree.update_and_process_changes(update, &mut handler); + } + + fn window_activated(&self, window: &NodeWrapper, events: &Sender) { + events + .send_blocking(Event::Window { + target: window.id(), + name: window.name(), + event: WindowEvent::Activated, + }) + .unwrap(); + events + .send_blocking(Event::Object { + target: window.id(), + event: ObjectEvent::StateChanged(State::Active, true), + }) + .unwrap(); + } + + fn window_deactivated(&self, window: &NodeWrapper, events: &Sender) { + events + .send_blocking(Event::Window { + target: window.id(), + name: window.name(), + event: WindowEvent::Deactivated, + }) + .unwrap(); + events + .send_blocking(Event::Object { + target: window.id(), + event: ObjectEvent::StateChanged(State::Active, false), + }) + .unwrap(); + } +} + +fn root_window(current_state: &TreeState) -> Option { + const WINDOW_ROLES: &[Role] = &[Role::AlertDialog, Role::Dialog, Role::Window]; + let root = current_state.root(); + if WINDOW_ROLES.contains(&root.role()) { + Some(root) + } else { + None + } +} + +async fn handle_events(bus: Bus, mut events: Receiver) { + while let Some(event) = events.next().await { + let _ = match event { + Event::Object { target, event } => bus.emit_object_event(target, event).await, + Event::Window { + target, + name, + event, + } => bus.emit_window_event(target, name, event).await, + }; + } +} diff --git a/platforms/unix/src/atspi/bus.rs b/platforms/unix/src/atspi/bus.rs new file mode 100644 index 000000000..ceac25a14 --- /dev/null +++ b/platforms/unix/src/atspi/bus.rs @@ -0,0 +1,247 @@ +// Copyright 2022 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 crate::{ + atspi::{interfaces::*, object_address::*, ObjectId}, + PlatformRootNode, +}; +use atspi::{bus::BusProxyBlocking, socket::SocketProxyBlocking, EventBody}; +use serde::Serialize; +use std::{collections::HashMap, env::var}; +use zbus::{ + blocking::{Connection, ConnectionBuilder}, + names::{BusName, InterfaceName, MemberName, OwnedUniqueName}, + zvariant::{ObjectPath, Str, Value}, + Address, Result, +}; + +#[derive(Clone)] +pub(crate) struct Bus { + conn: Connection, + socket_proxy: SocketProxyBlocking<'static>, +} + +impl Bus { + pub fn a11y_bus() -> Option { + let conn = a11y_bus()?; + let socket_proxy = SocketProxyBlocking::new(&conn).ok()?; + Some(Bus { conn, socket_proxy }) + } + + pub(crate) fn connection(&self) -> &Connection { + &self.conn + } + + pub fn unique_name(&self) -> &OwnedUniqueName { + self.conn.unique_name().unwrap() + } + + pub fn register_interface(&self, path: &str, interface: T) -> Result + where + T: zbus::Interface, + { + self.conn.object_server().at(path, interface) + } + + pub fn unregister_interface(&self, path: &str) -> Result + where + T: zbus::Interface, + { + self.conn.object_server().remove::(path) + } + + pub fn register_root_node(&mut self, node: PlatformRootNode) -> Result { + let path = format!("{}{}", ACCESSIBLE_PATH_PREFIX, ObjectId::root().as_str()); + let registered = self + .conn + .object_server() + .at(path.clone(), ApplicationInterface(node.clone()))? + && self.conn.object_server().at( + path, + AccessibleInterface::new(self.unique_name().to_owned(), node.clone()), + )?; + if registered { + let desktop = self.socket_proxy.embed(&( + self.unique_name().as_str(), + ObjectPath::from_str_unchecked(ROOT_PATH), + ))?; + if let Some(context) = node.context.upgrade() { + context.write().desktop_address = Some(desktop.into()); + } + Ok(true) + } else { + Ok(false) + } + } + + pub(crate) async fn emit_object_event( + &self, + target: ObjectId<'_>, + event: ObjectEvent, + ) -> Result<()> { + let interface = "org.a11y.atspi.Event.Object"; + let signal = match event { + ObjectEvent::BoundsChanged(_) => "BoundsChanged", + ObjectEvent::ChildAdded(_, _) | ObjectEvent::ChildRemoved(_) => "ChildrenChanged", + ObjectEvent::PropertyChanged(_) => "PropertyChange", + ObjectEvent::StateChanged(_, _) => "StateChanged", + }; + let properties = HashMap::new(); + match event { + ObjectEvent::BoundsChanged(bounds) => { + self.emit_event( + target, + interface, + signal, + EventBody { + kind: "", + detail1: 0, + detail2: 0, + any_data: Value::from(bounds), + properties, + }, + ) + .await + } + ObjectEvent::ChildAdded(index, child) => { + self.emit_event( + target, + interface, + signal, + EventBody { + kind: "add", + detail1: index as i32, + detail2: 0, + any_data: child.into_value(self.unique_name().clone()), + properties, + }, + ) + .await + } + ObjectEvent::ChildRemoved(child) => { + self.emit_event( + target, + interface, + signal, + EventBody { + kind: "remove", + detail1: -1, + detail2: 0, + any_data: child.into_value(self.unique_name().clone()), + properties, + }, + ) + .await + } + ObjectEvent::PropertyChanged(property) => { + self.emit_event( + target, + interface, + signal, + EventBody { + kind: match property { + Property::Name(_) => "accessible-name", + Property::Description(_) => "accessible-description", + Property::Parent(_) => "accessible-parent", + Property::Role(_) => "accessible-role", + Property::Value(_) => "accessible-value", + }, + detail1: 0, + detail2: 0, + any_data: match property { + Property::Name(value) => Str::from(value).into(), + Property::Description(value) => Str::from(value).into(), + Property::Parent(Some(parent)) => { + parent.into_value(self.unique_name().clone()) + } + Property::Parent(None) => { + OwnedObjectAddress::root(self.unique_name().clone()).into() + } + Property::Role(value) => Value::U32(value as u32), + Property::Value(value) => Value::F64(value), + }, + properties, + }, + ) + .await + } + ObjectEvent::StateChanged(state, value) => { + self.emit_event( + target, + interface, + signal, + EventBody { + kind: state, + detail1: value as i32, + detail2: 0, + any_data: 0i32.into(), + properties, + }, + ) + .await + } + } + } + + pub(crate) async fn emit_window_event( + &self, + target: ObjectId<'_>, + window_name: String, + event: WindowEvent, + ) -> Result<()> { + let signal = match event { + WindowEvent::Activated => "Activate", + WindowEvent::Deactivated => "Deactivate", + }; + self.emit_event( + target, + "org.a11y.atspi.Event.Window", + signal, + EventBody { + kind: "", + detail1: 0, + detail2: 0, + any_data: window_name.into(), + properties: HashMap::new(), + }, + ) + .await + } + + async fn emit_event( + &self, + id: ObjectId<'_>, + interface: &str, + signal_name: &str, + body: EventBody<'_, T>, + ) -> Result<()> { + let path = format!("{}{}", ACCESSIBLE_PATH_PREFIX, id.as_str()); + self.conn + .inner() + .emit_signal( + Option::::None, + path, + InterfaceName::from_str_unchecked(interface), + MemberName::from_str_unchecked(signal_name), + &body, + ) + .await + } +} + +fn a11y_bus() -> Option { + let address = match var("AT_SPI_BUS_ADDRESS") { + Ok(address) if !address.is_empty() => address, + _ => { + let session_bus = Connection::session().ok()?; + BusProxyBlocking::new(&session_bus) + .ok()? + .get_address() + .ok()? + } + }; + let address: Address = address.as_str().try_into().ok()?; + ConnectionBuilder::address(address).ok()?.build().ok() +} diff --git a/platforms/unix/src/atspi/interfaces/accessible.rs b/platforms/unix/src/atspi/interfaces/accessible.rs new file mode 100644 index 000000000..2d96ff091 --- /dev/null +++ b/platforms/unix/src/atspi/interfaces/accessible.rs @@ -0,0 +1,208 @@ +// Copyright 2022 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 crate::{ + atspi::{ObjectId, ObjectRef, OwnedObjectAddress}, + unknown_object, PlatformNode, PlatformRootNode, +}; +use atspi::{accessible::Role, Interface, InterfaceSet, StateSet}; +use std::convert::TryInto; +use zbus::{fdo, names::OwnedUniqueName, MessageHeader}; + +pub(crate) struct AccessibleInterface { + bus_name: OwnedUniqueName, + node: T, +} + +impl AccessibleInterface { + pub fn new(bus_name: OwnedUniqueName, node: T) -> Self { + Self { bus_name, node } + } +} + +#[dbus_interface(name = "org.a11y.atspi.Accessible")] +impl AccessibleInterface { + #[dbus_interface(property)] + fn name(&self) -> String { + self.node.name().unwrap_or_default() + } + + #[dbus_interface(property)] + fn description(&self) -> String { + self.node.description().unwrap_or_default() + } + + #[dbus_interface(property)] + fn parent(&self) -> OwnedObjectAddress { + match self.node.parent() { + Ok(ObjectRef::Managed(id)) => OwnedObjectAddress::accessible(self.bus_name.clone(), id), + Ok(ObjectRef::Unmanaged(address)) => address, + _ => OwnedObjectAddress::null(self.bus_name.clone()), + } + } + + #[dbus_interface(property)] + fn child_count(&self) -> i32 { + self.node.child_count().unwrap_or(0) + } + + #[dbus_interface(property)] + fn locale(&self) -> &str { + "" + } + + #[dbus_interface(property)] + fn accessible_id(&self) -> ObjectId { + self.node.accessible_id() + } + + fn get_child_at_index( + &self, + #[zbus(header)] hdr: MessageHeader<'_>, + index: i32, + ) -> fdo::Result<(OwnedObjectAddress,)> { + let index = index + .try_into() + .map_err(|_| fdo::Error::InvalidArgs("Index can't be negative.".into()))?; + super::object_address(hdr.destination()?, self.node.child_at_index(index)?) + } + + fn get_children(&self) -> fdo::Result> { + Ok(self + .node + .children()? + .into_iter() + .map(|child| match child { + ObjectRef::Managed(id) => OwnedObjectAddress::accessible(self.bus_name.clone(), id), + ObjectRef::Unmanaged(address) => address, + }) + .collect()) + } + + fn get_index_in_parent(&self) -> fdo::Result { + self.node.index_in_parent() + } + + fn get_role(&self) -> fdo::Result { + self.node.role() + } + + fn get_state(&self) -> fdo::Result { + self.node.state() + } + + fn get_application( + &self, + #[zbus(header)] hdr: MessageHeader<'_>, + ) -> fdo::Result<(OwnedObjectAddress,)> { + super::object_address( + hdr.destination()?, + Some(ObjectRef::Managed(ObjectId::root())), + ) + } + + fn get_interfaces(&self) -> fdo::Result { + self.node.interfaces() + } +} + +#[dbus_interface(name = "org.a11y.atspi.Accessible")] +impl AccessibleInterface { + #[dbus_interface(property)] + fn name(&self) -> String { + self.node + .context + .upgrade() + .map(|context| context.read().name.clone()) + .unwrap_or_default() + } + + #[dbus_interface(property)] + fn description(&self) -> &str { + "" + } + + #[dbus_interface(property)] + fn parent(&self) -> OwnedObjectAddress { + self.node + .context + .upgrade() + .and_then(|context| context.read().desktop_address.clone()) + .unwrap_or_else(|| OwnedObjectAddress::null(self.bus_name.clone())) + } + + #[dbus_interface(property)] + fn child_count(&self) -> i32 { + // TODO: Handle multiple top-level windows. + 1 + } + + #[dbus_interface(property)] + fn locale(&self) -> &str { + "" + } + + #[dbus_interface(property)] + fn accessible_id(&self) -> ObjectId { + ObjectId::root() + } + + fn get_child_at_index( + &self, + #[zbus(header)] hdr: MessageHeader<'_>, + index: i32, + ) -> fdo::Result<(OwnedObjectAddress,)> { + // TODO: Handle multiple top-level windows. + if index != 0 { + return super::object_address(hdr.destination()?, None); + } + let child = self + .node + .tree + .upgrade() + .map(|tree| ObjectRef::Managed(tree.read().root().id().into())); + super::object_address(hdr.destination()?, child) + } + + fn get_children(&self) -> fdo::Result> { + // TODO: Handle multiple top-level windows. + self.node + .tree + .upgrade() + .map(|tree| { + vec![OwnedObjectAddress::accessible( + self.bus_name.clone(), + tree.read().root().id().into(), + )] + }) + .ok_or_else(|| unknown_object(&ObjectId::root())) + } + + fn get_index_in_parent(&self) -> i32 { + -1 + } + + fn get_role(&self) -> Role { + Role::Application + } + + fn get_state(&self) -> StateSet { + StateSet::empty() + } + + fn get_application( + &self, + #[zbus(header)] hdr: MessageHeader<'_>, + ) -> fdo::Result<(OwnedObjectAddress,)> { + super::object_address( + hdr.destination()?, + Some(ObjectRef::Managed(ObjectId::root())), + ) + } + + fn get_interfaces(&self) -> InterfaceSet { + InterfaceSet::new(Interface::Accessible | Interface::Application) + } +} diff --git a/platforms/unix/src/atspi/interfaces/action.rs b/platforms/unix/src/atspi/interfaces/action.rs new file mode 100644 index 000000000..9c8bbbb36 --- /dev/null +++ b/platforms/unix/src/atspi/interfaces/action.rs @@ -0,0 +1,55 @@ +// Copyright 2022 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 crate::PlatformNode; +use serde::{Deserialize, Serialize}; +use zbus::{dbus_interface, fdo, zvariant::Type}; + +#[derive(Deserialize, Serialize, Type)] +pub(crate) struct Action { + pub localized_name: String, + pub description: String, + pub key_binding: String, +} + +pub(crate) struct ActionInterface(PlatformNode); + +impl ActionInterface { + pub fn new(node: PlatformNode) -> Self { + Self(node) + } +} + +#[dbus_interface(name = "org.a11y.atspi.Action")] +impl ActionInterface { + #[dbus_interface(property)] + fn n_actions(&self) -> i32 { + self.0.n_actions().unwrap_or(0) + } + + fn get_description(&self, _index: i32) -> &str { + "" + } + + fn get_name(&self, index: i32) -> fdo::Result { + self.0.get_action_name(index) + } + + fn get_localized_name(&self, index: i32) -> fdo::Result { + self.0.get_action_name(index) + } + + fn get_key_binding(&self, _index: i32) -> &str { + "" + } + + fn get_actions(&self) -> fdo::Result> { + self.0.get_actions() + } + + fn do_action(&self, index: i32) -> fdo::Result { + self.0.do_action(index) + } +} diff --git a/platforms/unix/src/atspi/interfaces/application.rs b/platforms/unix/src/atspi/interfaces/application.rs new file mode 100644 index 000000000..ccd9a9790 --- /dev/null +++ b/platforms/unix/src/atspi/interfaces/application.rs @@ -0,0 +1,53 @@ +// Copyright 2022 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 crate::{atspi::ObjectId, unknown_object, PlatformRootNode}; +use zbus::fdo; + +pub(crate) struct ApplicationInterface(pub PlatformRootNode); + +#[dbus_interface(name = "org.a11y.atspi.Application")] +impl ApplicationInterface { + #[dbus_interface(property)] + fn toolkit_name(&self) -> String { + self.0 + .context + .upgrade() + .map(|context| context.read().toolkit_name.clone()) + .unwrap_or_default() + } + + #[dbus_interface(property)] + fn version(&self) -> String { + self.0 + .context + .upgrade() + .map(|context| context.read().toolkit_version.clone()) + .unwrap_or_default() + } + + #[dbus_interface(property)] + fn atspi_version(&self) -> &str { + "2.1" + } + + #[dbus_interface(property)] + fn id(&self) -> i32 { + self.0 + .context + .upgrade() + .and_then(|context| context.read().id) + .unwrap_or(-1) + } + + #[dbus_interface(property)] + fn set_id(&mut self, id: i32) -> fdo::Result<()> { + self.0 + .context + .upgrade() + .map(|context| context.write().id = Some(id)) + .ok_or_else(|| unknown_object(&ObjectId::root())) + } +} diff --git a/platforms/unix/src/atspi/interfaces/component.rs b/platforms/unix/src/atspi/interfaces/component.rs new file mode 100644 index 000000000..42b638c34 --- /dev/null +++ b/platforms/unix/src/atspi/interfaces/component.rs @@ -0,0 +1,82 @@ +// Copyright 2022 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 crate::{ + atspi::{OwnedObjectAddress, Rect}, + unknown_object, + util::WindowBounds, + PlatformNode, +}; +use atspi::{component::Layer, CoordType}; +use parking_lot::RwLock; +use std::sync::{Arc, Weak}; +use zbus::{fdo, MessageHeader}; + +pub(crate) struct ComponentInterface { + node: PlatformNode, + root_window_bounds: Weak>, +} + +impl ComponentInterface { + pub(crate) fn new(node: PlatformNode, root_window_bounds: &Arc>) -> Self { + Self { + node, + root_window_bounds: Arc::downgrade(root_window_bounds), + } + } + + fn upgrade_bounds(&self) -> fdo::Result>> { + if let Some(bounds) = self.root_window_bounds.upgrade() { + Ok(bounds) + } else { + Err(unknown_object(&self.node.accessible_id())) + } + } +} + +#[dbus_interface(name = "org.a11y.atspi.Component")] +impl ComponentInterface { + fn contains(&self, x: i32, y: i32, coord_type: CoordType) -> fdo::Result { + let window_bounds = self.upgrade_bounds()?; + let contains = self.node.contains(&window_bounds.read(), x, y, coord_type); + contains + } + + fn get_accessible_at_point( + &self, + #[zbus(header)] hdr: MessageHeader<'_>, + x: i32, + y: i32, + coord_type: CoordType, + ) -> fdo::Result<(OwnedObjectAddress,)> { + let window_bounds = self.upgrade_bounds()?; + let accessible = + self.node + .get_accessible_at_point(&window_bounds.read(), x, y, coord_type)?; + super::object_address(hdr.destination()?, accessible) + } + + fn get_extents(&self, coord_type: CoordType) -> fdo::Result<(Rect,)> { + let window_bounds = self.upgrade_bounds()?; + let extents = self.node.get_extents(&window_bounds.read(), coord_type); + extents + } + + fn get_layer(&self) -> fdo::Result { + self.node.get_layer() + } + + fn grab_focus(&self) -> fdo::Result { + self.node.grab_focus() + } + + fn scroll_to_point(&self, coord_type: CoordType, x: i32, y: i32) -> fdo::Result { + let window_bounds = self.upgrade_bounds()?; + let scrolled = self + .node + .scroll_to_point(&window_bounds.read(), coord_type, x, y); + scrolled + } +} diff --git a/platforms/unix/src/atspi/interfaces/events.rs b/platforms/unix/src/atspi/interfaces/events.rs new file mode 100644 index 000000000..5824a84e4 --- /dev/null +++ b/platforms/unix/src/atspi/interfaces/events.rs @@ -0,0 +1,41 @@ +// Copyright 2022 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 crate::atspi::{ObjectId, ObjectRef, Rect}; +use atspi::{accessible::Role, State}; + +pub(crate) enum Event { + Object { + target: ObjectId<'static>, + event: ObjectEvent, + }, + Window { + target: ObjectId<'static>, + name: String, + event: WindowEvent, + }, +} + +pub(crate) enum Property { + Name(String), + Description(String), + Parent(Option), + Role(Role), + Value(f64), +} + +#[allow(clippy::enum_variant_names)] +pub(crate) enum ObjectEvent { + BoundsChanged(Rect), + ChildAdded(usize, ObjectRef), + ChildRemoved(ObjectRef), + PropertyChanged(Property), + StateChanged(State, bool), +} + +pub(crate) enum WindowEvent { + Activated, + Deactivated, +} diff --git a/platforms/unix/src/atspi/interfaces/mod.rs b/platforms/unix/src/atspi/interfaces/mod.rs new file mode 100644 index 000000000..2b0c54940 --- /dev/null +++ b/platforms/unix/src/atspi/interfaces/mod.rs @@ -0,0 +1,42 @@ +// Copyright 2022 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. + +mod accessible; +mod action; +mod application; +mod component; +mod events; +mod value; + +use crate::atspi::{ObjectRef, OwnedObjectAddress}; +use zbus::{ + fdo, + names::{BusName, OwnedUniqueName, UniqueName}, +}; + +fn object_address( + destination: Option<&BusName>, + object_ref: Option, +) -> fdo::Result<(OwnedObjectAddress,)> { + match object_ref { + Some(ObjectRef::Managed(id)) => { + Ok((OwnedObjectAddress::accessible(app_name(destination)?, id),)) + } + Some(ObjectRef::Unmanaged(address)) => Ok((address,)), + None => Ok((OwnedObjectAddress::null(app_name(destination)?),)), + } +} + +fn app_name(destination: Option<&BusName>) -> fdo::Result { + let destination = destination.ok_or(fdo::Error::ZBus(zbus::Error::MissingField))?; + Ok(UniqueName::from_str_unchecked(destination.as_str()).into()) +} + +pub(crate) use accessible::*; +pub(crate) use action::*; +pub(crate) use application::*; +pub(crate) use component::*; +pub(crate) use events::*; +pub(crate) use value::*; diff --git a/platforms/unix/src/atspi/interfaces/value.rs b/platforms/unix/src/atspi/interfaces/value.rs new file mode 100644 index 000000000..b949154f6 --- /dev/null +++ b/platforms/unix/src/atspi/interfaces/value.rs @@ -0,0 +1,44 @@ +// Copyright 2022 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 crate::PlatformNode; + +pub(crate) struct ValueInterface { + node: PlatformNode, +} + +impl ValueInterface { + pub fn new(node: PlatformNode) -> Self { + Self { node } + } +} + +#[dbus_interface(name = "org.a11y.atspi.Value")] +impl ValueInterface { + #[dbus_interface(property)] + fn minimum_value(&self) -> f64 { + self.node.minimum_value().unwrap() + } + + #[dbus_interface(property)] + fn maximum_value(&self) -> f64 { + self.node.maximum_value().unwrap() + } + + #[dbus_interface(property)] + fn minimum_increment(&self) -> f64 { + self.node.minimum_increment().unwrap() + } + + #[dbus_interface(property)] + fn current_value(&self) -> f64 { + self.node.current_value().unwrap() + } + + #[dbus_interface(property)] + fn set_current_value(&self, value: f64) { + self.node.set_current_value(value).unwrap(); + } +} diff --git a/platforms/unix/src/atspi/mod.rs b/platforms/unix/src/atspi/mod.rs new file mode 100644 index 000000000..fc85461d7 --- /dev/null +++ b/platforms/unix/src/atspi/mod.rs @@ -0,0 +1,46 @@ +// Copyright 2022 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 serde::{Deserialize, Serialize}; +use zbus::zvariant::{OwnedValue, Type, Value}; + +mod bus; +pub(crate) mod interfaces; +mod object_address; +mod object_id; +mod object_ref; + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, OwnedValue, Type, Value)] +pub(crate) struct Rect { + x: i32, + y: i32, + width: i32, + height: i32, +} + +impl Rect { + pub const INVALID: Rect = Rect { + x: -1, + y: -1, + width: -1, + height: -1, + }; +} + +impl From for Rect { + fn from(value: accesskit::kurbo::Rect) -> Rect { + Rect { + x: value.x0 as i32, + y: value.y0 as i32, + width: value.width() as i32, + height: value.height() as i32, + } + } +} + +pub(crate) use bus::Bus; +pub(crate) use object_address::*; +pub(crate) use object_id::*; +pub(crate) use object_ref::*; diff --git a/platforms/unix/src/atspi/object_address.rs b/platforms/unix/src/atspi/object_address.rs new file mode 100644 index 000000000..2e498909d --- /dev/null +++ b/platforms/unix/src/atspi/object_address.rs @@ -0,0 +1,58 @@ +// Copyright 2022 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 crate::atspi::ObjectId; +use serde::{Deserialize, Serialize}; +use zbus::{ + names::{OwnedUniqueName, UniqueName}, + zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Type, Value}, +}; + +pub(crate) const ACCESSIBLE_PATH_PREFIX: &str = "/org/a11y/atspi/accessible/"; +pub(crate) const NULL_PATH: &str = "/org/a11y/atspi/null"; +pub(crate) const ROOT_PATH: &str = "/org/a11y/atspi/accessible/root"; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, OwnedValue, Type, Value)] +pub(crate) struct OwnedObjectAddress { + bus_name: OwnedUniqueName, + path: OwnedObjectPath, +} + +impl OwnedObjectAddress { + pub(crate) fn accessible(bus_name: OwnedUniqueName, id: ObjectId) -> Self { + Self { + bus_name, + path: ObjectPath::from_string_unchecked(format!( + "{}{}", + ACCESSIBLE_PATH_PREFIX, + id.as_str() + )) + .into(), + } + } + + pub(crate) fn null(bus_name: OwnedUniqueName) -> Self { + Self { + bus_name, + path: ObjectPath::from_str_unchecked(NULL_PATH).into(), + } + } + + pub(crate) fn root(bus_name: OwnedUniqueName) -> Self { + Self { + bus_name, + path: ObjectPath::from_str_unchecked(ROOT_PATH).into(), + } + } +} + +impl From<(String, OwnedObjectPath)> for OwnedObjectAddress { + fn from(value: (String, OwnedObjectPath)) -> Self { + Self { + bus_name: OwnedUniqueName::from(UniqueName::from_string_unchecked(value.0)), + path: value.1, + } + } +} diff --git a/platforms/unix/src/atspi/object_id.rs b/platforms/unix/src/atspi/object_id.rs new file mode 100644 index 000000000..f85b819de --- /dev/null +++ b/platforms/unix/src/atspi/object_id.rs @@ -0,0 +1,27 @@ +// Copyright 2022 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 accesskit::NodeId; +use serde::{Deserialize, Serialize}; +use zbus::zvariant::{Str, Type, Value}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Type, Value)] +pub(crate) struct ObjectId<'a>(#[serde(borrow)] Str<'a>); + +impl<'a> ObjectId<'a> { + pub(crate) fn root() -> ObjectId<'static> { + ObjectId(Str::from("root")) + } + + pub(crate) fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl From for ObjectId<'static> { + fn from(value: NodeId) -> Self { + Self(Str::from(value.0.to_string())) + } +} diff --git a/platforms/unix/src/atspi/object_ref.rs b/platforms/unix/src/atspi/object_ref.rs new file mode 100644 index 000000000..f7be5f64e --- /dev/null +++ b/platforms/unix/src/atspi/object_ref.rs @@ -0,0 +1,42 @@ +// Copyright 2022 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 crate::atspi::{ObjectId, OwnedObjectAddress}; +use accesskit::NodeId; +use zbus::{names::OwnedUniqueName, zvariant::Value}; + +#[derive(Debug, PartialEq)] +pub(crate) enum ObjectRef { + Managed(ObjectId<'static>), + Unmanaged(OwnedObjectAddress), +} + +impl<'a> ObjectRef { + pub(crate) fn into_value(self, name: OwnedUniqueName) -> Value<'a> { + match self { + Self::Managed(object) => OwnedObjectAddress::accessible(name, object), + Self::Unmanaged(object) => object, + } + .into() + } +} + +impl From for ObjectRef { + fn from(value: NodeId) -> ObjectRef { + ObjectRef::Managed(value.into()) + } +} + +impl From> for ObjectRef { + fn from(value: ObjectId<'static>) -> ObjectRef { + ObjectRef::Managed(value) + } +} + +impl From for ObjectRef { + fn from(value: OwnedObjectAddress) -> ObjectRef { + ObjectRef::Unmanaged(value) + } +} diff --git a/platforms/unix/src/lib.rs b/platforms/unix/src/lib.rs new file mode 100644 index 000000000..63cb61bf2 --- /dev/null +++ b/platforms/unix/src/lib.rs @@ -0,0 +1,15 @@ +// Copyright 2022 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. + +#[macro_use] +extern crate zbus; + +mod adapter; +mod atspi; +mod node; +mod util; + +pub use adapter::Adapter; +pub(crate) use node::{unknown_object, PlatformNode, PlatformRootNode}; diff --git a/platforms/unix/src/node.rs b/platforms/unix/src/node.rs new file mode 100644 index 000000000..3baad926b --- /dev/null +++ b/platforms/unix/src/node.rs @@ -0,0 +1,919 @@ +// Copyright 2022 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 Chromium's accessibility abstraction. +// Copyright 2017 The Chromium 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 crate::{ + atspi::{ + interfaces::{Action, Event, ObjectEvent, Property}, + ObjectId, ObjectRef, Rect as AtspiRect, ACCESSIBLE_PATH_PREFIX, + }, + util::{AppContext, WindowBounds}, +}; +use accesskit::{ + kurbo::{Affine, Point, Rect}, + CheckedState, DefaultActionVerb, NodeId, Role, +}; +use accesskit_consumer::{DetachedNode, FilterResult, Node, NodeState, Tree, TreeState}; +use async_channel::Sender; +use atspi::{ + accessible::Role as AtspiRole, component::Layer, CoordType, Interface, InterfaceSet, State, + StateSet, +}; +use parking_lot::RwLock; +use std::{ + iter::FusedIterator, + sync::{Arc, Weak}, +}; +use zbus::fdo; + +fn filter_common(node: &NodeState) -> FilterResult { + if node.is_hidden() { + return FilterResult::ExcludeSubtree; + } + + let role = node.role(); + if role == Role::Presentation || role == Role::GenericContainer || role == Role::InlineTextBox { + return FilterResult::ExcludeNode; + } + + FilterResult::Include +} + +pub(crate) fn filter(node: &Node) -> FilterResult { + if node.is_focused() { + return FilterResult::Include; + } + + filter_common(node.state()) +} + +pub(crate) fn filter_detached(node: &DetachedNode) -> FilterResult { + if node.is_focused() { + return FilterResult::Include; + } + + filter_common(node.state()) +} + +pub(crate) enum NodeWrapper<'a> { + Node(&'a Node<'a>), + DetachedNode(&'a DetachedNode), +} + +impl<'a> NodeWrapper<'a> { + fn node_state(&self) -> &NodeState { + match self { + Self::Node(node) => node.state(), + Self::DetachedNode(node) => node.state(), + } + } + + pub fn name(&self) -> String { + match self { + Self::Node(node) => node.name(), + Self::DetachedNode(node) => node.name(), + } + .unwrap_or_default() + } + + pub fn description(&self) -> String { + String::new() + } + + pub fn parent_id(&self) -> Option { + self.node_state().parent_id() + } + + pub fn filtered_parent(&self) -> Option { + match self { + Self::Node(node) => node + .filtered_parent(&filter) + .map(|parent| parent.id().into()), + _ => unreachable!(), + } + } + + pub fn id(&self) -> ObjectId<'static> { + self.node_state().id().into() + } + + fn child_ids( + &self, + ) -> impl DoubleEndedIterator + + ExactSizeIterator + + FusedIterator + + '_ { + self.node_state().child_ids() + } + + fn filtered_child_ids( + &self, + ) -> impl DoubleEndedIterator + FusedIterator + '_ { + match self { + Self::Node(node) => node.filtered_children(&filter).map(|child| child.id()), + Self::DetachedNode(_) => unreachable!(), + } + } + + pub fn role(&self) -> AtspiRole { + match self.node_state().role() { + Role::Alert => AtspiRole::Notification, + Role::AlertDialog => AtspiRole::Alert, + Role::Comment | Role::Suggestion => AtspiRole::Section, + // TODO: See how to represent ARIA role="application" + Role::Application => AtspiRole::Embedded, + Role::Article => AtspiRole::Article, + Role::Audio => AtspiRole::Audio, + Role::Banner | Role::Header => AtspiRole::Landmark, + Role::Blockquote => AtspiRole::BlockQuote, + Role::Caret => AtspiRole::Unknown, + Role::Button => AtspiRole::PushButton, + Role::Canvas => AtspiRole::Canvas, + Role::Caption => AtspiRole::Caption, + Role::Cell => AtspiRole::TableCell, + Role::CheckBox => AtspiRole::CheckBox, + Role::Switch => AtspiRole::ToggleButton, + Role::ColorWell => AtspiRole::PushButton, + Role::Column => AtspiRole::Unknown, + Role::ColumnHeader => AtspiRole::ColumnHeader, + Role::ComboBoxGrouping | Role::ComboBoxMenuButton => AtspiRole::ComboBox, + Role::Complementary => AtspiRole::Landmark, + Role::ContentDeletion => AtspiRole::ContentDeletion, + Role::ContentInsertion => AtspiRole::ContentInsertion, + Role::ContentInfo | Role::Footer => AtspiRole::Landmark, + Role::Date | Role::DateTime => AtspiRole::DateEditor, + Role::Definition | Role::DescriptionListDetail => AtspiRole::DescriptionValue, + Role::DescriptionList => AtspiRole::DescriptionList, + Role::DescriptionListTerm => AtspiRole::DescriptionTerm, + Role::Details => AtspiRole::Panel, + Role::Dialog => AtspiRole::Dialog, + Role::Directory => AtspiRole::List, + Role::DisclosureTriangle => AtspiRole::ToggleButton, + Role::DocCover => AtspiRole::Image, + Role::DocBackLink | Role::DocBiblioRef | Role::DocGlossRef | Role::DocNoteRef => { + AtspiRole::Link + } + Role::DocBiblioEntry | Role::DocEndnote => AtspiRole::ListItem, + Role::DocNotice | Role::DocTip => AtspiRole::Comment, + Role::DocFootnote => AtspiRole::Footnote, + Role::DocPageBreak => AtspiRole::Separator, + Role::DocPageFooter => AtspiRole::Footer, + Role::DocPageHeader => AtspiRole::Header, + Role::DocAcknowledgements + | Role::DocAfterword + | Role::DocAppendix + | Role::DocBibliography + | Role::DocChapter + | Role::DocConclusion + | Role::DocCredits + | Role::DocEndnotes + | Role::DocEpilogue + | Role::DocErrata + | Role::DocForeword + | Role::DocGlossary + | Role::DocIndex + | Role::DocIntroduction + | Role::DocPageList + | Role::DocPart + | Role::DocPreface + | Role::DocPrologue + | Role::DocToc => AtspiRole::Landmark, + Role::DocAbstract + | Role::DocColophon + | Role::DocCredit + | Role::DocDedication + | Role::DocEpigraph + | Role::DocExample + | Role::DocPullquote + | Role::DocQna => AtspiRole::Section, + Role::DocSubtitle => AtspiRole::Heading, + Role::Document => AtspiRole::DocumentFrame, + Role::EmbeddedObject => AtspiRole::Embedded, + // TODO: Forms which lack an accessible name are no longer + // exposed as forms. Forms which have accessible + // names should be exposed as `AtspiRole::Landmark` according to Core AAM. + Role::Form => AtspiRole::Form, + Role::Figure | Role::Feed => AtspiRole::Panel, + Role::GenericContainer + | Role::FooterAsNonLandmark + | Role::HeaderAsNonLandmark + | Role::Ruby => AtspiRole::Section, + Role::GraphicsDocument => AtspiRole::DocumentFrame, + Role::GraphicsObject => AtspiRole::Panel, + Role::GraphicsSymbol => AtspiRole::Image, + Role::Grid => AtspiRole::Table, + Role::Group => AtspiRole::Panel, + Role::Heading => AtspiRole::Heading, + Role::Iframe | Role::IframePresentational => AtspiRole::InternalFrame, + // TODO: If there are unignored children, then it should be AtspiRole::ImageMap. + Role::Image => AtspiRole::Image, + Role::InlineTextBox => AtspiRole::Static, + Role::InputTime => AtspiRole::DateEditor, + Role::LabelText | Role::Legend => AtspiRole::Label, + // Layout table objects are treated the same as `Role::GenericContainer`. + Role::LayoutTable => AtspiRole::Section, + Role::LayoutTableCell => AtspiRole::Section, + Role::LayoutTableRow => AtspiRole::Section, + // TODO: Having a separate accessible object for line breaks + // is inconsistent with other implementations. + Role::LineBreak => AtspiRole::Static, + Role::Link => AtspiRole::Link, + Role::List => AtspiRole::List, + Role::ListBox => AtspiRole::ListBox, + // TODO: Use `AtspiRole::MenuItem' inside a combo box. + Role::ListBoxOption => AtspiRole::ListItem, + Role::ListGrid => AtspiRole::Table, + Role::ListItem => AtspiRole::ListItem, + // Regular list markers only expose their alternative text, but do not + // expose their descendants, and the descendants should be ignored. This + // is because the alternative text depends on the counter style and can + // be different from the actual (visual) marker text, and hence, + // inconsistent with the descendants. We treat a list marker as non-text + // only if it still has non-ignored descendants, which happens only when => + // - The list marker itself is ignored but the descendants are not + // - Or the list marker contains images + // TODO: How to check for unignored children when the node is detached? + Role::ListMarker => AtspiRole::Static, + Role::Log => AtspiRole::Log, + Role::Main => AtspiRole::Landmark, + Role::Mark => AtspiRole::Static, + Role::Math => AtspiRole::Math, + Role::Marquee => AtspiRole::Marquee, + Role::Menu | Role::MenuListPopup => AtspiRole::Menu, + Role::MenuBar => AtspiRole::MenuBar, + Role::MenuItem | Role::MenuListOption => AtspiRole::MenuItem, + Role::MenuItemCheckBox => AtspiRole::CheckMenuItem, + Role::MenuItemRadio => AtspiRole::RadioMenuItem, + Role::Meter => AtspiRole::LevelBar, + Role::Navigation => AtspiRole::Landmark, + Role::Note => AtspiRole::Comment, + Role::Pane | Role::ScrollView => AtspiRole::Panel, + Role::Paragraph => AtspiRole::Paragraph, + Role::PdfActionableHighlight => AtspiRole::PushButton, + Role::PdfRoot => AtspiRole::DocumentFrame, + Role::PluginObject => AtspiRole::Embedded, + Role::PopupButton => AtspiRole::PushButton, + Role::Portal => AtspiRole::PushButton, + Role::Pre => AtspiRole::Section, + Role::ProgressIndicator => AtspiRole::ProgressBar, + Role::RadioButton => AtspiRole::RadioButton, + Role::RadioGroup => AtspiRole::Panel, + Role::Region => AtspiRole::Landmark, + Role::RootWebArea => AtspiRole::DocumentWeb, + Role::Row => AtspiRole::TableRow, + Role::RowGroup => AtspiRole::Panel, + Role::RowHeader => AtspiRole::RowHeader, + // TODO: Generally exposed as description on (`Role::Ruby`) element, not + // as its own object in the tree. + // However, it's possible to make a `Role::RubyAnnotation` element show up in the + // tree, for example by adding tabindex="0" to the source or + // element or making the source element the target of an aria-owns. + // Therefore, we need to gracefully handle it if it actually + // shows up in the tree. + Role::RubyAnnotation => AtspiRole::Static, + Role::Section => AtspiRole::Section, + Role::ScrollBar => AtspiRole::ScrollBar, + Role::Search => AtspiRole::Landmark, + Role::Slider => AtspiRole::Slider, + Role::SpinButton => AtspiRole::SpinButton, + Role::Splitter => AtspiRole::Separator, + Role::StaticText => AtspiRole::Static, + Role::Status => AtspiRole::StatusBar, + Role::SvgRoot => AtspiRole::DocumentFrame, + Role::Tab => AtspiRole::PageTab, + Role::Table => AtspiRole::Table, + // TODO: This mapping is correct, but it doesn't seem to be + // used. We don't necessarily want to always expose these containers, but + // we must do so if they are focusable. + Role::TableHeaderContainer => AtspiRole::Panel, + Role::TabList => AtspiRole::PageTabList, + Role::TabPanel => AtspiRole::ScrollPane, + // TODO: This mapping should also be applied to the dfn + // element. + Role::Term => AtspiRole::DescriptionTerm, + Role::TitleBar => AtspiRole::TitleBar, + Role::TextField | Role::SearchBox => { + if self.node_state().is_protected() { + AtspiRole::PasswordText + } else { + AtspiRole::Entry + } + } + Role::TextFieldWithComboBox => AtspiRole::ComboBox, + Role::Abbr | Role::Code | Role::Emphasis | Role::Strong | Role::Time => { + AtspiRole::Static + } + Role::Timer => AtspiRole::Timer, + Role::ToggleButton => AtspiRole::ToggleButton, + Role::Toolbar => AtspiRole::ToolBar, + Role::Tooltip => AtspiRole::ToolTip, + Role::Tree => AtspiRole::Tree, + Role::TreeItem => AtspiRole::TreeItem, + Role::TreeGrid => AtspiRole::TreeTable, + Role::Video => AtspiRole::Video, + // In AT-SPI, elements with `AtspiRole::Frame` are windows with titles and + // buttons, while those with `AtspiRole::Window` are windows without those + // elements. + Role::Window => AtspiRole::Frame, + Role::Client | Role::WebView => AtspiRole::Panel, + Role::FigureCaption => AtspiRole::Caption, + // TODO: Are there special cases to consider? + Role::Presentation | Role::Unknown => AtspiRole::Unknown, + Role::ImeCandidate | Role::Keyboard => AtspiRole::RedundantObject, + } + } + + fn is_focused(&self) -> bool { + match self { + Self::Node(node) => node.is_focused(), + Self::DetachedNode(node) => node.is_focused(), + } + } + + pub fn state(&self) -> StateSet { + let state = self.node_state(); + let atspi_role = self.role(); + let mut atspi_state = StateSet::empty(); + if state.role() == Role::Window && state.parent_id().is_none() { + atspi_state.insert(State::Active); + } + // TODO: Focus and selection. + if state.is_focusable() { + atspi_state.insert(State::Focusable); + } + let filter_result = match self { + Self::Node(node) => filter(node), + Self::DetachedNode(node) => filter_detached(node), + }; + if filter_result == FilterResult::Include { + atspi_state.insert(State::Visible | State::Showing); + } + if atspi_role != AtspiRole::ToggleButton && state.checked_state().is_some() { + atspi_state.insert(State::Checkable); + } + if let Some(selected) = state.is_selected() { + if !state.is_disabled() { + atspi_state.insert(State::Selectable); + } + if selected { + atspi_state.insert(State::Selected); + } + } + if state.is_text_field() { + atspi_state.insert(State::SelectableText); + atspi_state.insert(match state.is_multiline() { + true => State::MultiLine, + false => State::SingleLine, + }); + } + + // Special case for indeterminate progressbar. + if state.role() == Role::ProgressIndicator && state.numeric_value().is_none() { + atspi_state.insert(State::Indeterminate); + } + + // Checked state + match state.checked_state() { + Some(CheckedState::Mixed) => atspi_state.insert(State::Indeterminate), + Some(CheckedState::True) if atspi_role == AtspiRole::ToggleButton => { + atspi_state.insert(State::Pressed) + } + Some(CheckedState::True) => atspi_state.insert(State::Checked), + _ => {} + } + + if state.is_read_only_supported() && state.is_read_only_or_disabled() { + atspi_state.insert(State::ReadOnly); + } else { + atspi_state.insert(State::Enabled | State::Sensitive); + } + + if self.is_focused() { + atspi_state.insert(State::Focused); + } + + atspi_state + } + + fn is_root(&self) -> bool { + match self { + Self::Node(node) => node.is_root(), + Self::DetachedNode(node) => node.is_root(), + } + } + + pub fn interfaces(&self) -> InterfaceSet { + let state = self.node_state(); + let mut interfaces = InterfaceSet::new(Interface::Accessible); + if state.default_action_verb().is_some() { + interfaces.insert(Interface::Action); + } + if state.raw_bounds().is_some() || self.is_root() { + interfaces.insert(Interface::Component); + } + if self.current_value().is_some() { + interfaces.insert(Interface::Value); + } + interfaces + } + + fn n_actions(&self) -> i32 { + match self.node_state().default_action_verb() { + Some(_) => 1, + None => 0, + } + } + + fn get_action_name(&self, index: i32) -> String { + if index != 0 { + return String::new(); + } + String::from(match self.node_state().default_action_verb() { + Some(DefaultActionVerb::Click) => "click", + Some(DefaultActionVerb::Focus) => "focus", + Some(DefaultActionVerb::Check) => "check", + Some(DefaultActionVerb::Uncheck) => "uncheck", + Some(DefaultActionVerb::ClickAncestor) => "clickAncestor", + Some(DefaultActionVerb::Jump) => "jump", + Some(DefaultActionVerb::Open) => "open", + Some(DefaultActionVerb::Press) => "press", + Some(DefaultActionVerb::Select) => "select", + None => "", + }) + } + + fn raw_bounds_and_transform(&self) -> (Option, Affine) { + let state = self.node_state(); + (state.raw_bounds(), state.direct_transform()) + } + + fn extents(&self, window_bounds: &WindowBounds) -> AtspiRect { + if self.is_root() { + return window_bounds.outer.into(); + } + match self { + Self::Node(node) => node.bounding_box().map_or_else( + || AtspiRect::INVALID, + |bounds| { + let window_top_left = window_bounds.inner.origin(); + let node_origin = bounds.origin(); + let new_origin = Point::new( + window_top_left.x + node_origin.x, + window_top_left.y + node_origin.y, + ); + bounds.with_origin(new_origin).into() + }, + ), + _ => unreachable!(), + } + } + + fn current_value(&self) -> Option { + self.node_state().numeric_value() + } + + pub fn notify_changes( + &self, + window_bounds: &WindowBounds, + events: &Sender, + old: &NodeWrapper, + ) { + self.notify_state_changes(events, old); + self.notify_property_changes(events, old); + self.notify_bounds_changes(window_bounds, events, old); + self.notify_children_changes(events, old); + } + + fn notify_state_changes(&self, events: &Sender, old: &NodeWrapper) { + let old_state = old.state(); + let new_state = self.state(); + let changed_states = old_state ^ new_state; + for state in changed_states.iter() { + events + .send_blocking(Event::Object { + target: self.id(), + event: ObjectEvent::StateChanged(state, new_state.contains(state)), + }) + .unwrap(); + } + } + + fn notify_property_changes(&self, events: &Sender, old: &NodeWrapper) { + let name = self.name(); + if name != old.name() { + events + .send_blocking(Event::Object { + target: self.id(), + event: ObjectEvent::PropertyChanged(Property::Name(name)), + }) + .unwrap(); + } + let description = self.description(); + if description != old.description() { + events + .send_blocking(Event::Object { + target: self.id(), + event: ObjectEvent::PropertyChanged(Property::Description(description)), + }) + .unwrap(); + } + let parent_id = self.parent_id(); + if parent_id != old.parent_id() { + events + .send_blocking(Event::Object { + target: self.id(), + event: ObjectEvent::PropertyChanged(Property::Parent(self.filtered_parent())), + }) + .unwrap(); + } + let role = self.role(); + if role != old.role() { + events + .send_blocking(Event::Object { + target: self.id(), + event: ObjectEvent::PropertyChanged(Property::Role(role)), + }) + .unwrap(); + } + if let Some(value) = self.current_value() { + if Some(value) != old.current_value() { + events + .send_blocking(Event::Object { + target: self.id(), + event: ObjectEvent::PropertyChanged(Property::Value(value)), + }) + .unwrap(); + } + } + } + + fn notify_bounds_changes( + &self, + window_bounds: &WindowBounds, + events: &Sender, + old: &NodeWrapper, + ) { + if self.raw_bounds_and_transform() != old.raw_bounds_and_transform() { + events + .send_blocking(Event::Object { + target: self.id(), + event: ObjectEvent::BoundsChanged(self.extents(window_bounds)), + }) + .unwrap(); + } + } + + fn notify_children_changes(&self, events: &Sender, old: &NodeWrapper) { + let old_children = old.child_ids().collect::>(); + let filtered_children = self.filtered_child_ids().collect::>(); + for (index, child) in filtered_children.iter().enumerate() { + if !old_children.contains(child) { + events + .send_blocking(Event::Object { + target: self.id(), + event: ObjectEvent::ChildAdded(index, ObjectRef::from(*child)), + }) + .unwrap(); + } + } + for child in old_children.into_iter() { + if !filtered_children.contains(&child) { + events + .send_blocking(Event::Object { + target: self.id(), + event: ObjectEvent::ChildRemoved(child.into()), + }) + .unwrap(); + } + } + } +} + +pub(crate) fn unknown_object(id: &ObjectId) -> fdo::Error { + fdo::Error::UnknownObject(format!("{}{}", ACCESSIBLE_PATH_PREFIX, id.as_str())) +} + +#[derive(Clone)] +pub(crate) struct PlatformNode { + tree: Weak, + node_id: NodeId, +} + +impl PlatformNode { + pub(crate) fn new(tree: &Arc, node_id: NodeId) -> Self { + Self { + tree: Arc::downgrade(tree), + node_id, + } + } + + fn upgrade_tree(&self) -> fdo::Result> { + if let Some(tree) = self.tree.upgrade() { + Ok(tree) + } else { + Err(unknown_object(&self.accessible_id())) + } + } + + fn with_tree_state(&self, f: F) -> fdo::Result + where + F: FnOnce(&TreeState) -> fdo::Result, + { + let tree = self.upgrade_tree()?; + let state = tree.read(); + f(&state) + } + + fn resolve(&self, f: F) -> fdo::Result + where + for<'a> F: FnOnce(Node<'a>) -> fdo::Result, + { + self.with_tree_state(|state| { + if let Some(node) = state.node_by_id(self.node_id) { + f(node) + } else { + Err(unknown_object(&self.accessible_id())) + } + }) + } + + fn validate_for_action(&self) -> fdo::Result> { + let tree = self.upgrade_tree()?; + let state = tree.read(); + if state.has_node(self.node_id) { + drop(state); + Ok(tree) + } else { + Err(unknown_object(&self.accessible_id())) + } + } + + pub fn name(&self) -> fdo::Result { + self.resolve(|node| { + let wrapper = NodeWrapper::Node(&node); + Ok(wrapper.name()) + }) + } + + pub fn description(&self) -> fdo::Result { + self.resolve(|node| { + let wrapper = NodeWrapper::Node(&node); + Ok(wrapper.description()) + }) + } + + pub fn parent(&self) -> fdo::Result { + self.resolve(|node| { + Ok(node + .filtered_parent(&filter) + .map(|parent| parent.id().into()) + .unwrap_or_else(|| ObjectRef::Managed(ObjectId::root()))) + }) + } + + pub fn child_count(&self) -> fdo::Result { + self.resolve(|node| { + i32::try_from(node.filtered_children(&filter).count()) + .map_err(|_| fdo::Error::Failed("Too many children.".into())) + }) + } + + pub fn accessible_id(&self) -> ObjectId<'static> { + self.node_id.into() + } + + pub fn child_at_index(&self, index: usize) -> fdo::Result> { + self.resolve(|node| { + let child = node + .filtered_children(&filter) + .nth(index) + .map(|child| child.id().into()); + Ok(child) + }) + } + + pub fn children(&self) -> fdo::Result> { + self.resolve(|node| { + let children = node + .filtered_children(&filter) + .map(|child| child.id().into()) + .collect(); + Ok(children) + }) + } + + pub fn index_in_parent(&self) -> fdo::Result { + self.resolve(|node| { + i32::try_from(node.preceding_filtered_siblings(&filter).count()) + .map_err(|_| fdo::Error::Failed("Index is too big.".into())) + }) + } + + pub fn role(&self) -> fdo::Result { + self.resolve(|node| { + let wrapper = NodeWrapper::Node(&node); + Ok(wrapper.role()) + }) + } + + pub fn state(&self) -> fdo::Result { + self.resolve(|node| { + let wrapper = NodeWrapper::Node(&node); + Ok(wrapper.state()) + }) + } + + pub fn interfaces(&self) -> fdo::Result { + self.resolve(|node| { + let wrapper = NodeWrapper::Node(&node); + Ok(wrapper.interfaces()) + }) + } + + pub fn n_actions(&self) -> fdo::Result { + self.resolve(|node| { + let wrapper = NodeWrapper::Node(&node); + Ok(wrapper.n_actions()) + }) + } + + pub fn get_action_name(&self, index: i32) -> fdo::Result { + self.resolve(|node| { + let wrapper = NodeWrapper::Node(&node); + Ok(wrapper.get_action_name(index)) + }) + } + + pub fn get_actions(&self) -> fdo::Result> { + self.resolve(|node| { + let wrapper = NodeWrapper::Node(&node); + let n_actions = wrapper.n_actions() as usize; + let mut actions = Vec::with_capacity(n_actions); + for i in 0..n_actions { + actions.push(Action { + localized_name: wrapper.get_action_name(i as i32), + description: "".into(), + key_binding: "".into(), + }); + } + Ok(actions) + }) + } + + pub fn do_action(&self, index: i32) -> fdo::Result { + if index != 0 { + return Ok(false); + } + let tree = self.validate_for_action()?; + tree.do_default_action(self.node_id); + Ok(true) + } + + pub fn contains( + &self, + window_bounds: &WindowBounds, + x: i32, + y: i32, + coord_type: CoordType, + ) -> fdo::Result { + self.resolve(|node| { + let bounds = match node.bounding_box() { + Some(node_bounds) => { + let top_left = window_bounds.top_left(coord_type, node.is_root()); + let new_origin = + Point::new(top_left.x + node_bounds.x0, top_left.y + node_bounds.y0); + node_bounds.with_origin(new_origin) + } + None if node.is_root() => { + let bounds = window_bounds.outer; + match coord_type { + CoordType::Screen => bounds, + CoordType::Window => bounds.with_origin(Point::ZERO), + _ => unimplemented!(), + } + } + _ => return Err(unknown_object(&self.accessible_id())), + }; + Ok(bounds.contains(Point::new(x.into(), y.into()))) + }) + } + + pub fn get_accessible_at_point( + &self, + window_bounds: &WindowBounds, + x: i32, + y: i32, + coord_type: CoordType, + ) -> fdo::Result> { + self.resolve(|node| { + let top_left = window_bounds.top_left(coord_type, node.is_root()); + let point = Point::new(f64::from(x) - top_left.x, f64::from(y) - top_left.y); + let point = node.transform().inverse() * point; + Ok(node + .node_at_point(point, &filter) + .map(|node| ObjectRef::Managed(NodeWrapper::Node(&node).id()))) + }) + } + + pub fn get_extents( + &self, + window_bounds: &WindowBounds, + coord_type: CoordType, + ) -> fdo::Result<(AtspiRect,)> { + self.resolve(|node| match node.bounding_box() { + Some(node_bounds) => { + let top_left = window_bounds.top_left(coord_type, node.is_root()); + let new_origin = + Point::new(top_left.x + node_bounds.x0, top_left.y + node_bounds.y0); + Ok((node_bounds.with_origin(new_origin).into(),)) + } + None if node.is_root() => { + let bounds = window_bounds.outer; + Ok((match coord_type { + CoordType::Screen => bounds.into(), + CoordType::Window => bounds.with_origin(Point::ZERO).into(), + _ => unimplemented!(), + },)) + } + _ => Err(unknown_object(&self.accessible_id())), + }) + } + + pub fn get_layer(&self) -> fdo::Result { + self.resolve(|node| { + let wrapper = NodeWrapper::Node(&node); + if wrapper.role() == AtspiRole::Window { + Ok(Layer::Window) + } else { + Ok(Layer::Widget) + } + }) + } + + pub fn grab_focus(&self) -> fdo::Result { + let tree = self.validate_for_action()?; + tree.set_focus(self.node_id); + Ok(true) + } + + pub fn scroll_to_point( + &self, + window_bounds: &WindowBounds, + coord_type: CoordType, + x: i32, + y: i32, + ) -> fdo::Result { + let tree = self.validate_for_action()?; + let is_root = self.node_id == tree.read().root_id(); + let top_left = window_bounds.top_left(coord_type, is_root); + let point = Point::new(f64::from(x) - top_left.x, f64::from(y) - top_left.y); + tree.scroll_to_point(self.node_id, point); + Ok(true) + } + + pub fn minimum_value(&self) -> fdo::Result { + self.resolve(|node| Ok(node.state().min_numeric_value().unwrap_or(std::f64::MIN))) + } + + pub fn maximum_value(&self) -> fdo::Result { + self.resolve(|node| Ok(node.state().max_numeric_value().unwrap_or(std::f64::MAX))) + } + + pub fn minimum_increment(&self) -> fdo::Result { + self.resolve(|node| Ok(node.state().numeric_value_step().unwrap_or(0.0))) + } + + pub fn current_value(&self) -> fdo::Result { + self.resolve(|node| { + let wrapper = NodeWrapper::Node(&node); + Ok(wrapper.current_value().unwrap_or(0.0)) + }) + } + + pub fn set_current_value(&self, value: f64) -> fdo::Result<()> { + let tree = self.validate_for_action()?; + tree.set_numeric_value(self.node_id, value); + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct PlatformRootNode { + pub(crate) context: Weak>, + pub(crate) tree: Weak, +} + +impl PlatformRootNode { + pub fn new(context: &Arc>, tree: &Arc) -> Self { + Self { + context: Arc::downgrade(context), + tree: Arc::downgrade(tree), + } + } +} diff --git a/platforms/unix/src/util.rs b/platforms/unix/src/util.rs new file mode 100644 index 000000000..35c0fed5b --- /dev/null +++ b/platforms/unix/src/util.rs @@ -0,0 +1,53 @@ +// Copyright 2022 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 crate::atspi::OwnedObjectAddress; +use accesskit::kurbo::{Point, Rect}; +use atspi::CoordType; + +pub(crate) struct AppContext { + pub(crate) name: String, + pub(crate) toolkit_name: String, + pub(crate) toolkit_version: String, + pub(crate) id: Option, + pub(crate) desktop_address: Option, +} + +impl AppContext { + pub(crate) fn new(name: String, toolkit_name: String, toolkit_version: String) -> Self { + Self { + name, + toolkit_name, + toolkit_version, + id: None, + desktop_address: None, + } + } +} + +#[derive(Default)] +pub(crate) struct WindowBounds { + pub(crate) outer: Rect, + pub(crate) inner: Rect, +} + +impl WindowBounds { + pub(crate) fn top_left(&self, coord_type: CoordType, is_root: bool) -> Point { + match coord_type { + CoordType::Screen if is_root => self.outer.origin(), + CoordType::Screen => self.inner.origin(), + CoordType::Window if is_root => Point::ZERO, + CoordType::Window => { + let outer_position = self.outer.origin(); + let inner_position = self.inner.origin(); + Point::new( + inner_position.x - outer_position.x, + inner_position.y - outer_position.y, + ) + } + _ => unimplemented!(), + } + } +} diff --git a/platforms/winit/Cargo.toml b/platforms/winit/Cargo.toml index a643d93f4..91734b7d5 100644 --- a/platforms/winit/Cargo.toml +++ b/platforms/winit/Cargo.toml @@ -21,5 +21,8 @@ accesskit_windows = { version = "0.10.2", path = "../windows" } [target.'cfg(target_os = "macos")'.dependencies] accesskit_macos = { version = "0.4.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.1.0", path = "../unix" } + [dev-dependencies] winit = "0.27.2" diff --git a/platforms/winit/examples/simple.rs b/platforms/winit/examples/simple.rs index 747525d01..d5070dd16 100644 --- a/platforms/winit/examples/simple.rs +++ b/platforms/winit/examples/simple.rs @@ -137,6 +137,14 @@ fn main() { println!("- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed."); #[cfg(target_os = "windows")] println!("Enable Narrator with [Win]+[Ctrl]+[Enter] (or [Win]+[Enter] on older versions of Windows)."); + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + println!("Enable Orca with [Super]+[Alt]+[S]."); let event_loop = EventLoopBuilder::with_user_event().build(); @@ -166,7 +174,7 @@ fn main() { *control_flow = ControlFlow::Wait; match event { - Event::WindowEvent { event, .. } => match event { + Event::WindowEvent { event, .. } if adapter.on_event(&window, &event) => match event { WindowEvent::CloseRequested => { *control_flow = ControlFlow::ExitWithCode(0); } diff --git a/platforms/winit/src/lib.rs b/platforms/winit/src/lib.rs index eb54ff1d8..2fdcb80b7 100644 --- a/platforms/winit/src/lib.rs +++ b/platforms/winit/src/lib.rs @@ -5,6 +5,7 @@ use accesskit::{ActionHandler, ActionRequest, TreeUpdate}; use parking_lot::Mutex; use winit::{ + event::WindowEvent, event_loop::EventLoopProxy, window::{Window, WindowId}, }; @@ -63,6 +64,66 @@ impl Adapter { Self { adapter } } + #[cfg(all( + not(target_os = "linux"), + not(target_os = "dragonfly"), + not(target_os = "freebsd"), + not(target_os = "netbsd"), + not(target_os = "openbsd") + ))] + #[must_use] + pub fn on_event(&self, _window: &Window, _event: &WindowEvent) -> bool { + true + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[must_use] + pub fn on_event(&self, window: &Window, event: &WindowEvent) -> bool { + use accesskit::kurbo::Rect; + + match event { + WindowEvent::Moved(outer_position) => { + let outer_position: (_, _) = outer_position.cast::().into(); + let outer_size: (_, _) = window.outer_size().cast::().into(); + let inner_position: (_, _) = window + .inner_position() + .unwrap_or_default() + .cast::() + .into(); + let inner_size: (_, _) = window.inner_size().cast::().into(); + self.adapter.set_root_window_bounds( + Rect::from_origin_size(outer_position, outer_size), + Rect::from_origin_size(inner_position, inner_size), + ) + } + WindowEvent::Resized(outer_size) => { + let outer_position: (_, _) = window + .outer_position() + .unwrap_or_default() + .cast::() + .into(); + let outer_size: (_, _) = outer_size.cast::().into(); + let inner_position: (_, _) = window + .inner_position() + .unwrap_or_default() + .cast::() + .into(); + let inner_size: (_, _) = window.inner_size().cast::().into(); + self.adapter.set_root_window_bounds( + Rect::from_origin_size(outer_position, outer_size), + Rect::from_origin_size(inner_position, inner_size), + ) + } + _ => (), + } + true + } + pub fn update(&self, update: TreeUpdate) { self.adapter.update(update) } diff --git a/platforms/winit/src/platform_impl/mod.rs b/platforms/winit/src/platform_impl/mod.rs index 56e0040ce..1d0956c36 100644 --- a/platforms/winit/src/platform_impl/mod.rs +++ b/platforms/winit/src/platform_impl/mod.rs @@ -14,6 +14,24 @@ mod platform; #[path = "macos.rs"] mod platform; -#[cfg(all(not(target_os = "windows"), not(target_os = "macos"),))] +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +#[path = "unix.rs"] +mod platform; + +#[cfg(all( + not(target_os = "windows"), + not(target_os = "macos"), + not(target_os = "linux"), + not(target_os = "dragonfly"), + not(target_os = "freebsd"), + not(target_os = "netbsd"), + not(target_os = "openbsd") +))] #[path = "null.rs"] mod platform; diff --git a/platforms/winit/src/platform_impl/unix.rs b/platforms/winit/src/platform_impl/unix.rs new file mode 100644 index 000000000..3a0d1c0d8 --- /dev/null +++ b/platforms/winit/src/platform_impl/unix.rs @@ -0,0 +1,46 @@ +// Copyright 2022 The AccessKit Authors. All rights reserved. +// Licensed under the Apache License, Version 2.0 (found in +// the LICENSE-APACHE file). + +use accesskit::{kurbo::Rect, ActionHandler, TreeUpdate}; +use accesskit_unix::Adapter as UnixAdapter; +use winit::window::Window; + +pub struct Adapter { + adapter: Option, +} + +impl Adapter { + pub fn new( + _: &Window, + source: impl 'static + FnOnce() -> TreeUpdate, + action_handler: Box, + ) -> Self { + let adapter = UnixAdapter::new( + String::new(), + String::new(), + String::new(), + source, + action_handler, + ); + Self { adapter } + } + + pub fn set_root_window_bounds(&self, outer: Rect, inner: Rect) { + if let Some(adapter) = &self.adapter { + adapter.set_root_window_bounds(outer, inner); + } + } + + pub fn update(&self, update: TreeUpdate) { + if let Some(adapter) = &self.adapter { + adapter.update(update); + } + } + + pub fn update_if_active(&self, updater: impl FnOnce() -> TreeUpdate) { + if let Some(adapter) = &self.adapter { + adapter.update(updater()); + } + } +} diff --git a/release-please-config.json b/release-please-config.json index a345b03a5..2a636a3e6 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -7,6 +7,7 @@ "common": {}, "consumer": {}, "platforms/macos": {}, + "platforms/unix": {}, "platforms/windows": {}, "platforms/winit": {} }