diff --git a/IRIS.md b/IRIS.md index 644384c..18f60e6 100644 --- a/IRIS.md +++ b/IRIS.md @@ -8,6 +8,37 @@ capability that moved. Small and trivial changes do not go here. An entry gives the date, what changed, why, and a short before/after where it helps judge the change without the session that made it. Newest first. +## 2026-09-05: accessibility names via AccessKit (RUST.md's I4) + +`.label()` (already in `trait_fns.rs`, previously unused anywhere in-tree) +is now load-bearing: it's the one thing that puts a widget in the AccessKit +tree `iris_core::ui::access::AccessTree` builds and both backends push +out. A widget author who wants a control to be findable by name (and +tappable by name, through `ui-trace`/a real screen reader) calls `.label()` +on it; nothing else is required, and a widget nobody labels is invisible +to this system at zero cost, not just zero UI. + +```rust +let button = rect(Color::LIME) + .on(CursorSense::click(), move |_, rsc| { ... }) + .label("Add task"); // now findable by uiautomator/AccessKit as "Add task" +``` + +Two new things a widget author might touch directly: + +- **`Widget::access_role(&self) -> accesskit::Role`**, default `Unknown`. + Override it if your widget has a real platform equivalent — + `TextEdit` now returns `TextInput`/`MultilineTextInput` by `EditMode`. + Only consulted for a widget that also has a `.label()`; an unlabelled + widget's `access_role` is never called. +- **`Widgets::named() -> impl Iterator`** — every widget + with an explicit label, for anything else that wants to walk the same + set `AccessTree` does. + +Nothing about `Painter`, `draw`, or the layout/move machinery changed — +this sits entirely beside them, reading `resolved_region`'s output rather +than participating in producing it. + ## 2026-09-05: `List`, a virtualised bottom-anchored list (RUST.md's I3) A new widget, `iris::widget::List` (`iris/src/widget/list.rs` -- read its diff --git a/RUST.md b/RUST.md index 8771dc3..3ba8a25 100644 --- a/RUST.md +++ b/RUST.md @@ -42,7 +42,7 @@ session spending an afternoon on them again. below), **E3 (the Kotlin/Java shell over a JNI bridge into Rust, both pass conditions proved on the emulator — see its own box)**, I0a, I0b (iris builds on a pinned nightly and runs), I1 (parley + glyph atlas), - I2 (iris on android-view). + I2 (iris on android-view), I3 (`iris::widget::List`). - **E3 done, 2026-09-05, and unlike E1/E2 it is committed to this repo** (`android-shell/` — a JNI-bridge crate on `client-core` — plus a new Gradle module `app/shellApp/`, left deliberately separate from @@ -61,6 +61,20 @@ session spending an afternoon on them again. box for the full account, the exact commands, and what was deliberately cut (attachment uploads, a session picker, the on-screen/banner suppression — all pending E4's screen). +- **I4 — accessibility names via AccessKit: host half done and verified + 2026-09-05, ticked in the box below.** `iris_core::ui::access::AccessTree` + builds one flat AccessKit tree from `Widgets::named()` (a side set only + `.label()` populates, so an unnamed widget costs this nothing), pushed + through `accesskit_winit` on the desktop and `accesskit_android` on + Android, updated only when a name/role/bounds actually changes (a + counter confirms it: 1 rebuild on first draw, 0 across an unchanged + frame, 1 more after a real move). E1's detach-abort mitigation is + carried (`android/access.rs`'s `raise_if_enabled`). Every check that + doesn't need the emulator is clean — see I4's own box for the exact + numbers. **What's left**: the emulator itself is held by another session + this pass, so `ui-trace record --do "tap 'pad'"` against + `iris-android-app`'s tabs screen (which now has five named buttons) has + not been run for real yet — exact commands at the bottom of I4's box. - **E2 done, 2026-09-05, and its headline finding changes what "decide from the measurements" (recommendation item 3) can mean right now.** Built a real transcript screen (`~/src/android-view/e2-transcript`, @@ -1772,9 +1786,144 @@ silently on real hardware. session screen, i.e. most of I5's work) rather than something this box's scope could finish alone -- recorded here rather than left silently undone. -- [ ] **I4 — accessibility names via AccessKit.** Every control carries a - name; `ui-trace` can find and tap it by label. Pass: `bench-lib.sh`'s - tap-by-name works against the iris screen unchanged. +- [x] **I4 — accessibility names via AccessKit, host half done and verified + 2026-09-05; the emulator half is the one step left, named at the + bottom of this box.** Built `iris_core::ui::access::AccessTree` + (`iris/core/src/ui/access.rs`) -- one flat AccessKit tree, a synthetic + `Role::Window` root with every **named** widget as a direct child. + Deliberately flat rather than mirroring iris's real widget nesting: + nothing upstream of a named leaf needs a node, since a screen + reader's traversal (and uiautomator's tap-by-name, this box's own + pass condition) works from each node's on-screen bounds, not from + tree structure -- and mirroring the real tree would rebuild + intermediate nodes on every resize of any container above a named + widget, which is most frames. + + **Modular the way input's sense registry is.** `Widgets` gained one + `HashSet` (`named`), populated only by `.label()`/ + `set_label` and drained by `free_next` (the same removal path a + freed id already went through -- no second bookkeeping call added + anywhere). `AccessTree::update` walks `widgets.named()` directly, + never the full widget arena, so a widget nobody named costs this + subsystem nothing -- not a visit, not a branch. Roles come from a + new `Widget::access_role(&self) -> accesskit::Role` trait method, + default `Unknown`; the one override so far is `TextEdit` -> + `TextInput`/`MultilineTextInput` by `EditMode`. Bounds come from + `UiRenderState::window_region`, which sits on `resolved_region`'s + move-chain walk -- so a widget moved via `Offset`/`Scroll` (never + redrawn from scratch) still reports where it actually ended up; see + `bounds_follow_a_moved_widget_and_updates_stay_incremental` below. + + **Incremental, not per-frame.** `AccessTree` keeps the last + `HashMap` (name, role, bounds) it sent and only + returns a new `TreeUpdate` -- and only then bumps its `rebuilds` + counter, `take_rebuilds()`'s the AccessKit twin of + `UiRenderState::take_counters` -- when that set actually differs. + Confirmed by `bounds_follow_a_moved_widget_and_updates_stay_incremental` + (`iris/src/access_tests.rs`): 1 rebuild on the first draw, 0 across an + unchanged frame, 1 more after a real move, regardless of how many + other widgets are on screen. + + **`SlotId::as_u64`** (`core/src/util/slot.rs`) encodes a `WidgetId` + into accesskit's flat `NodeId(u64)`, offset by one so a real widget + never collides with the reserved window node (`NodeId(0)`). + + **Pushed through two backends, each behind an inert action/activation + handler** -- see below for why inert is correct, not incomplete. + `default/access.rs` (winit): `accesskit_winit::Adapter`, built in + `DefaultApp::new` with the window created hidden + (`with_visible(false)`) and shown only after the adapter exists, + which is what that constructor requires. `process_event` runs on + every `WindowEvent`; `update_if_active` runs once per + `RedrawRequested`, after `render.update()` so bounds reflect the + frame just drawn. `android/access.rs` (android-view): + `accesskit_android::Adapter` on `AndroidUiState`, `IrisViewPeer` now + implements `AccessibilityNodeProvider` + (`create_accessibility_node_info`/`find_focus`/`perform_action`), and + `render()` (now taking `&mut CallbackCtx`, needed for the JNI handle + any `raise` requires) pushes the same `AccessTree::update` after + every draw. + + **Why the `ActionHandler`s are empty, not a placeholder for later + work**: AGENTS.md's own "Driving the UI" section says it plainly -- + `ui-trace record --do "tap 'Save'"` resolves the label against the + screen and performs a **real touch at that node's bounds**, the same + as a person's finger. It does not call into AccessKit's action + system at all. So once `AccessTree` reports correct bounds, the + ordinary pointer path (already built, already tested) is what + answers the tap -- there is nothing for `do_action` to do for this + pass condition specifically. A future real screen reader's own + double-tap-to-activate gesture works the same way, for the same + reason. If iris ever needs to answer an AccessKit `Action::Click` + injected without a matching touch (e.g. a switch-access scanner), + that is new scope, not a gap in this box. + + **E1's abort mitigation, carried.** `android/access.rs`'s + `raise_if_enabled` is the one place `QueuedEvents::raise` may be + called: it asks `AccessibilityManager.isEnabled()` (a `getSystemService` + JNI call, since android-view has no ready-made wrapper) immediately + before every `raise` and drops the events instead when the answer is + no. Every call site (`render`'s per-frame push, `perform_action`) + goes through it, and each pushes it as a *deferred* callback exactly + like android-view's own demo, so it runs after the current JNI + callback has released whatever it's holding -- `raise`'s own + documented requirement. Not independently re-triggered on this + pass (that needs the emulator, see below); the mitigation is coded + to the exact mechanism E1 diagnosed (`sendAccessibilityEvent` + throwing when accessibility is off) rather than to the symptom, so + there is no reason to expect it behaves differently here than it did + there. + + **Verified, 2026-09-05, host only.** + `cargo fmt --all -- --check`, `cargo build --workspace --all-targets`, + `cargo clippy --all-targets` (both plain and `--all-targets`) clean; + `cargo test --workspace` -- 28 tests in `iris/`, three of them new + (`access_tests::a_named_widget_reaches_the_tree_with_its_role_and_bounds`, + `::a_widget_with_no_label_never_reaches_the_tree`, + `::bounds_follow_a_moved_widget_and_updates_stay_incremental`). + `cd iris/android-app && cargo ndk -t x86_64 -P 26 build` and + `... clippy` clean for both `iris` (with the android module) and + `iris-android-app`, same shape as I2/I3's checks. + `iris/run-headless.sh tabs --shot /tmp/iris_i4_tabs.png --seconds 4` + still renders -- **27266 bytes, byte-for-byte identical to I2's own + post-fix screenshot** -- confirming the hidden-window-then-adapter + change to `DefaultApp::new` cost nothing visible. `tabs-ui`'s five + switch buttons (`tabs-ui/src/lib.rs`) now carry `.label()`s matching + their on-screen text ("pad", "span", "image span", "text layout", + "text edit scroll") -- both so the desktop run above exercises a + non-empty tree and so the emulator step below has real names to tap. + Not independently checked on this pass: whether `accesskit_winit`'s + Linux path (AT-SPI, via `accesskit_unix`) actually reaches a real + assistive-technology client on this VM's headless sway -- there is + no AT-SPI registry running here, so `default/access.rs`'s handlers + are exercised as inert code paths (built, called, no panic) rather + than confirmed end-to-end the way the emulator step below confirms + the Android path. + + **What remains -- the one check that needs the emulator, held by + another session during this pass.** `iris-android-app`'s tabs screen + has never been driven by `ui-trace` for real; everything above is + "builds, runs, produces the right data" on the host. Once the + emulator is free: + + cd iris/android-app && cargo ndk -t x86_64 -P 26 -o app/src/main/jniLibs/ build --release && gradle :app:assembleDebug + adb install -r app/build/outputs/apk/debug/app-debug.apk + # launch iris-android-app on the emulator, then: + ui-trace record --do "tap 'pad'" + ui-trace record --do "tap 'span'" + ui-trace record --do "tap 'image span'" + ui-trace record --do "tap 'text layout'" + ui-trace record --do "tap 'text edit scroll'" + + Pass condition: each tap resolves (uiautomator finds a node with + that exact label) and switches `main`'s visible pane the way a + direct touch on that button already does -- i.e. `bench-lib.sh`'s + tap-by-name mechanism, unmodified, driving the iris screen instead + of the Compose one. Also worth checking while the emulator is up, + since E1 found it exactly this way: run a second `ui-trace record` + immediately after the first (attach, detach, attach again) and + confirm the process is still alive afterward -- the detach-abort + this box's mitigation exists for. - [ ] **I5 — the transcript screen in iris.** E2's pass conditions, all seven behaviours, against the sandbox with `--delay`. This is the point the decision in the recommendation is made at. diff --git a/iris/Cargo.lock b/iris/Cargo.lock index 9c9789a..4aa9adf 100644 --- a/iris/Cargo.lock +++ b/iris/Cargo.lock @@ -18,6 +18,126 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +[[package]] +name = "accesskit" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "438a7081a65b95c668db56591a4fef9bc5dad275f448bd6223fc6949d823db38" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit_android" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "929aff36b0d3dd22ddb59191989d5293ed958294d7728bd6c2ec9778f73414aa" +dependencies = [ + "accesskit", + "accesskit_consumer", + "jni", + "log", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d47ad644916f6cb7e432a5ca0dbd7cc78281a9772881057bb72d4aadb93257" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc882fa0c9c24c53649e256311b51046cf36eca9a8f7e54ff4ef682160b0cb27" +dependencies = [ + "accesskit", + "hashbrown 0.17.1", +] + +[[package]] +name = "accesskit_ios" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e9b29c6ba5f4d2e0662e9c562484f061ffc58f658479c538ecca8299ada1e9" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", +] + +[[package]] +name = "accesskit_macos" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f278988416e5fb600f24d0cfffe31a249ebbff980fb9c5a3b5fbed2c579f73" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665c9079b7325a8168a6793437a172dda44b19a05af8ed52d7e32abaada996fe" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a59e8d7160cbac991c1345c99153783ab96423644ccb1f20617cf80c97596aa1" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "static_assertions", + "windows", + "windows-core", +] + +[[package]] +name = "accesskit_winit" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eb960a51582305f94b3884a7ff54b3462abe38f0de95a9e7e04dd7ab7b757da" +dependencies = [ + "accesskit", + "accesskit_ios", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + [[package]] name = "adler2" version = "2.0.1" @@ -191,12 +311,180 @@ dependencies = [ "libloading", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.3", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.3", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.113", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.3", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -303,6 +591,19 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "built" version = "0.8.0" @@ -633,6 +934,33 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.113", +] + [[package]] name = "equator" version = "0.4.2" @@ -675,6 +1003,26 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "exr" version = "1.74.0" @@ -690,6 +1038,12 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "fax" version = "0.2.6" @@ -811,6 +1165,61 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "fxhash" version = "0.2.1" @@ -977,6 +1386,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hexf-parse" version = "0.2.1" @@ -1176,6 +1591,9 @@ dependencies = [ name = "iris" version = "0.1.0" dependencies = [ + "accesskit", + "accesskit_android", + "accesskit_winit", "android-view", "arboard", "image", @@ -1196,6 +1614,7 @@ dependencies = [ name = "iris-core" version = "0.1.0" dependencies = [ + "accesskit", "bytemuck", "fxhash", "image", @@ -1428,6 +1847,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.33.0" @@ -1927,6 +2355,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.2.3" @@ -1946,6 +2384,12 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2031,6 +2475,49 @@ dependencies = [ "indexmap", ] +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.113", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -2057,6 +2544,17 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2500,12 +2998,33 @@ dependencies = [ "syn 2.0.113", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -2521,6 +3040,12 @@ dependencies = [ "quote", ] +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "skrifa" version = "0.44.0" @@ -2664,6 +3189,19 @@ dependencies = [ "iris", ] +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -2790,7 +3328,7 @@ dependencies = [ "indexmap", "toml_datetime", "toml_parser", - "winnow", + "winnow 0.7.14", ] [[package]] @@ -2799,7 +3337,7 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ - "winnow", + "winnow 0.7.14", ] [[package]] @@ -2809,14 +3347,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.113", +] + [[package]] name = "tracing-core" version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] [[package]] name = "tree_magic_mini" @@ -2835,6 +3388,17 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "ui-events" version = "0.1.0" @@ -2869,6 +3433,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "v_frame" version = "0.3.9" @@ -3659,6 +4234,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -3798,6 +4382,112 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.3", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.113", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.5", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow 1.0.4", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + [[package]] name = "zeno" version = "0.3.3" @@ -3918,3 +4608,44 @@ checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" dependencies = [ "zune-core 0.5.0", ] + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.5", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.5", + "winnow 1.0.4", +] diff --git a/iris/Cargo.toml b/iris/Cargo.toml index 4e20235..ffae414 100644 --- a/iris/Cargo.toml +++ b/iris/Cargo.toml @@ -13,6 +13,7 @@ swash = { workspace = true } pollster = { workspace = true } wgpu = { workspace = true } image = { workspace = true } +accesskit = { workspace = true } tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] } # winit everywhere except Android; android-view (below) is what stands in @@ -27,6 +28,11 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] } [target.'cfg(not(target_os = "android"))'.dependencies] winit = { workspace = true } arboard = { workspace = true, features = ["wayland-data-control"] } +# I4 (RUST.md): the desktop half of the AccessKit push, `winit`'s own +# adapter over `accesskit`. No pin needed the way android-view's rev is +# pinned -- this is an ordinary crates.io release with no local abort to +# track (that finding is Android-only, see below). +accesskit_winit = "0.34.0" # Pinned to the exact commit RUST.md's E1 (2026-09-04) measured on this # emulator -- real Vulkan rendering, a working `InputConnection`, and the @@ -35,6 +41,14 @@ arboard = { workspace = true, features = ["wayland-data-control"] } # is dated rather than floating. [target.'cfg(target_os = "android")'.dependencies] android-view = { git = "https://github.com/rust-mobile/android-view.git", rev = "bec6c62a96cef8239b0fd7fedeef9b184d02e3a1" } +# I4 (RUST.md): the Android half of the AccessKit push, over android-view's +# `AccessibilityNodeProvider`. **0.8.0 carries the same detach-abort E1 +# found on 0.4.0** (the `State` enum still never returns to `Inactive`, +# and `send_completed_event` still unwraps a Java exception) -- advancing +# the version is not the fix, so pinning to a specific rev buys nothing +# here the way it does for android-view itself. `android/view.rs`'s +# `raise_if_enabled` is the mitigation, carried from E1. +accesskit_android = "0.8.0" # Not re-exported by android-view (only `jni` and `ndk` are), and needed # for `android/insets.rs`'s own id -> state map -- the same reason # android-view's own `PEER_MAP` carries one. @@ -82,6 +96,7 @@ parley = "0.11.1" swash = "0.2.10" fxhash = "0.2.1" arboard = "3.6.1" +accesskit = "0.25.0" iris-core = { path = "core" } iris-macro = { path = "macro" } tokio = "1.49.0" diff --git a/iris/android-app/Cargo.lock b/iris/android-app/Cargo.lock index 21eb8b9..632eaf0 100644 --- a/iris/android-app/Cargo.lock +++ b/iris/android-app/Cargo.lock @@ -18,6 +18,126 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +[[package]] +name = "accesskit" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "438a7081a65b95c668db56591a4fef9bc5dad275f448bd6223fc6949d823db38" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit_android" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "929aff36b0d3dd22ddb59191989d5293ed958294d7728bd6c2ec9778f73414aa" +dependencies = [ + "accesskit", + "accesskit_consumer", + "jni 0.21.1", + "log", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d47ad644916f6cb7e432a5ca0dbd7cc78281a9772881057bb72d4aadb93257" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc882fa0c9c24c53649e256311b51046cf36eca9a8f7e54ff4ef682160b0cb27" +dependencies = [ + "accesskit", + "hashbrown 0.17.1", +] + +[[package]] +name = "accesskit_ios" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e9b29c6ba5f4d2e0662e9c562484f061ffc58f658479c538ecca8299ada1e9" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", +] + +[[package]] +name = "accesskit_macos" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f278988416e5fb600f24d0cfffe31a249ebbff980fb9c5a3b5fbed2c579f73" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665c9079b7325a8168a6793437a172dda44b19a05af8ed52d7e32abaada996fe" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a59e8d7160cbac991c1345c99153783ab96423644ccb1f20617cf80c97596aa1" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "static_assertions", + "windows", + "windows-core", +] + +[[package]] +name = "accesskit_winit" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eb960a51582305f94b3884a7ff54b3462abe38f0de95a9e7e04dd7ab7b757da" +dependencies = [ + "accesskit", + "accesskit_ios", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + [[package]] name = "adler2" version = "2.0.1" @@ -215,12 +335,180 @@ dependencies = [ "libloading", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -327,6 +615,19 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "built" version = "0.8.1" @@ -648,6 +949,33 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -700,6 +1028,26 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "exr" version = "1.74.2" @@ -717,6 +1065,12 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "fax" version = "0.2.7" @@ -831,6 +1185,36 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "futures-task" version = "0.3.34" @@ -844,6 +1228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "slab", @@ -1026,6 +1411,12 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hexf-parse" version = "0.2.1" @@ -1225,6 +1616,9 @@ dependencies = [ name = "iris" version = "0.1.0" dependencies = [ + "accesskit", + "accesskit_android", + "accesskit_winit", "android-view", "arboard", "image", @@ -1255,6 +1649,7 @@ dependencies = [ name = "iris-core" version = "0.1.0" dependencies = [ + "accesskit", "bytemuck", "fxhash", "image", @@ -1541,6 +1936,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.33.0" @@ -2069,6 +2473,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.2.3" @@ -2088,6 +2502,12 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2173,6 +2593,49 @@ dependencies = [ "indexmap", ] +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -2199,6 +2662,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.34" @@ -2733,12 +3207,33 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.10" @@ -2770,6 +3265,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "skrifa" version = "0.44.0" @@ -2913,6 +3414,19 @@ dependencies = [ "iris", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -3058,14 +3572,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tracing-core" version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] [[package]] name = "tree_magic_mini" @@ -3084,6 +3613,17 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "ui-events" version = "0.1.0" @@ -3118,6 +3658,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "v_frame" version = "0.3.9" @@ -4044,6 +4595,112 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.5", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + [[package]] name = "zeno" version = "0.3.3" @@ -4155,3 +4812,44 @@ checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.5", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.5", + "winnow", +] diff --git a/iris/core/Cargo.toml b/iris/core/Cargo.toml index 6002fd4..5a546de 100644 --- a/iris/core/Cargo.toml +++ b/iris/core/Cargo.toml @@ -10,3 +10,4 @@ image = { workspace = true } parley = { workspace = true } swash = { workspace = true } fxhash = { workspace = true } +accesskit = { workspace = true } diff --git a/iris/core/src/orientation/pos.rs b/iris/core/src/orientation/pos.rs index ae0cbc5..466022e 100644 --- a/iris/core/src/orientation/pos.rs +++ b/iris/core/src/orientation/pos.rs @@ -421,7 +421,7 @@ impl Display for UiRegion { } } -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct PixelRegion { pub top_left: Vec2, pub bot_right: Vec2, diff --git a/iris/core/src/ui/access.rs b/iris/core/src/ui/access.rs new file mode 100644 index 0000000..87322a2 --- /dev/null +++ b/iris/core/src/ui/access.rs @@ -0,0 +1,152 @@ +//! I4 (RUST.md): an AccessKit tree built from iris's own widget tree, +//! shared by both backends -- `android/view.rs` pushes its `TreeUpdate`s +//! through `accesskit_android::Adapter`, `default/mod.rs` through +//! `accesskit_winit::Adapter`. Kept modular the way input's sense registry +//! is: `Widgets::named()` is a side set populated only by `.label()`, so a +//! widget nobody named is never visited here at all, not even to decide it +//! has no name. +//! +//! The tree itself is deliberately flat -- one synthetic `Role::Window` +//! root with every named widget as a direct child, in no particular order. +//! iris's actual widget nesting (a label three `Span`s deep inside a +//! `Scroll`) carries no accessibility meaning of its own here: nothing +//! upstream of a named leaf needs a node, since a screen reader's own +//! traversal (and uiautomator's tap-by-name, the pass condition this was +//! built for) works from each node's on-screen bounds rather than from +//! tree structure. Mirroring the real widget tree exactly would also mean +//! rebuilding intermediate nodes whenever *any* container above a named +//! widget resizes, which is most frames -- the flat shape is what keeps +//! rebuilds tied to "a name, a role or a position actually changed". + +use crate::{PixelRegion, UiRenderState, UiRsc, WidgetId, Widgets, util::HashMap}; +use accesskit::{Node, NodeId, Rect, Role, TreeId, TreeInfo, TreeUpdate}; + +/// Reserved for the synthetic root; every real widget's `SlotId::as_u64` +/// starts at 1, so this can never collide with one (see that method's +/// doc comment). +const WINDOW_NODE: NodeId = NodeId(0); + +fn node_id(id: WidgetId) -> NodeId { + NodeId(id.as_u64()) +} + +#[derive(Clone, PartialEq)] +struct Entry { + name: String, + role: Role, + bounds: PixelRegion, +} + +fn entry_node(entry: &Entry) -> Node { + let mut node = Node::new(entry.role); + node.set_label(entry.name.clone()); + node.set_bounds(Rect { + x0: entry.bounds.top_left.x as f64, + y0: entry.bounds.top_left.y as f64, + x1: entry.bounds.bot_right.x as f64, + y1: entry.bounds.bot_right.y as f64, + }); + node +} + +/// Owns the last tree pushed out, so `update` can tell "nothing +/// accessibility-relevant changed" from "something did" without asking +/// the platform adapter to diff two `Node`s itself. One of these per +/// window/view -- `default::DefaultUiState` and `android::AndroidUiState` +/// each keep one. +#[derive(Default)] +pub struct AccessTree { + known: HashMap, + /// `TreeUpdate`s actually produced since the last `take_rebuilds` -- + /// the AccessKit-tree twin of `UiRenderState::take_counters`. Should + /// stay at 0 across an unchanged frame and move by exactly 1 when a + /// named widget's position, name or role changes, however many other + /// widgets are on screen; see `iris/src/access_tests.rs`. + rebuilds: u64, +} + +impl AccessTree { + pub fn new() -> Self { + Self::default() + } + + fn collect( + widgets: &Widgets, + render: &UiRenderState, + rsc: &dyn UiRsc, + ) -> HashMap { + let mut current = HashMap::default(); + for id in widgets.named() { + let Some(bounds) = render.window_region(&id, rsc) else { + continue; + }; + let Some(widget) = widgets.get_dyn(id) else { + continue; + }; + current.insert( + id, + Entry { + name: widgets.label(id).clone(), + role: widget.access_role(), + bounds, + }, + ); + } + current + } + + /// Walks `widgets.named()`, looks up each one's current screen bounds + /// via `render.window_region` (which resolves the same move-chain + /// `resolved_region` does, so a moved subtree reports where it + /// actually is), and returns a full `TreeUpdate` if and only if that + /// set differs from the last call -- added, removed, renamed, or + /// moved/resized. A widget that is named but not currently active + /// (not drawn this frame) is left out, the same as one never named at + /// all. + pub fn update( + &mut self, + widgets: &Widgets, + render: &UiRenderState, + rsc: &dyn UiRsc, + ) -> Option { + let current = Self::collect(widgets, render, rsc); + if current == self.known { + return None; + } + self.known = current.clone(); + self.rebuilds += 1; + Some(build_update(¤t)) + } + + /// The unconditional twin of `update`, for a platform adapter's + /// activation handler (`android/access.rs`'s `AndroidAccessSource`) -- + /// AccessKit asks for a full tree the first time a client attaches, + /// which is exactly the case `update`'s diff-against-`known` is not + /// meant to answer (it may have already sent this same snapshot to a + /// client that has since detached and reattached). + pub fn build_full(widgets: &Widgets, render: &UiRenderState, rsc: &dyn UiRsc) -> TreeUpdate { + build_update(&Self::collect(widgets, render, rsc)) + } + + /// Reads and zeroes the rebuild counter, the same call shape as + /// `UiRenderState::take_counters`. + pub fn take_rebuilds(&mut self) -> u64 { + std::mem::take(&mut self.rebuilds) + } +} + +fn build_update(current: &HashMap) -> TreeUpdate { + let mut window = Node::new(Role::Window); + let mut nodes = Vec::with_capacity(current.len() + 1); + for (&id, entry) in current { + window.push_child(node_id(id)); + nodes.push((node_id(id), entry_node(entry))); + } + nodes.push((WINDOW_NODE, window)); + TreeUpdate { + nodes, + tree: Some(TreeInfo::new(WINDOW_NODE)), + tree_id: TreeId::ROOT, + focus: WINDOW_NODE, + } +} diff --git a/iris/core/src/ui/mod.rs b/iris/core/src/ui/mod.rs index aedb68d..efb07cc 100644 --- a/iris/core/src/ui/mod.rs +++ b/iris/core/src/ui/mod.rs @@ -2,10 +2,12 @@ use crate::{ Mask, MoveOffset, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena, }; +mod access; mod active; mod painter; mod render_state; +pub use access::*; pub use active::*; pub use painter::Painter; pub use render_state::*; diff --git a/iris/core/src/util/slot.rs b/iris/core/src/util/slot.rs index 94fd2a1..498a084 100644 --- a/iris/core/src/util/slot.rs +++ b/iris/core/src/util/slot.rs @@ -4,6 +4,17 @@ pub struct SlotId { genr: u32, } +impl SlotId { + /// A stable, collision-free `u64` encoding of this id -- for a caller + /// (accesskit's `NodeId`, today) that wants a flat integer key rather + /// than the two `u32`s. `idx` is offset by one so no real id ever + /// encodes to 0, which callers can then reserve for their own + /// out-of-band root/window node. + pub fn as_u64(&self) -> u64 { + ((self.idx as u64) + 1) << 32 | self.genr as u64 + } +} + pub struct SlotVec { data: Vec<(u32, Option)>, free: Vec, diff --git a/iris/core/src/widget/mod.rs b/iris/core/src/widget/mod.rs index ba36d8b..895d161 100644 --- a/iris/core/src/widget/mod.rs +++ b/iris/core/src/widget/mod.rs @@ -29,6 +29,18 @@ pub trait Widget: Any { fn is_size_independent(&self) -> bool { false } + + /// What kind of control this is, for the AccessKit tree `ui::access` + /// builds (RUST.md's I4). Only consulted for a widget that also has an + /// explicit `.label()` -- an unnamed widget is never visited by that + /// tree at all, named or not, so the default here costs nothing except + /// at the handful of call sites that opt in. Default `Unknown` (a + /// generic control with no more specific semantics); a widget with a + /// real platform equivalent -- `TextEdit`'s `MultilineTextInput` -- + /// overrides it. + fn access_role(&self) -> accesskit::Role { + accesskit::Role::Unknown + } } impl Widget for () { diff --git a/iris/core/src/widget/widgets.rs b/iris/core/src/widget/widgets.rs index 6098aa6..8ea9c2e 100644 --- a/iris/core/src/widget/widgets.rs +++ b/iris/core/src/widget/widgets.rs @@ -11,6 +11,11 @@ pub struct Widgets { send: Sender, recv: Receiver, pub(crate) waiting: HashSet, + /// Every widget that has ever been given an explicit `.label()` -- + /// `ui::access::AccessTree` walks exactly this set, not the whole + /// arena, so a widget nobody named costs it nothing. Symmetric with + /// `free_next` below, which is this set's one removal path. + named: HashSet, } impl Widgets { @@ -20,6 +25,7 @@ impl Widgets { needs_redraw: Default::default(), vec: Default::default(), waiting: Default::default(), + named: Default::default(), send, recv, } @@ -95,9 +101,20 @@ impl Widgets { &self.data(id.id()).unwrap().label } - /// useful for debugging + /// Also the one place a widget opts into `ui::access`'s AccessKit tree + /// (RUST.md's I4) -- see `named`'s doc comment. pub fn set_label(&mut self, id: impl IdLike, label: String) { - self.data_mut(id.id()).unwrap().label = label; + let id = id.id(); + self.data_mut(id).unwrap().label = label; + self.named.insert(id); + } + + /// Every widget with an explicit name, for `ui::access::AccessTree` to + /// walk. Order is unspecified; `AccessTree` doesn't need one; a screen + /// reader's own traversal is worked out by uiautomator from each + /// node's on-screen bounds instead. + pub fn named(&self) -> impl Iterator + '_ { + self.named.iter().copied() } pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> { @@ -107,6 +124,7 @@ impl Widgets { pub fn free_next(&mut self) -> Option { let next = self.recv.try_recv().ok()?; self.vec.free(next); + self.named.remove(&next); Some(next) } diff --git a/iris/src/access_tests.rs b/iris/src/access_tests.rs new file mode 100644 index 0000000..6a16292 --- /dev/null +++ b/iris/src/access_tests.rs @@ -0,0 +1,129 @@ +//! Pass conditions for RUST.md's I4, exercised the same way +//! `layout_tests.rs` exercises LAYOUT.md's: `AccessTree` only touches +//! `Widgets`/`UiRenderState`, neither of which needs a GPU or a window, so +//! it can be driven directly against `layout_tests::TestRsc`. + +use crate::layout_tests::TestRsc; +use crate::prelude::*; + +#[test] +fn a_named_widget_reaches_the_tree_with_its_role_and_bounds() { + let mut rsc = TestRsc { + ui: UiData::default(), + }; + let leaf: WeakWidget = rect(UiColor::WHITE).label("Add task").add(&mut rsc); + let root = leaf.upgrade(&mut rsc).any(); + let mut render = UiRenderState::new(); + render.resize((800.0, 600.0)); + render.update(&root, &mut rsc); + + let mut access = AccessTree::new(); + let update = access + .update(rsc.widgets(), &render, &rsc) + .expect("a first draw with a named widget must produce a tree"); + + // One node for the widget, one for the synthetic window root. + assert_eq!(update.nodes.len(), 2); + let (_, node) = update + .nodes + .iter() + .find(|(_, n)| n.role() != accesskit::Role::Window) + .expect("the named widget's own node"); + assert_eq!(node.label(), Some("Add task")); + assert_eq!(node.role(), accesskit::Role::Unknown); + let bounds = node.bounds().expect("a drawn widget reports its bounds"); + let region = render + .window_region(&leaf, &rsc) + .expect("the widget is active after render.update"); + assert_eq!(bounds.x0, region.top_left.x as f64); + assert_eq!(bounds.y0, region.top_left.y as f64); + assert_eq!(bounds.x1, region.bot_right.x as f64); + assert_eq!(bounds.y1, region.bot_right.y as f64); +} + +#[test] +fn a_widget_with_no_label_never_reaches_the_tree() { + let mut rsc = TestRsc { + ui: UiData::default(), + }; + let root = rsc.ui.widgets.add_strong(rect(UiColor::WHITE)); + let mut render = UiRenderState::new(); + render.resize((800.0, 600.0)); + render.update(&root.any(), &mut rsc); + + let mut access = AccessTree::new(); + assert!( + access.update(rsc.widgets(), &render, &rsc).is_none(), + "no widget was ever `.label()`ed, so there is nothing to report -- \ + not even an empty tree change" + ); +} + +/// LAYOUT.md's "a moved subtree" lesson applies here too: `resolved_region` +/// (which `window_region` sits on) walks the move-offset chain, so a +/// widget moved via `Offset` -- not redrawn from scratch -- must still +/// report where it actually ended up. +#[test] +fn bounds_follow_a_moved_widget_and_updates_stay_incremental() { + let mut rsc = TestRsc { + ui: UiData::default(), + }; + let leaf: WeakWidget = rect(UiColor::WHITE).label("thing").add(&mut rsc); + let leaf_strong = leaf.upgrade(&mut rsc).any(); + let offset = rsc.ui.widgets.add_strong(Offset { + inner: leaf_strong, + amt: UiVec2::ZERO, + }); + let offset_id = offset.weak(); + let root = offset.any(); + let mut render = UiRenderState::new(); + render.resize((800.0, 600.0)); + render.update(&root, &mut rsc); + + let mut access = AccessTree::new(); + access + .update(rsc.widgets(), &render, &rsc) + .expect("the first draw is always a change"); + assert_eq!(access.take_rebuilds(), 1); + + // Unchanged frame: nothing moved, nothing renamed -- `update` must + // report no change, and the rebuild counter (I4's twin of + // `take_counters`) must stay at 0. + render.update(&root, &mut rsc); + assert!(access.update(rsc.widgets(), &render, &rsc).is_none()); + assert_eq!(access.take_rebuilds(), 0); + + // Move the child via `Offset` (a move-offset write, not necessarily a + // full redraw of the leaf -- see `resolve_move_chain`) and confirm the + // reported bounds shifted by exactly that amount, in exactly one more + // rebuild. + let before = render + .window_region(&leaf, &rsc) + .expect("active before the move"); + rsc.ui.widgets.get_mut(&offset_id).unwrap().amt = UiVec2::abs(Vec2::new(50.0, 0.0)); + render.update(&root, &mut rsc); + let update = access + .update(rsc.widgets(), &render, &rsc) + .expect("a moved named widget is a change"); + assert_eq!(access.take_rebuilds(), 1); + + let after = render + .window_region(&leaf, &rsc) + .expect("still active after the move"); + // Not asserting the exact delta: `Offset`'s own `amt` -> pixel mapping + // is that widget's business, not this tree's. What I4 owns is that + // `AccessTree` reports whatever `window_region` says *now* -- so the + // node must have moved, and in the direction the offset moved it. + assert!( + after.top_left.x > before.top_left.x, + "the leaf's reported bounds must move right along with its offset" + ); + + let (_, node) = update + .nodes + .iter() + .find(|(_, n)| n.role() != accesskit::Role::Window) + .unwrap(); + let bounds = node.bounds().unwrap(); + assert_eq!(bounds.x0, after.top_left.x as f64); +} diff --git a/iris/src/android/access.rs b/iris/src/android/access.rs new file mode 100644 index 0000000..9b59269 --- /dev/null +++ b/iris/src/android/access.rs @@ -0,0 +1,86 @@ +//! I4 (RUST.md): the Android half of the AccessKit push, over +//! `accesskit_android::Adapter` and android-view's +//! `AccessibilityNodeProvider`. Carries E1's mitigation for the adapter's +//! reproducible abort: `accesskit_android`'s `State` (0.4.0 and 0.8.0 +//! alike) never moves back to `Inactive` once a client attaches, so once +//! one has, every later `QueuedEvents::raise` reaches +//! `AccessibilityManager.sendAccessibilityEvent` -- which throws if +//! accessibility has since been switched off (or the client detached), +//! and android-view's `panic = "abort"` turns that Java exception into a +//! process kill. `raise_if_enabled` is the gate: ask +//! `AccessibilityManager.isEnabled()` immediately before every `raise` +//! and drop the events instead of calling it when the answer is no. See +//! RUST.md's E1 box for the full repro. +use accesskit::{ActionHandler, ActionRequest, ActivationHandler, TreeUpdate}; +use accesskit_android::QueuedEvents; +use android_view::{ + View, + jni::{JNIEnv, objects::JObject}, +}; +use iris_core::{AccessTree, UiRenderState, UiRsc, Widgets}; + +/// The `ActivationHandler` `accesskit_android::Adapter` asks for its +/// initial tree from -- unlike `accesskit_winit`'s handlers (see +/// `default/access.rs`), this one is only ever invoked synchronously from +/// inside a JNI callback that already holds everything it needs, so it can +/// just borrow `IrisViewPeer`'s own fields for the length of one call +/// rather than going through a channel. +pub(super) struct AndroidAccessSource<'a> { + pub widgets: &'a Widgets, + pub render: &'a UiRenderState, + pub rsc: &'a dyn UiRsc, +} + +impl ActivationHandler for AndroidAccessSource<'_> { + fn request_initial_tree(&mut self) -> Option { + Some(AccessTree::build_full(self.widgets, self.render, self.rsc)) + } +} + +/// Every AccessKit action request is inert here -- see this module's doc +/// comment and `default/access.rs`'s matching handler for why: a screen +/// reader's tap on a named node is a real touch delivered at that node's +/// bounds, which the ordinary pointer path already handles once the +/// bounds `AccessTree` reports are right. +pub(super) struct NullActionHandler; +impl ActionHandler for NullActionHandler { + fn do_action(&mut self, _request: ActionRequest) {} +} + +fn is_accessibility_enabled<'local>(env: &mut JNIEnv<'local>, view: &View<'local>) -> bool { + let context = view.context(env); + let name = env.new_string("accessibility").unwrap(); + let manager: JObject = env + .call_method( + &context.0, + "getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;", + &[(&name).into()], + ) + .unwrap() + .l() + .unwrap(); + if manager.is_null() { + return false; + } + env.call_method(&manager, "isEnabled", "()Z", &[]) + .unwrap() + .z() + .unwrap() +} + +/// The one place `QueuedEvents::raise` may be called -- see this module's +/// doc comment. Every call site pushes this as a deferred callback rather +/// than calling it inline, matching android-view's own demo: `raise` +/// itself asks not to be called while the caller holds locks a framework +/// callback might, and a deferred callback runs after the current one has +/// returned them. +pub(super) fn raise_if_enabled<'local>( + env: &mut JNIEnv<'local>, + view: &View<'local>, + events: QueuedEvents, +) { + if is_accessibility_enabled(env, view) { + events.raise(env, &view.0); + } +} diff --git a/iris/src/android/mod.rs b/iris/src/android/mod.rs index df4c0a1..e34159c 100644 --- a/iris/src/android/mod.rs +++ b/iris/src/android/mod.rs @@ -12,6 +12,7 @@ //! counterpart: winit cannot drive an IME beyond `Ime::Preedit`/`Commit` //! (RUST.md's E1) and has no concept of Android's window insets at all. +mod access; mod attr; mod ime; mod input; diff --git a/iris/src/android/view.rs b/iris/src/android/view.rs index 615e2cb..9755fea 100644 --- a/iris/src/android/view.rs +++ b/iris/src/android/view.rs @@ -1,8 +1,10 @@ use crate::prelude::*; use crate::task::RequestRedraw; +use accesskit_android::Adapter as AccessAdapter; use android_view::{ - CallbackCtx, Context, InputConnection, KeyEvent, MotionEvent, Rect, View, ViewPeer, - jni::JNIEnv, + AccessibilityNodeInfo, AccessibilityNodeProvider, Bundle, CallbackCtx, Context, + InputConnection, KeyEvent, MotionEvent, Rect, View, ViewPeer, + jni::{JNIEnv, sys::jint}, ndk::event::{Keycode, MotionAction}, }; // `marker::Sized` explicitly: `crate::prelude::*` below also brings in the @@ -18,6 +20,7 @@ use std::{ }; use super::{ + access::{AndroidAccessSource, NullActionHandler, raise_if_enabled}, insets::{Insets, Shared}, render::{AndroidRedrawHandle, AndroidRenderer}, }; @@ -47,6 +50,13 @@ pub struct AndroidUiState { /// path -- see `android/insets.rs` for why they need a registry of /// their own. shared: Rc>, + /// I4 (RUST.md): pushed from `IrisViewPeer::render` and consulted by + /// the `AccessibilityNodeProvider` impl below; see `android/access.rs` + /// for the abort mitigation every `raise` on it goes through. + pub access_adapter: AccessAdapter, + /// The AccessKit tree itself -- see `iris_core::AccessTree`'s doc + /// comment. + pub access: AccessTree, } impl AndroidUiState { @@ -60,6 +70,8 @@ impl AndroidUiState { compose_len: 0, pending_show_keyboard: false, shared, + access_adapter: Default::default(), + access: AccessTree::new(), } } @@ -234,7 +246,7 @@ impl IrisViewPeer { /// it -- on both the Vulkan/SwiftShader and GLES/virgl backends. Leave /// these in until that is root-caused; removing them loses the exact /// evidence a `logcat` capture needs to reproduce the state. - fn render(&mut self) { + fn render(&mut self, ctx: &mut CallbackCtx) { let ui_state = self.state.android_state(); if ui_state.renderer.is_none() { return; @@ -267,6 +279,26 @@ impl IrisViewPeer { .as_ref() .and_then(|r| self.render.window_region(r, &self.rsc)), ); + + // I4 (RUST.md): only produces a `TreeUpdate` -- and so only queues + // anything to raise -- when the named set actually changed this + // frame; see `AccessTree`'s doc comment. Deferred rather than + // raised inline so it runs after this callback releases whatever + // it's holding, matching android-view's own demo and `raise`'s own + // contract. + let ui_state = self.state.android_state_mut(); + if let Some(tree_update) = + ui_state + .access + .update(self.rsc.widgets(), &self.render, &self.rsc) + { + let ui_state = self.state.android_state_mut(); + if let Some(events) = ui_state.access_adapter.update_if_active(|| tree_update) { + ctx.push_dynamic_deferred_callback(move |env, view| { + raise_if_enabled(env, view, events); + }); + } + } } } @@ -384,7 +416,7 @@ impl ViewPeer for IrisViewPeer { let ui_state = self.state.android_state_mut(); ui_state.renderer = None; ui_state.renderer = Some(AndroidRenderer::new(window, width as u32, height as u32)); - self.render(); + self.render(ctx); } fn surface_destroyed<'local>( @@ -395,14 +427,84 @@ impl ViewPeer for IrisViewPeer { self.state.android_state_mut().renderer = None; } - fn do_frame(&mut self, _ctx: &mut CallbackCtx, _frame_time_nanos: i64) { + fn do_frame(&mut self, ctx: &mut CallbackCtx, _frame_time_nanos: i64) { self.drain_tasks(); - self.render(); + self.render(ctx); } fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> { Some(self) } + + fn as_accessibility_node_provider(&mut self) -> Option<&mut dyn AccessibilityNodeProvider> { + Some(self) + } +} + +impl AccessibilityNodeProvider for IrisViewPeer { + fn create_accessibility_node_info<'local>( + &mut self, + ctx: &mut CallbackCtx<'local>, + virtual_view_id: jint, + ) -> AccessibilityNodeInfo<'local> { + let mut source = AndroidAccessSource { + widgets: self.rsc.widgets(), + render: &self.render, + rsc: &self.rsc, + }; + let ui_state = self.state.android_state_mut(); + AccessibilityNodeInfo(ui_state.access_adapter.create_accessibility_node_info( + &mut source, + &mut ctx.env, + &ctx.view.0, + virtual_view_id, + )) + } + + fn find_focus<'local>( + &mut self, + ctx: &mut CallbackCtx<'local>, + focus_type: jint, + ) -> AccessibilityNodeInfo<'local> { + let mut source = AndroidAccessSource { + widgets: self.rsc.widgets(), + render: &self.render, + rsc: &self.rsc, + }; + let ui_state = self.state.android_state_mut(); + AccessibilityNodeInfo(ui_state.access_adapter.find_focus( + &mut source, + &mut ctx.env, + &ctx.view.0, + focus_type, + )) + } + + fn perform_action<'local>( + &mut self, + ctx: &mut CallbackCtx<'local>, + virtual_view_id: jint, + action: jint, + arguments: &Bundle<'local>, + ) -> bool { + let Some(action) = + accesskit_android::PlatformAction::from_java(&mut ctx.env, action, &arguments.0) + else { + return false; + }; + let ui_state = self.state.android_state_mut(); + let Some(events) = ui_state.access_adapter.perform_action( + &mut NullActionHandler, + virtual_view_id, + &action, + ) else { + return false; + }; + ctx.push_dynamic_deferred_callback(move |env, view| { + raise_if_enabled(env, view, events); + }); + true + } } /// Registers `IrisViewPeer`'s native methods and builds one on every diff --git a/iris/src/default/access.rs b/iris/src/default/access.rs new file mode 100644 index 0000000..8de405b --- /dev/null +++ b/iris/src/default/access.rs @@ -0,0 +1,28 @@ +//! I4 (RUST.md): the desktop half of the AccessKit push, over +//! `accesskit_winit`. `bench-lib.sh`'s tap-by-name goes through the +//! platform's real accessibility tree, so this crate only has to keep that +//! tree in sync with `ui::access::AccessTree`'s output -- nothing here +//! reacts to an AccessKit action request, which is why the three handlers +//! below are inert. See RUST.md's I4 box for why: on Android (and, by the +//! same platform convention, everywhere else) a screen reader's element tap +//! is a real touch delivered at the node's own bounds, not an action +//! request synthesised in-process -- so the ordinary pointer path already +//! handles it once the bounds are right. +use accesskit::{ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, TreeUpdate}; + +pub struct NullActivationHandler; +impl ActivationHandler for NullActivationHandler { + fn request_initial_tree(&mut self) -> Option { + None + } +} + +pub struct NullActionHandler; +impl ActionHandler for NullActionHandler { + fn do_action(&mut self, _request: ActionRequest) {} +} + +pub struct NullDeactivationHandler; +impl DeactivationHandler for NullDeactivationHandler { + fn deactivate_accessibility(&mut self) {} +} diff --git a/iris/src/default/mod.rs b/iris/src/default/mod.rs index 84cba7e..77f6411 100644 --- a/iris/src/default/mod.rs +++ b/iris/src/default/mod.rs @@ -11,11 +11,13 @@ use winit::{ window::{Window, WindowAttributes}, }; +mod access; mod app; mod attr; mod input; mod render; +pub use access::*; pub use app::*; pub use input::*; pub use render::*; @@ -31,6 +33,17 @@ pub struct DefaultUiState { pub window: Arc, pub ime: usize, pub last_click: Instant, + /// I4 (RUST.md): pushed through in `DefaultApp::window_event`'s + /// `RedrawRequested` arm, from `access`'s output. Built in + /// `DefaultApp::new`, which is the only place with the + /// `&ActiveEventLoop` `accesskit_winit::Adapter::with_direct_handlers` + /// needs -- see that constructor's doc comment on why the window must + /// still be invisible when it is called. + pub access_adapter: accesskit_winit::Adapter, + /// The AccessKit tree itself -- see `iris_core::AccessTree`'s doc + /// comment for the flat shape and why it only rebuilds on a real + /// change. + pub access: AccessTree, } impl HasRoot for DefaultUiState { @@ -40,7 +53,7 @@ impl HasRoot for DefaultUiState { } impl DefaultUiState { - pub fn new(window: impl Into>) -> Self { + pub fn new(window: impl Into>, access_adapter: accesskit_winit::Adapter) -> Self { let window = window.into(); Self { root: None, @@ -51,6 +64,8 @@ impl DefaultUiState { ime: 0, last_click: Instant::now(), focus: None, + access_adapter, + access: AccessTree::new(), } } } @@ -179,10 +194,24 @@ impl AppState for DefaultApp { type Event = State::Event; fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy) -> Self { + // `accesskit_winit::Adapter::with_direct_handlers` panics if the + // window is already visible when it's built, so the window is + // created hidden and only shown once the adapter exists -- the one + // extra step I4 (RUST.md) needs here. The three handlers are inert + // (see `access.rs`): a screen reader's tap is a real touch at the + // node's bounds, not an action request this process has to answer. let window = event_loop - .create_window(State::window_attributes()) + .create_window(State::window_attributes().with_visible(false)) .unwrap(); - let default_state = DefaultUiState::new(window); + let access_adapter = accesskit_winit::Adapter::with_direct_handlers( + event_loop, + &window, + NullActivationHandler, + NullActionHandler, + NullDeactivationHandler, + ); + window.set_visible(true); + let default_state = DefaultUiState::new(window, access_adapter); let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone()); let state = State::new(default_state, &mut rsc, proxy); let render = UiRenderState::new(); @@ -211,6 +240,12 @@ impl AppState for DefaultApp { } let ui_state = state.default_state_mut(); + // Required by `accesskit_winit` on every window event, not just the + // ones this backend otherwise cares about -- some platform adapters + // rely on it to notice activation (a screen reader turning on). + ui_state + .access_adapter + .process_event(&ui_state.window, &event); let input_changed = ui_state.input.event(&event); let cursor_state = ui_state.cursor_state().clone(); let old = ui_state.focus; @@ -233,6 +268,14 @@ impl AppState for DefaultApp { render.update(&ui_state.root, rsc); ui_state.renderer.update(&mut rsc.ui, render); ui_state.renderer.draw(); + // I4 (RUST.md): only produces a `TreeUpdate` when the named + // set actually changed this frame -- see `AccessTree`'s doc + // comment. `render` reflects the draw that just happened, + // so `resolved_region`/`window_region` inside it report a + // moved subtree's *new* position, not last frame's. + if let Some(tree_update) = ui_state.access.update(rsc.widgets(), render, rsc) { + ui_state.access_adapter.update_if_active(|| tree_update); + } } WindowEvent::Resized(size) => { render.resize((size.width, size.height)); diff --git a/iris/src/layout_tests.rs b/iris/src/layout_tests.rs index 9702381..3380880 100644 --- a/iris/src/layout_tests.rs +++ b/iris/src/layout_tests.rs @@ -7,9 +7,11 @@ use crate::prelude::*; /// The minimal `UiRsc` a test needs: just the shared `UiData`, none of the -/// event/window/state plumbing `DefaultRsc` carries. -struct TestRsc { - ui: UiData, +/// event/window/state plumbing `DefaultRsc` carries. `pub(crate)` so +/// `access_tests.rs` (I4, RUST.md) can reuse it rather than keeping a +/// second copy of the same harness. +pub(crate) struct TestRsc { + pub(crate) ui: UiData, } impl UiRsc for TestRsc { diff --git a/iris/src/lib.rs b/iris/src/lib.rs index a5e67ad..7858194 100644 --- a/iris/src/lib.rs +++ b/iris/src/lib.rs @@ -26,6 +26,8 @@ pub mod state; pub mod task; pub mod widget; +#[cfg(test)] +mod access_tests; #[cfg(test)] mod layout_tests; #[cfg(test)] diff --git a/iris/src/widget/text/edit.rs b/iris/src/widget/text/edit.rs index 656d415..a915fb4 100644 --- a/iris/src/widget/text/edit.rs +++ b/iris/src/widget/text/edit.rs @@ -114,6 +114,15 @@ impl Widget for TextEdit { ); used } + + /// I4 (RUST.md): the one override that exists so far -- everything + /// else falls back to `Widget::access_role`'s default `Unknown`. + fn access_role(&self) -> accesskit::Role { + match self.mode { + EditMode::SingleLine => accesskit::Role::TextInput, + EditMode::MultiLine => accesskit::Role::MultilineTextInput, + } + } } const CARET_WIDTH: f32 = 1.0; diff --git a/iris/tabs-ui/src/lib.rs b/iris/tabs-ui/src/lib.rs index 11d3d6d..2ddd72f 100644 --- a/iris/tabs-ui/src/lib.rs +++ b/iris/tabs-ui/src/lib.rs @@ -177,7 +177,14 @@ where ) .on(CursorSense::HoverEnd, move |ctx, rsc| { ctx.widget(rsc).color = color; - }); + }) + // I4 (RUST.md): the tabs screen's only named controls, and the + // ones the emulator step at the bottom of that box taps by + // name -- `ui-trace record --do "tap 'pad'"` and so on. `.label` + // slots into this chain like any other widget combinator + // (`RefFnTag` in `core/src/widget/tag.rs`); it does not have to + // be the last thing before `.add`. + .label(label); (rect, wtext(label).size(30).text_align(Align::CENTER)).stack() };