From 5031c47cb27e2801d849eb04543eb61bcd79de55 Mon Sep 17 00:00:00 2001 From: shuppy Date: Mon, 16 Mar 2026 17:34:54 +0800 Subject: [PATCH] Update accesskit to 0.24.0 (and related deps) (#7850) this patch updates our deps to [accesskit 0.24.0](https://docs.rs/accesskit/0.24.0/accesskit/) (was [0.21.1](https://docs.rs/accesskit/0.21.1/accesskit/)), [accesskit_consumer 0.35.0](https://docs.rs/accesskit_consumer/0.35.0/accesskit_consumer/) (was [0.30.1](https://docs.rs/accesskit_consumer/0.30.1/accesskit_consumer/)), and [accesskit_winit 0.32.0](https://docs.rs/accesskit_winit/0.32.0/accesskit_winit/) (was [0.29.1](https://docs.rs/accesskit_winit/0.29.1/accesskit_winit/)), allowing egui to be used in apps that use accessibility subtrees (AccessKit/accesskit#655). for now, we handle the subtree-related breaking changes by assuming that egui will use [the root tree](https://docs.rs/accesskit/0.24.0/accesskit/struct.TreeId.html#associatedconstant.ROOT), which is good enough for [servoshell](https://github.com/servo/servo) and does not require any API changes. * [x] I have followed the instructions in the PR template image --------- Co-authored-by: Luke Warlow Co-authored-by: lucasmerlin Co-authored-by: Arnold Loubriat --- Cargo.lock | 225 ++++++++++++------ Cargo.toml | 8 +- crates/egui-winit/src/lib.rs | 2 +- crates/egui/src/context.rs | 1 + crates/egui/src/id.rs | 2 +- crates/egui/src/input_state/mod.rs | 6 +- crates/egui/src/memory/mod.rs | 6 +- .../egui/src/text_selection/accesskit_text.rs | 209 +++++++++++----- .../egui/src/text_selection/cursor_range.rs | 38 ++- .../src/accessibility_inspector.rs | 30 ++- crates/egui_kittest/src/node.rs | 28 ++- deny.toml | 1 + 12 files changed, 383 insertions(+), 173 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62fd9b36c..1c22e9ddf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,47 +20,48 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "accesskit" -version = "0.21.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99" +checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a" dependencies = [ "enumn", "serde", + "uuid", ] [[package]] name = "accesskit_atspi_common" -version = "0.14.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f73a9b855b6f4af4962a94553ef0c092b80cf5e17038724d5e30945d036f69" +checksum = "842fd8203e6dfcf531d24f5bac792088edfba7d6b35844fead191603fb32a260" dependencies = [ "accesskit", "accesskit_consumer", "atspi-common", + "phf 0.13.1", "serde", - "thiserror 1.0.66", "zvariant", ] [[package]] name = "accesskit_consumer" -version = "0.30.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd06f5fea9819250fffd4debf926709f3593ac22f8c1541a2573e5ee0ca01cd" +checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" dependencies = [ "accesskit", - "hashbrown 0.15.2", + "hashbrown 0.16.1", ] [[package]] name = "accesskit_macos" -version = "0.22.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fbaf15815f39084e0cb24950c232f0e3634702c2dfbf182ae3b4919a4a1d45" +checksum = "534bc3fdc89a64a1db3c46b33c198fde2b7c3c7d094e5809c8c8bf2970c18243" dependencies = [ "accesskit", "accesskit_consumer", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -68,9 +69,9 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.17.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64926a930368d52d95422b822ede15014c04536cabaa2394f99567a1f4788dc6" +checksum = "90e549dd7c6562b6a2ea807b44726e6241707db054a817dc4c7e2b8d3b39bfac" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -86,23 +87,23 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.29.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "792991159fa9ba57459de59e12e918bb90c5346fea7d40ac1a11f8632b41e63a" +checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" dependencies = [ "accesskit", "accesskit_consumer", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "static_assertions", - "windows 0.61.1", - "windows-core 0.61.0", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] name = "accesskit_winit" -version = "0.29.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9db0ea66997e3f4eae4a5f2c6b6486cf206642639ee629dbbb860ace1dec87" +checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" dependencies = [ "accesskit", "accesskit_macos", @@ -452,20 +453,19 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atspi" -version = "0.25.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" dependencies = [ "atspi-common", - "atspi-connection", "atspi-proxies", ] [[package]] name = "atspi-common" -version = "0.9.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" dependencies = [ "enumflags2", "serde", @@ -477,23 +477,11 @@ dependencies = [ "zvariant", ] -[[package]] -name = "atspi-connection" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4193d51303d8332304056ae0004714256b46b6635a5c556109b319c0d3784938" -dependencies = [ - "atspi-common", - "atspi-proxies", - "futures-lite", - "zbus", -] - [[package]] name = "atspi-proxies" -version = "0.9.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" dependencies = [ "atspi-common", "serde", @@ -2484,12 +2472,10 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01fd6dd2cce251a360101038acb9334e3a50cd38cd02fefddbf28aa975f043c8" +source = "git+https://github.com/rerun-io/kittest?branch=main#ce7a2f3b12c36021889b50bdff671cec8016b0fb" dependencies = [ "accesskit", "accesskit_consumer", - "parking_lot", ] [[package]] @@ -2689,8 +2675,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" dependencies = [ "mime", - "phf", - "phf_shared", + "phf 0.11.3", + "phf_shared 0.11.3", "unicase", ] @@ -3269,8 +3255,19 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros", - "phf_shared", + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", ] [[package]] @@ -3279,8 +3276,8 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.3", + "phf_shared 0.11.3", ] [[package]] @@ -3289,24 +3286,47 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared", + "phf_shared 0.11.3", "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.3", + "phf_shared 0.11.3", "proc-macro2", "quote", "syn", "unicase", ] +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "phf_shared" version = "0.11.3" @@ -3317,6 +3337,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -4692,7 +4721,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d189085656ca1203291e965444e7f6a2723fbdd1dd9f34f8482e79bafd8338a0" dependencies = [ - "phf", + "phf 0.11.3", "unicode_names2_generator", ] @@ -5289,24 +5318,23 @@ dependencies = [ [[package]] name = "windows" -version = "0.61.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", - "windows-core 0.61.0", + "windows-core 0.62.2", "windows-future", - "windows-link 0.1.3", "windows-numerics", ] [[package]] name = "windows-collections" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core 0.61.0", + "windows-core 0.62.2", ] [[package]] @@ -5328,21 +5356,35 @@ version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-implement 0.60.0", - "windows-interface 0.59.1", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.2", "windows-strings 0.4.0", ] [[package]] -name = "windows-future" -version = "0.2.0" +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-core 0.61.0", - "windows-link 0.1.3", + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading", ] [[package]] @@ -5358,9 +5400,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -5380,9 +5422,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -5403,12 +5445,12 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core 0.61.0", - "windows-link 0.1.3", + "windows-core 0.62.2", + "windows-link 0.2.1", ] [[package]] @@ -5429,6 +5471,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -5448,6 +5499,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -5541,6 +5601,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -5897,9 +5966,9 @@ dependencies = [ [[package]] name = "zbus-lockstep" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a22426b1bc2aca91de97772506f0655fa373448e6010d79d5d5880915c388409" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" dependencies = [ "zbus_xml", "zvariant", @@ -5907,9 +5976,9 @@ dependencies = [ [[package]] name = "zbus-lockstep-macros" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "100ffec29ed51859052f4563061abe35557acb56ba574510571f8398efc70a29" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b10a4fd44..f083dd586 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,9 +68,9 @@ egui_glow = { version = "0.33.3", path = "crates/egui_glow", default-features = egui_kittest = { version = "0.33.3", path = "crates/egui_kittest", default-features = false } eframe = { version = "0.33.3", path = "crates/eframe", default-features = false } -accesskit = "0.21.1" -accesskit_consumer = "0.30.1" -accesskit_winit = "0.29.1" +accesskit = "0.24.0" +accesskit_consumer = "0.35.0" +accesskit_winit = "0.32.0" ahash = { version = "0.8.12", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead "std", @@ -148,6 +148,8 @@ wgpu = { version = "27.0.1", default-features = false, features = ["std"] } windows-sys = "0.61.2" winit = { version = "0.30.12", default-features = false } +[patch.crates-io] +kittest = { git = "https://github.com/rerun-io/kittest", branch = "main" } [workspace.lints.rust] unsafe_code = "deny" diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index c010febd5..243ed119a 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -102,7 +102,7 @@ pub struct State { has_sent_ime_enabled: bool, #[cfg(feature = "accesskit")] - accesskit: Option, + pub accesskit: Option, allow_ime: bool, ime_rect_px: Option, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 55e3d75e1..3b36150e4 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2616,6 +2616,7 @@ impl ContextImpl { platform_output.accesskit_update = Some(accesskit::TreeUpdate { nodes, tree: Some(accesskit::Tree::new(root_id)), + tree_id: accesskit::TreeId::ROOT, focus: focus_id, }); } diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 7484930c8..661bdf2bf 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -79,7 +79,7 @@ impl Id { self.0.get() } - pub(crate) fn accesskit_id(&self) -> accesskit::NodeId { + pub fn accesskit_id(&self) -> accesskit::NodeId { self.value().into() } diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 7122af699..b32a75c57 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -871,7 +871,8 @@ impl InputState { let accesskit_id = id.accesskit_id(); self.events.iter().filter_map(move |event| { if let Event::AccessKitActionRequest(request) = event - && request.target == accesskit_id + && request.target_node == accesskit_id + && request.target_tree == accesskit::TreeId::ROOT && request.action == action { return Some(request); @@ -888,7 +889,8 @@ impl InputState { let accesskit_id = id.accesskit_id(); self.events.retain(|event| { if let Event::AccessKitActionRequest(request) = event - && request.target == accesskit_id + && request.target_node == accesskit_id + && request.target_tree == accesskit::TreeId::ROOT { return !consume(request); } diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index fbc8e6f68..51ab2cde4 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -564,11 +564,13 @@ impl Focus { if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest { action: accesskit::Action::Focus, - target, + target_node, + target_tree, data: None, }) = event + && *target_tree == accesskit::TreeId::ROOT { - self.id_requested_by_accesskit = Some(*target); + self.id_requested_by_accesskit = Some(*target_node); } } } diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index 974a334d0..650e7e5c0 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -4,6 +4,26 @@ use crate::{Context, Galley, Id}; use super::{CCursorRange, text_cursor_state::is_word_char}; +/// AccessKit's `word_starts` uses `u8` indices, so text runs cannot exceed this length. +pub(crate) const MAX_CHARS_PER_TEXT_RUN: usize = 255; + +/// Convert a (row, column) layout cursor position to a text run node ID and character index, +/// accounting for rows that are split into multiple text runs. +fn text_run_position(parent_id: Id, row: usize, column: usize) -> accesskit::TextPosition { + // When column lands exactly on a chunk boundary (e.g., 255), it refers to + // the end of the previous chunk, not the start of a new one. + let chunk_index = if column > 0 && column.is_multiple_of(MAX_CHARS_PER_TEXT_RUN) { + column / MAX_CHARS_PER_TEXT_RUN - 1 + } else { + column / MAX_CHARS_PER_TEXT_RUN + }; + let character_index = column - chunk_index * MAX_CHARS_PER_TEXT_RUN; + accesskit::TextPosition { + node: parent_id.with(row).with(chunk_index).accesskit_id(), + character_index, + } +} + /// Update accesskit with the current text state. pub fn update_accesskit_for_text_widget( ctx: &Context, @@ -20,14 +40,8 @@ pub fn update_accesskit_for_text_widget( let anchor = galley.layout_from_cursor(cursor_range.secondary); let focus = galley.layout_from_cursor(cursor_range.primary); builder.set_text_selection(accesskit::TextSelection { - anchor: accesskit::TextPosition { - node: parent_id.with(anchor.row).accesskit_id(), - character_index: anchor.column, - }, - focus: accesskit::TextPosition { - node: parent_id.with(focus.row).accesskit_id(), - character_index: focus.column, - }, + anchor: text_run_position(parent_id, anchor.row, anchor.column), + focus: text_run_position(parent_id, focus.row, focus.column), }); } @@ -40,61 +54,144 @@ pub fn update_accesskit_for_text_widget( return; }; + let mut prev_row_ended_with_newline = true; + for (row_index, row) in galley.rows.iter().enumerate() { - let row_id = parent_id.with(row_index); + let glyph_count = row.glyphs.len(); + let mut value = String::with_capacity(glyph_count); + let mut character_lengths = Vec::::with_capacity(glyph_count); + let mut character_positions = Vec::::with_capacity(glyph_count); + let mut character_widths = Vec::::with_capacity(glyph_count); + let mut word_starts = Vec::::new(); + // For soft-wrapped continuation rows, treat the start as a word + // boundary so the first word character gets a `word_starts` entry. + // Paragraph-starting runs (first row or after a newline) get an + // implicit word start from AccessKit, so they don't need this. + let mut was_at_word_end = !prev_row_ended_with_newline; - ctx.register_accesskit_parent(row_id, parent_id); + for glyph in &row.glyphs { + let is_word_char = is_word_char(glyph.chr); + if is_word_char && was_at_word_end { + word_starts.push(character_lengths.len()); + } + was_at_word_end = !is_word_char; + let old_len = value.len(); + value.push(glyph.chr); + character_lengths.push((value.len() - old_len) as _); + character_positions.push(glyph.pos.x - row.pos.x); + character_widths.push(glyph.advance_width); + } - ctx.accesskit_node_builder(row_id, |builder| { - builder.set_role(accesskit::Role::TextRun); - let rect = global_from_galley * row.rect_without_leading_space(); - builder.set_bounds(accesskit::Rect { - x0: rect.min.x.into(), - y0: rect.min.y.into(), - x1: rect.max.x.into(), - y1: rect.max.y.into(), - }); - builder.set_text_direction(accesskit::TextDirection::LeftToRight); - // TODO(mwcampbell): Set more node fields for the row - // once AccessKit adapters expose text formatting info. + if row.ends_with_newline { + value.push('\n'); + character_lengths.push(1); + character_positions.push(row.size.x); + character_widths.push(0.0); + } - let glyph_count = row.glyphs.len(); - let mut value = String::new(); - value.reserve(glyph_count); - let mut character_lengths = Vec::::with_capacity(glyph_count); - let mut character_positions = Vec::::with_capacity(glyph_count); - let mut character_widths = Vec::::with_capacity(glyph_count); - let mut word_lengths = Vec::::new(); - let mut was_at_word_end = false; - let mut last_word_start = 0usize; + let total_chars = character_lengths.len(); - for glyph in &row.glyphs { - let is_word_char = is_word_char(glyph.chr); - if is_word_char && was_at_word_end { - word_lengths.push((character_lengths.len() - last_word_start) as _); - last_word_start = character_lengths.len(); + if total_chars <= MAX_CHARS_PER_TEXT_RUN { + let run_id = parent_id.with(row_index).with(0usize); + ctx.register_accesskit_parent(run_id, parent_id); + + ctx.accesskit_node_builder(run_id, |builder| { + builder.set_role(accesskit::Role::TextRun); + builder.set_text_direction(accesskit::TextDirection::LeftToRight); + // TODO(mwcampbell): Set more node fields for the row + // once AccessKit adapters expose text formatting info. + + let rect = global_from_galley * row.rect_without_leading_space(); + builder.set_bounds(accesskit::Rect { + x0: rect.min.x.into(), + y0: rect.min.y.into(), + x1: rect.max.x.into(), + y1: rect.max.y.into(), + }); + builder.set_value(value); + builder.set_character_lengths(character_lengths); + + let pos_offset = character_positions.first().copied().unwrap_or(0.0); + for p in &mut character_positions { + *p -= pos_offset; } - was_at_word_end = !is_word_char; - let old_len = value.len(); - value.push(glyph.chr); - character_lengths.push((value.len() - old_len) as _); - character_positions.push(glyph.pos.x - row.pos.x); - character_widths.push(glyph.advance_width); - } + builder.set_character_positions(character_positions); + builder.set_character_widths(character_widths); - if row.ends_with_newline { - value.push('\n'); - character_lengths.push(1); - character_positions.push(row.size.x); - character_widths.push(0.0); - } - word_lengths.push((character_lengths.len() - last_word_start) as _); + let chunk_word_starts: Vec = word_starts.iter().map(|&ws| ws as u8).collect(); + builder.set_word_starts(chunk_word_starts); + }); + } else { + let num_chunks = total_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN); + let mut byte_offset = 0usize; - builder.set_value(value); - builder.set_character_lengths(character_lengths); - builder.set_character_positions(character_positions); - builder.set_character_widths(character_widths); - builder.set_word_lengths(word_lengths); - }); + for chunk_idx in 0..num_chunks { + let char_start = chunk_idx * MAX_CHARS_PER_TEXT_RUN; + let char_end = (char_start + MAX_CHARS_PER_TEXT_RUN).min(total_chars); + + let byte_start = byte_offset; + let chunk_byte_len: usize = character_lengths[char_start..char_end] + .iter() + .map(|&l| l as usize) + .sum(); + let byte_end = byte_start + chunk_byte_len; + byte_offset = byte_end; + + let run_id = parent_id.with(row_index).with(chunk_idx); + ctx.register_accesskit_parent(run_id, parent_id); + + ctx.accesskit_node_builder(run_id, |builder| { + builder.set_role(accesskit::Role::TextRun); + builder.set_text_direction(accesskit::TextDirection::LeftToRight); + // TODO(mwcampbell): Set more node fields for the row + // once AccessKit adapters expose text formatting info. + + if chunk_idx > 0 { + let prev_id = parent_id.with(row_index).with(chunk_idx - 1); + builder.set_previous_on_line(prev_id.accesskit_id()); + } + if chunk_idx + 1 < num_chunks { + let next_id = parent_id.with(row_index).with(chunk_idx + 1); + builder.set_next_on_line(next_id.accesskit_id()); + } + + let row_rect = row.rect_without_leading_space(); + let chunk_x0 = row.pos.x + character_positions[char_start]; + let chunk_x1 = row.pos.x + + character_positions[char_end - 1] + + character_widths[char_end - 1]; + let chunk_rect = emath::Rect::from_min_max( + emath::pos2(chunk_x0, row_rect.min.y), + emath::pos2(chunk_x1, row_rect.max.y), + ); + let rect = global_from_galley * chunk_rect; + builder.set_bounds(accesskit::Rect { + x0: rect.min.x.into(), + y0: rect.min.y.into(), + x1: rect.max.x.into(), + y1: rect.max.y.into(), + }); + builder.set_value(value[byte_start..byte_end].to_owned()); + builder.set_character_lengths(character_lengths[char_start..char_end].to_vec()); + + let pos_offset = character_positions[char_start]; + let chunk_positions: Vec = character_positions[char_start..char_end] + .iter() + .map(|&p| p - pos_offset) + .collect(); + builder.set_character_positions(chunk_positions); + builder.set_character_widths(character_widths[char_start..char_end].to_vec()); + + let chunk_word_starts: Vec = word_starts + .iter() + .filter(|&&ws| ws >= char_start && ws < char_end) + .map(|&ws| (ws - char_start) as u8) + .collect(); + builder.set_word_starts(chunk_word_starts); + }); + } + } + + prev_row_ended_with_newline = row.ends_with_newline; } } diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs index a816f5f26..f0445217f 100644 --- a/crates/egui/src/text_selection/cursor_range.rs +++ b/crates/egui/src/text_selection/cursor_range.rs @@ -192,10 +192,13 @@ impl CCursorRange { Event::AccessKitActionRequest(accesskit::ActionRequest { action: accesskit::Action::SetTextSelection, - target, + target_node, + target_tree, data: Some(accesskit::ActionData::SetTextSelection(selection)), }) => { - if _widget_id.accesskit_id() == *target { + if _widget_id.accesskit_id() == *target_node + && *target_tree == accesskit::TreeId::ROOT + { let primary = ccursor_from_accesskit_text_position(_widget_id, galley, &selection.focus); let secondary = @@ -224,18 +227,31 @@ fn ccursor_from_accesskit_text_position( galley: &Galley, position: &accesskit::TextPosition, ) -> Option { + use super::accesskit_text::MAX_CHARS_PER_TEXT_RUN; + let mut total_length = 0usize; for (i, row) in galley.rows.iter().enumerate() { - let row_id = id.with(i); - if row_id.accesskit_id() == position.node { - return Some(CCursor { - index: total_length + position.character_index, - prefer_next_row: !(position.character_index == row.glyphs.len() - && !row.ends_with_newline - && (i + 1) < galley.rows.len()), - }); + let row_chars = row.glyphs.len() + (row.ends_with_newline as usize); + let num_chunks = if row_chars == 0 { + 1 + } else { + row_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN) + }; + + for chunk_idx in 0..num_chunks { + let run_id = id.with(i).with(chunk_idx); + if run_id.accesskit_id() == position.node { + let column = chunk_idx * MAX_CHARS_PER_TEXT_RUN + position.character_index; + return Some(CCursor { + index: total_length + column, + prefer_next_row: !(column == row.glyphs.len() + && !row.ends_with_newline + && (i + 1) < galley.rows.len()), + }); + } } - total_length += row.glyphs.len() + (row.ends_with_newline as usize); + + total_length += row_chars; } None } diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index 138c05ede..db34c85ac 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -1,7 +1,7 @@ use std::mem; -use accesskit::{Action, ActionRequest, NodeId}; -use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler}; +use accesskit::{Action, ActionRequest}; +use accesskit_consumer::{FilterResult, Node, NodeId, Tree, TreeChangeHandler}; use eframe::epaint::text::TextWrapMode; use egui::{ @@ -25,7 +25,7 @@ use egui::{ pub struct AccessibilityInspectorPlugin { pub open: bool, tree: Option, - selected_node: Option, + selected_node: Option, queued_action: Option, } @@ -113,13 +113,17 @@ impl AccessibilityInspectorPlugin { Id::new("Accessibility Inspector") } - fn selection_ui(&mut self, ui: &mut Ui, selected_node: Id) { + fn selection_ui(&mut self, ui: &mut Ui, selected_node: NodeId) { ui.separator(); if let Some(tree) = &self.tree - && let Some(node) = tree.state().node_by_id(NodeId::from(selected_node.value())) + && let Some(node) = tree.state().node_by_id(selected_node) { - let node_response = ui.ctx().read_response(selected_node); + // Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`. + #[expect(unsafe_code)] + let egui_node_id = unsafe { Id::from_high_entropy_bits(node.locate().0.0) }; + + let node_response = ui.ctx().read_response(egui_node_id); if let Some(widget_response) = node_response { ui.debug_painter().debug_rect( @@ -174,8 +178,10 @@ impl AccessibilityInspectorPlugin { if node.supports_action(action, &|_node| FilterResult::Include) && ui.button(format!("{action:?}")).clicked() { + let (target_node, target_tree) = node.locate(); let action_request = ActionRequest { - target: node.id(), + target_node, + target_tree, action, data: None, }; @@ -188,8 +194,8 @@ impl AccessibilityInspectorPlugin { } } - fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option) { - if node.id() == Self::id().value().into() + fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option) { + if node.locate() == (Self::id().value().into(), accesskit::TreeId::ROOT) || node .value() .as_deref() @@ -200,12 +206,12 @@ impl AccessibilityInspectorPlugin { let label = node .label() .or_else(|| node.value()) - .unwrap_or_else(|| node.id().0.to_string()); + .unwrap_or_else(|| node.locate().0.0.to_string()); let label = format!("({:?}) {}", node.role(), label); // Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`. #[expect(unsafe_code)] - let egui_node_id = unsafe { Id::from_high_entropy_bits(node.id().0) }; + let egui_node_id = unsafe { Id::from_high_entropy_bits(node.locate().0.0) }; ui.push_id(node.id(), |ui| { let child_count = node.children().len(); @@ -228,7 +234,7 @@ impl AccessibilityInspectorPlugin { collapsing.set_open(!collapsing.is_open()); } let label_response = - ui.selectable_value(selected_node, Some(egui_node_id), label.clone()); + ui.selectable_value(selected_node, Some(node.id()), label.clone()); if label_response.hovered() { let widget_response = ui.ctx().read_response(egui_node_id); diff --git a/crates/egui_kittest/src/node.rs b/crates/egui_kittest/src/node.rs index 94940ffff..ed68d50ee 100644 --- a/crates/egui_kittest/src/node.rs +++ b/crates/egui_kittest/src/node.rs @@ -98,9 +98,11 @@ impl Node<'_> { /// This will trigger a [`accesskit::Action::Click`] action. /// In contrast to `click()`, this can also click widgets that are not currently visible. pub fn click_accesskit(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest( accesskit::ActionRequest { - target: self.accesskit_node.id(), + target_node, + target_tree, action: accesskit::Action::Click, data: None, }, @@ -119,9 +121,11 @@ impl Node<'_> { } pub fn focus(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::Focus, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } @@ -162,45 +166,55 @@ impl Node<'_> { /// Scroll the node into view. pub fn scroll_to_me(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::ScrollIntoView, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } /// Scroll the [`egui::ScrollArea`] containing this node down (100px). pub fn scroll_down(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::ScrollDown, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } /// Scroll the [`egui::ScrollArea`] containing this node up (100px). pub fn scroll_up(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::ScrollUp, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } /// Scroll the [`egui::ScrollArea`] containing this node left (100px). pub fn scroll_left(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::ScrollLeft, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } /// Scroll the [`egui::ScrollArea`] containing this node right (100px). pub fn scroll_right(&self) { + let (target_node, target_tree) = self.accesskit_node.locate(); self.event(egui::Event::AccessKitActionRequest(ActionRequest { action: accesskit::Action::ScrollRight, - target: self.accesskit_node.id(), + target_node, + target_tree, data: None, })); } diff --git a/deny.toml b/deny.toml index e740494fb..845aa847c 100644 --- a/deny.toml +++ b/deny.toml @@ -64,6 +64,7 @@ skip-tree = [ { name = "hashbrown" }, # wgpu's naga depends on 0.16, accesskit depends on 0.15 { name = "rfd" }, # example dependency { name = "windows" }, # the ecosystem is currently transitioning from 0.58 to 0.61 + { name = "phf" }, # mime_guess2, unicode_names2 -> 0.11.3; accesskit -> 0.13.1 ]